تطوير الويب

استهلاك API REST في Swift بـ async/await وURLSession

3 min de lecture

📘 الدليل الرئيسي: تطوير تطبيق iOS بـ Swift وSwiftUI: بانوراما 2026. دليل @Observable مكمِّل مفيد لهيكلة view model للعميل.

كلّ app ينتهي به المطاف بمحادثة خادم. استرداد قائمة، إرسال نموذج، تنزيل ملفّ — كلّها سيناريوهات تمرّ بالشبكة. الطبقة الرسمية في Swift لهذه العمليات تُسَمّى URLSession. منذ Swift 5.5 وiOS 15، تكشف واجهة async/await كاملة تجعل كود الشبكة خطّيًّا كاستدعاء متزامن، دون قصور completion handlers. يبني هذا الدليل خطوة بخطوة عميل REST مُختزَلًا يستهلك API عامًّا، يُدير الأخطاء، ويُغَذِّي view SwiftUI.

المتطلّبات

  • Xcode 26 على macOS Tahoe.
  • هدف iOS 15 كحدّ أدنى لـ async/await على URLSession.
  • أساسيات Swift: structs، optionals، closures — دليل الأساسيات.
  • اتّصال إنترنت للاختبار ضدّ API عامّ.
  • الوقت المتوقّع: 70 إلى 100 دقيقة.

الخطوة 1 — اختيار API اختباري

للتعلّم دون استيثاق، عدّة APIs عامّة تعرض endpoint مستقرًّا وقابلًا للقراءة. في هذا الدليل، نستعمل https://jsonplaceholder.typicode.com، خدمة يصونها المجتمع تُرجع بيانات صُورِيَّة للاختبارات: posts، users، comments، todos. لا token مطلوب، زمن استجابة منخفض، مخطّطات بسيطة.

curl https://jsonplaceholder.typicode.com/posts/1

الردّ كائن JSON بأربعة حقول: userId، id، title، body. هذا تمامًا الشكل الذي سنُنَمذجه في Swift عبر struct Codable. حالة HTTP المُرجَعة 200 وcontent-type application/json; charset=utf-8.

الخطوة 2 — نمذجة الردّ في Swift

فكّ ترميز JSON التلقائي يمرّ ببروتوكول Codable. struct تطابق Codable يمكن إنشاؤها من Data JSON دون كود parsing يدوي.

struct Post: Codable, Identifiable {
    let userId: Int
    let id: Int
    let title: String
    let body: String
}

هذا struct يُجَمَّع كما هو. Codable اسم مستعار لـ Encodable & Decodable. Identifiable يُتيح لـ ForEach وList إدارة القائمة دون id: صريح.

الخطوة 3 — كتابة عميل الشبكة

import Foundation

enum APIError: Error {
    case mauvaiseURL
    case mauvaiseReponse(statut: Int)
    case decodage(Error)
    case reseau(Error)
}

struct ClientAPI {
    static let baseURL = URL(string: "https://jsonplaceholder.typicode.com")!

    func fetchPosts() async throws -> [Post] {
        let url = Self.baseURL.appending(path: "/posts")
        let (data, response) = try await URLSession.shared.data(from: url)

        guard let httpResponse = response as? HTTPURLResponse,
              (200..<300).contains(httpResponse.statusCode) else {
            let code = (response as? HTTPURLResponse)?.statusCode ?? -1
            throw APIError.mauvaiseReponse(statut: code)
        }

        do {
            return try JSONDecoder().decode([Post].self, from: data)
        } catch {
            throw APIError.decodage(error)
        }
    }
}

الدالّة data(from:) لـ URLSession تُرجع tuple (Data, URLResponse). الـ guard يُصادِق على حالة HTTP — استدعاء يُرجع 404 أو 500 يجب رفع خطأ صريح، وإلّا حاولنا فكّ ترميز صفحة خطأ HTML كأنّها JSON.

الخطوة 4 — استدعاء العميل من view SwiftUI

الاستدعاء يتمّ في modifier .task، الذي يُشَغِّل مهمّة غير متزامنة مرتبطة بدورة حياة الـ view. حين تختفي الـ view، تُلغى المهمّة تلقائيًّا.

struct PostsView: View {
    @State private var posts: [Post] = []
    @State private var enChargement: Bool = false
    @State private var erreur: String? = nil

    var body: some View {
        NavigationStack {
            Group {
                if enChargement {
                    ProgressView("Chargement…")
                } else if let erreur {
                    VStack {
                        Image(systemName: "exclamationmark.triangle")
                            .font(.largeTitle)
                        Text(erreur).foregroundStyle(.secondary)
                        Button("Réessayer") { Task { await charger() } }
                    }
                } else {
                    List(posts) { post in
                        VStack(alignment: .leading) {
                            Text(post.title).font(.headline)
                            Text(post.body).font(.subheadline).foregroundStyle(.secondary)
                        }
                    }
                }
            }
            .navigationTitle("Posts")
            .task { await charger() }
        }
    }

    private func charger() async {
        enChargement = true
        erreur = nil
        do {
            posts = try await ClientAPI().fetchPosts()
        } catch let e as APIError {
            erreur = description(e)
        } catch {
            erreur = error.localizedDescription
        }
        enChargement = false
    }

    private func description(_ e: APIError) -> String {
        switch e {
        case .mauvaiseURL: return "URL invalide"
        case .mauvaiseReponse(let s): return "Erreur HTTP \(s)"
        case .decodage: return "Réponse mal formée"
        case .reseau(let err): return err.localizedDescription
        }
    }
}

ثلاث حالات تتعايش: التحميل، الخطأ، النجاح. الدالّة charger() async و.task { await charger() } تُشَغِّلها فور ظهور الـ view.

الخطوة 5 — إرسال POST بجسد JSON

struct NouveauPost: Encodable {
    let title: String
    let body: String
    let userId: Int
}

func creerPost(_ p: NouveauPost) async throws -> Post {
    let url = Self.baseURL.appending(path: "/posts")
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(p)

    let (data, response) = try await URLSession.shared.data(for: request)
    guard let http = response as? HTTPURLResponse, http.statusCode == 201 else {
        throw APIError.mauvaiseReponse(statut: (response as? HTTPURLResponse)?.statusCode ?? -1)
    }
    return try JSONDecoder().decode(Post.self, from: data)
}

data(for:) يقبل URLRequest بدل URL بسيط. الحالة المنتظرة لـ POST /posts على jsonplaceholder هي 201 Created؛ جسد الردّ يحوي المنشور المُنشأ مع id مُسَنَد من الخادم.

الخطوة 6 — إدارة الإلغاء والتزامن

مهمّة Swift غير متزامنة يمكن إلغاؤها من أبيها. هذا الإلغاء يُنتشَر: URLSession يكتشف cancel ويقطع الطلب الشبكي.

@State private var tacheCourante: Task? = nil

func charger() {
    tacheCourante?.cancel()
    tacheCourante = Task {
        do {
            posts = try await ClientAPI().fetchPosts()
        } catch is CancellationError {
            // إلغاء نظيف، لا شيء
        } catch {
            erreur = error.localizedDescription
        }
    }
}

الـ Task المُنشأ حديثًا يحلّ محلّ القديم — إن استُدعي charger() سريعًا عدّة مرّات (swipe-to-refresh عدواني)، فقط آخر طلب يُحتَرَم. catch is CancellationError يعترض الإلغاء تحديدًا دون خلطه بخطأ شبكي.

الخطوة 7 — فكّ ترميز JSON بمفاتيح snake_case

كثير من APIs الواقعية يستعمل snake_case (user_id بدل userId). JSONDecoder القياسي يُحَوِّل تلقائيًّا بين الأسلوبَين عبر keyDecodingStrategy.

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
let posts = try decoder.decode([Post].self, from: data)

مع .convertFromSnakeCase، المفتاح user_id يُربط تلقائيًّا بخاصية Swift userId. .iso8601 يفكّ تشفير التواريخ بصيغة "2026-05-17T09:00:00Z" إلى Date Swift دون formatter يدوي.

الخطوة 8 — الاختبار ضدّ API حقيقي

extension Bundle {
    var baseAPIURL: URL {
        guard let s = object(forInfoDictionaryKey: "API_BASE_URL") as? String,
              let url = URL(string: s) else {
            fatalError("API_BASE_URL absent dans Info.plist")
        }
        return url
    }
}

fatalError في حالة إعداد مفقود قرار براغماتي: binary سيّء الإعداد يجب أن يفشل أبكر ما يمكن، لا إرسال طلبات إلى URL وهمي.

أنماط لهيكلة عميل API يكبر

العميل المُختزَل في هذا الدليل يكفي لبضعة endpoints. لـ app يستهلك عشرات الـ routes، عدّة أنماط تُساعد. الأوّل فصل النقل (URLSession، headers، retry) عن الميدان (الـ routes الخاصّة). طبقة منخفضة HTTPClient تُدير data(for:)، الاستيثاق، وتحويل أخطاء HTTP إلى أنواع Swift؛ طبقة عمل أعلى تستدعي HTTPClient وتعرض دوالّ مُسَمَّاة مثل fetchPosts()، updateProfile(_:)، deleteOrder(id:).

النمط الثاني المفيد إدخال نوع APIEndpoint يصف كلّ route عبر enum أو struct: طريقة HTTP، مسار، معاملات استعلام، جسد. العميل العامّ يستهلك هذا endpoint ويُنتج URLRequest المقابلة. بهذا التصميم، إضافة route جديد يصير إضافة حالة enum، وتوقيع العميل يبقى مستقرًّا.

أخطاء شائعة في شبكة Swift

الخطأ السبب الحلّ
"App Transport Security has blocked a cleartext HTTP connection" URL بـ http:// بدل https:// انتقل إلى HTTPS؛ ATS Exception عبر Info.plist للتطوير المحلّي فقط
"The data couldn't be read because it is missing" خطأ فكّ ترميز على مفتاح متوقَّع غائب وَسم الخاصية اختياريّة (let title: String?)
crash على URL(string: …)! محارف خاصّة غير مُرَمَّزة استعمل URLComponents وaddingPercentEncoding
"Task was cancelled" في console view مُدَمَّرة قبل نهاية الطلب سلوك طبيعي لـ .task؛ اعترض CancellationError إن لزم
headers استيثاق مُتجاهَلة reset بعد كلّ استدعاء على URLSession.shared استعمل URLSessionConfiguration مخصَّص ومَرِّره إلى URLSession(configuration:)
ردّ 401 صامت token منتهي غير مُعالَج نَفِّذ تحقّقًا من رمز الحالة وrefresh token صريحًا

أسئلة شائعة

URLSession.shared أم instance مخصَّص؟ URLSession.shared يكفي 90% من الحالات. instance مخصَّص مفيد لإعداد محدّد (timeout مخصَّص، header افتراضي، cache منفصل، certificate pinning).

كيف نُضيف header استيثاق؟ إمّا على كلّ URLRequest عبر setValue(_:forHTTPHeaderField:)، أو شموليًّا عبر URLSessionConfiguration.default.httpAdditionalHeaders. الثاني أنظف لـ token مستعمَل في كلّ مكان.

لماذا async/await بدل completion handlers؟ الكود الخطّي async/await يتفادى التداخل الكلاسيكي للـ completion handlers (callback hell). الأخطاء تنتقل طبيعيًّا عبر throws. الإلغاء مدمج في النظام. المُجَمِّع يتحقّق ساكنًا من data races.

أيّ مكتبة لـ certificate pinning؟ URLSession يعرض URLSessionDelegate مع urlSession(_:didReceive:completionHandler:) للمصادقة اليدوية. لحالات متقدّمة، TrustKit مكتبة مفتوحة المصدر.

URLSession أم Alamofire؟ URLSession يكفي للغالبية العظمى — لا اعتمادية خارجية، مدمج كلّيًّا في النظام، دعم رسمي Apple. Alamofire يبقى مفيدًا لـ interceptors مُعَقَّدة، retry تلقائي متطوّر، أو فريق مألوف بالواجهة.

كيف نُنَزِّل ملفًّا كبيرًا دون إغراق الذاكرة؟ استعمل download(from:) بدل data(from:). النتيجة URL يُشير إلى ملفّ مؤقّت على القرص، لا Data في RAM. أساسي للملفّات بعشرات الميغا.

الأدلّة الموصى بها بعد هذا

مصادر رسمية

  • توثيق URLSession.
  • توثيق JSONDecoder.
  • توثيق Swift Concurrency.
  • Swift 6.2 — التزامن القابل للتقريب.

🔝 العودة إلى الدليل الرئيسي.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité