📘 الدليل الرئيسي: تطوير تطبيق 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. أساسي للملفّات بعشرات الميغا.
الأدلّة الموصى بها بعد هذا
- اختبار app SwiftUI بـ Swift Testing وXcode Previews — كتابة اختبارات لعميل API.
- إدارة حالة app بـ @Observable — هيكلة view model الذي يستهلك API.
مصادر رسمية
- توثيق URLSession.
- توثيق JSONDecoder.
- توثيق Swift Concurrency.
- Swift 6.2 — التزامن القابل للتقريب.
🔝 العودة إلى الدليل الرئيسي.