Combine هو إطار البرمجة التفاعلية الأصلي من Apple، تم تقديمه في iOS 13 وmacOS Catalina وtvOS 13 وwatchOS 6. يوفر واجهة برمجة تطبيقات Swift تعريفية لمعالجة الأحداث غير المتزامنة من خلال نمط Publisher وSubscriber، ليحل محل المندوبين والإغلاقات وNotificationCenter بسلسلة موحدة. وفقًا لـ Apple، 2025، يعتبر Combine أساسًا لـ SwiftUI والهندسات المعمارية الحديثة لنظام iOS، حيث يعمل بشكل وثيق مع async/await وStructured Concurrency. تم تصميم الإطار لتكوين العمليات غير المتزامنة مع ضمانات سلامة الخيوط.
الخلاصة
Combine هو إطار برمجة تفاعلية تعريفية مدمج في SDK من Apple. ينفذ نمط التدفقات التفاعلية (Reactive Streams): Publisher ينتج القيم، وSubscriber يستهلكها، والمشغلات تحول التدفق بينهما. يحل Combine مشكلة الاستدعاءات العكسية والمندوبين من خلال توفير نموذج تكوين موحد لأي أحداث غير متزامنة — من استجابات الشبكة إلى تغييرات واجهة المستخدم.
قبل ظهور Combine، كان مطورو iOS يستخدمون مكتبات طرف ثالث، أبرزها RxSwift. أنشأت Apple Combine كبديل أصلي مع تكامل عميق في النظام البيئي: يدعم الإطار Objective-C عبر جسور @objc، ويعمل مع KVO (مراقبة القيمة الرئيسية عبر NSObject.keyValuePublisher) وNotificationCenter، وهو أيضًا أساس SwiftUI. جميع مكونات UIKit المنشورة في SwiftUI تستخدم Combine داخليًا لتحديثات العرض.
تم تصميم Combine مع مراعاة Swift Concurrency: بدءًا من iOS 15، يمكن تحويل Publisher إلى AsyncSequence عبر .values واستخدامه في حلقات for-await-in. يتم التحويل العكسي من الدوال غير المتزامنة إلى Publisher عبر Future. وفقًا لـ Apple WWDC 2024، يظل Combine هو الإطار الموصى به لمعالجة بيانات التدفق في تطبيقات UIKit، على الرغم من ظهور async/await للاستدعاءات غير المتزامنة المنفردة.
يعتمد Combine على ثلاثة بروتوكولات: Publisher (يصدر قيمًا من نوع Output، يمكن أن يفشل بخطأ من نوع Failure)، Subscriber (يستقبل القيم، يدير Demand — عدد العناصر المطلوبة)، Subscription (يمثل الاتصال بين Publisher وSubscriber مع إمكانية الإلغاء). يتم تهيئة قناة البيانات عند الاشتراك وتنتهي عند الإلغاء أو الإكمال أو الخطأ. Demand هو مفهوم فريد في Combine: يخبر Subscriber الـ Publisher بعدد العناصر التي يمكنه معالجتها، مما يحقق الضغط العكسي (backpressure) على مستوى البروتوكول.
Publisher هو بروتوكول بنوعين مرتبطين: Output (نوع القيم المصدرة) وFailure (نوع الخطأ المطبق لـ Error). إذا كان التدفق لا يمكن أن يفشل، يتم تحديد Failure كـ Never — وهذا يضمن لـ Subscriber أن onReceive سيتم استدعاؤه فقط مع Output. تشمل الـ Publishers المضمنة Just (قيمة واحدة)، Sequence (مصفوفة)، URLSession.DataTaskPublisher (طلب شبكة)، NotificationCenter.Publisher و@property wrapper @Published.
import Combine
// إنشاء Publisher من تسلسل
let publisher = [1, 2, 3, 4, 5].publisher
// إنشاء Subscriber مع معالجة القيم
class PrintSubscriber: Subscriber {
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.unlimited)
}
func receive(_ input: Int) -> Subscribers.Demand {
print("Received: \(input)")
return .unlimited
}
func receive(completion: Subscribers.Completion<Never>) {
print("مكتمل")
}
}
publisher.subscribe(PrintSubscriber())
Subscription هو بروتوكول يمثل الاتصال النشط بين Publisher وSubscriber. يتلقى Subscriber الـ Subscription في طريقة receive(subscription:) ويستدعي request(_:) لتحديد Demand: .unlimited (كل القيم)، .max(N) (عدد محدود) أو .none (إيقاف مؤقت). يمكن أن يتغير Demand ديناميكيًا — يمكن لـ Subscriber زيادة أو تقليل عدد العناصر المطلوبة أثناء استقبال البيانات. يوفر هذا ضغطًا عكسيًا دون تخزين مؤقت على جانب Publisher.
Subject هو نوع يجمع بين Publisher وSubscriber. يمكن استخدام Subject كـ Publisher (يشترك فيه المشتركون) وفي نفس الوقت كـ Subscriber (تُرسل إليه القيم). يوفر Combine نوعين من Subject: PassthroughSubject (لا يخزن الحالة، يمرر القيم الجديدة فقط) وCurrentValueSubject (يخزن القيمة الحالية ويمررها للمشتركين الجدد). Subject ضروري لدمج الكود الأمرّي في السلاسل التفاعلية لـ Combine.
let subject = PassthroughSubject<String, Never>()
// الاشتراك كـ Publisher
let cancellable = subject
.map { $0.uppercased() }
.sink { print($0) }
// إرسال القيم كـ Subscriber
subject.send("hello") // يطبع "HELLO"
subject.send("world") // يطبع "WORLD"
CurrentValueSubject يختلف عن PassthroughSubject بوجود قيمة أولية وخاصية value: المشترك يتلقى فورًا القيمة الحالية عند الاشتراك، ثم جميع التحديثات اللاحقة. CurrentValueSubject.value قابل للقراءة والكتابة — تغيير value يرسل تلقائيًا القيمة الجديدة لجميع المشتركين. هذا يجعل CurrentValueSubject خيارًا مثاليًا لتمثيل الحالة في بنية MVVM: ViewModel ينشر CurrentValueSubject، وView تشترك مع التغييرات عبر sink.
كلا النوعين من Subject يمكنهما إنهاء التدفق باستدعاء send(completion: .finished) أو send(completion: .failure(error)). بعد الإنهاء، يتوقف Subject عن استقبال وإصدار الأحداث. للتدفقات طويلة العمر التي لا يجب أن تنتهي (مثل أحداث واجهة المستخدم)، يُوصى باستخدام PassthroughSubject مع Never Failure لتجنب استدعاء send(completion:) عن طريق الخطأ.
مشغلات Combine هي طرق Publisher تُرجع Publisher جديدًا. كل مشغل ينشئ كائنًا جديدًا يشترك في Publisher العلوي ويصدر قيمًا محولة نحو الأسفل. نظرًا لأن Publisher هو نوع عام، تحافظ المشغلات على كتابة صارمة: map يحول Output<A> إلى Output<B>، tryMap يضيف إمكانية الخطأ. يحتوي Combine على حوالي 100 مشغل مدمج.
| الفئة | المشغل | الغرض |
|---|---|---|
| التحويل | map / tryMap / flatMap | تحويل القيم أو التدفقات |
| التصفية | filter / compactMap / removeDuplicates | اختيار أو تنظيف القيم |
| الدمج | combineLatest / zip / merge | دمج عدة Publishers |
| التحكم بالوقت | debounce / throttle / delay | تأخير وتقليل الأحداث |
| معالجة الأخطاء | catch / retry / replaceError | الاسترداد بعد Failure |
| إدارة Demand | buffer / collect | التجميع أو التخزين المؤقت |
flatMap في Combine لديه اختلاف مهم عن نسخة RxSwift: يقبل إغلاقًا يعيد Publisher بنفس نوع Failure ويفرد Publisher المتداخل في التدفق الرئيسي. flatMap مع maxPublishers: .max(1) يعمل مثل switchMap — يلغي Publisher المتداخل السابق عند وصول قيمة جديدة. هذا أمر بالغ الأهمية لسيناريوهات البحث: عند كتابة حرف جديد، يتم إلغاء طلب HTTP السابق تلقائيًا.
// بحث مع debounce وإلغاء الطلب السابق
searchTextField.textPublisher
.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap(maxPublishers: .max(1)) { query in
apiService.searchPublisher(query)
.catch { _ in Just([]) }
}
.receive(on: DispatchQueue.main)
.sink { results in
self.tableView.reloadData()
}
.store(in: &cancellables)
مشغلات الدمج — combineLatest وzip — تعمل بشكل مشابه لـ RxSwift: combineLatest يصدر مجموعة من أحدث القيم من جميع Publishers عند تغيير أي منها؛ zip يزاوج القيم حسب الفهرس. merge يدمج Publishers من نفس النوع في تدفق واحد، الحفاظ على الترتيب غير مضمون. Combine يحتوي أيضًا على select — مشغل نادر يختار أول Publisher يكتمل من بين عدة، وshare — بث متعدد للتدفق لعدة مشتركين دون إعادة تنفيذ.
Scheduler في Combine هو بروتوكول يحدد سياق التنفيذ للمشغلات. على عكس RxSwift مع أكثر من 5 جدولات مدمجة، يستخدم Combine آليات Apple الموجودة: DispatchQueue وRunLoop وOperationQueue. كل من هذه الأنواع يتوافق مع بروتوكول Scheduler، مما يسمح بتمريرها مباشرة إلى receive(on:) وsubscribe(on:) دون محولات إضافية.
receive(on:) يحول المصب إلى Scheduler المحدد — ما يعادل observeOn في RxSwift. جميع المشغلات بعد receive(on:) يتم تنفيذها على Scheduler المحدد. subscribe(on:) يحول المنبع — يؤثر على تنفيذ Publisher. نمط نموذجي: subscribe(on: DispatchQueue.global()) للعمل في الخلفية وreceive(on: DispatchQueue.main) لتحديثات واجهة المستخدم. في SwiftUI، عند استخدام .onReceive، لا يلزم ربط مدمج بالخيط الرئيسي، ولكن بالنسبة لـ sink يُوصى بـ receive(on:).main صريح.
// تحميل في الخلفية + واجهة مستخدم على الخيط الرئيسي
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap { data, response -> Data in
guard let http = response as? HTTPURLResponse,
http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
.receive(on: DispatchQueue.main)
.decode(type: User.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print($0) },
receiveValue: { self.nameLabel.text = $0.name })
.store(in: &cancellables)
RunLoop.main هو بديل لـ DispatchQueue.main لعمليات واجهة المستخدم. الفرق هو أن RunLoop.main مرتبط بدورة الأحداث الحالية للتطبيق، بينما DispatchQueue.main مرتبط بقائمة الانتظار العالمية للخيط الرئيسي. لـ UIKit يُوصى بـ DispatchQueue.main، ولـ SwiftUI — RunLoop.main. ImmediateWhenScheduler ينفذ العمليات بشكل متزامن على الخيط الحالي — يُستخدم افتراضيًا للاختبارات والـ Publishers البسيطة.
ObservableObject هو بروتوكول SwiftUI للكائنات التي تنشر التغييرات. يمكن للفئة التي تنفذ ObservableObject استخدام property wrapper @Published للخصائص التي تُعلم تغييراتها SwiftUI تلقائيًا بضرورة إعادة الرسم. تحت الغطاء، ينشئ @Published Publisher يخطر Publisher objectWillChange عند تغيير wrappedValue. تشترك SwiftUI في objectWillChange عبر @StateObject أو @ObservedObject أو @EnvironmentObject.
@Published هو الطريقة الأكثر شيوعًا لدمج Combine في SwiftUI. عندما تتغير قيمة خاصية @Published، يقوم SwiftUI بتحديث جميع الـ Views التي تستخدم ذلك الكائن. @StateObject ينشئ مثيل ObservableObject ويشترك في تغييراته. View المنشأة بـ @StateObject تُعاد رسمها تلقائيًا عند تغيير خصائص @Published. إذا كان الكائن بحاجة للمشاركة بين عدة Views، يتم استخدام @ObservedObject أو @EnvironmentObject.
class UserViewModel: ObservableObject {
@Published var name: String = ""
@Published var age: Int = 0
private var cancellables = Set<AnyCancellable>()
init() {
$name
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { [weak self] newName in
AnalyticsService.logNameChange(newName)
}
.store(in: &cancellables)
}
}
struct UserView: View {
@StateObject var viewModel = UserViewModel()
var body: some View {
TextField("Name", text: $viewModel.name)
}
}
AnyCancellable هو غلاف محو نوع لـ Cancellable يخزن رمز إلغاء الاشتراك. Set<AnyCancellable> يدير دورة حياة الاشتراكات: عند إلغاء تهيئة المالك، يتم إلغاء جميع Cancellable تلقائيًا. في مشاريع SwiftUI، يتم تعريف Set<AnyCancellable> في فئة ObservableObject، وتُضاف الاشتراكات عبر .store(in: &cancellables). لـ UIKit، تُستخدم نفس الآليات مع التخزين في UIViewController عبر &cancellables أو استدعاءات manual لـ cancel().
Combine وRxSwift يحلان نفس مهام البرمجة التفاعلية لكن لديهما اختلافات معمارية جوهرية. Combine هو جزء من SDK من Apple مع توافق عكسي حتى iOS 13، RxSwift هي مكتبة طرف ثالث تدعم iOS 8+. Combine يستخدم كتابة أخطاء صارمة عبر Failure العام، RxSwift يستخدم نوع Error واحد. Combine مدمج مع SwiftUI على مستوى المنصة، RxSwift يتطلب RxCocoa لإضافات واجهة المستخدم.
الاختيار بين Combine وRxSwift يعتمد على متطلبات المشروع. إذا كان الحد الأدنى لإصدار iOS هو >= 13 والمشروع يستخدم SwiftUI — فإن Combine هو الخيار الطبيعي بفضل التكامل المدمج وعدم وجود تبعيات إضافية. إذا كان المشروع يدعم iOS 11-12، أو يحتوي على قاعدة كود RxSwift موجودة، أو يتطلب مشغلات محددة متاحة فقط في RxSwift (مثل Observable.from(path:))، — فإن RxSwift يبقى حلاً مناسبًا.
| الخاصية | Combine | RxSwift |
|---|---|---|
| المطور | Apple (مدمج في SDK) | ReactiveX (مجتمع) |
| إصدار iOS | iOS 13+ | iOS 8+ |
| نوع الخطأ | Failure عام (Never لواجهة المستخدم) | Error (أي) |
| تكامل UI | @Published + SwiftUI | RxCocoa + UIKit |
| المشغلات | ~100 مدمجة | 400+ مشغل |
| Swift Concurrency | عبر .values (تسلسل غير متزامن) | عبر مكتبة جسر |
الأسئلة الشائعة
PassthroughSubject لا يخزن الحالة — المشترك يتلقى فقط الأحداث المرسلة بعد الاشتراك. CurrentValueSubject يخزن القيمة الحالية ويمررها لكل مشترك جديد فور الاشتراك. CurrentValueSubject مناسب لتمثيل الحالة (مثل isLoggedIn).
الاشتراك يُرجع AnyCancellable، الذي يُلغى عند استدعاء cancel() أو عند إلغاء التهيئة. للإدارة الجماعية، استخدم Set<AnyCancellable> — يتم إلغاء جميع الاشتراكات عند مسح المجموعة. هذا مشابه لـ DisposeBag في RxSwift.
نعم، Combine يظل مهمًا للبيانات المتدفقة: أحداث واجهة المستخدم، debounce، combineLatest، WebSocket. async/await مناسب للاستدعاءات المنفردة، Combine — للتدفقات المستمرة أو المتعددة. كلا الإطارين متكاملان — يمكن تحويل Publisher إلى AsyncSequence.
UIKit لا يحتوي على Publishers مدمجة، لكن Apple توفر إضافات: NotificationCenter.default.publisher(for:)، Timer.publish، URLSession.dataTaskPublisher. لأحداث واجهة المستخدم المخصصة، يُستخدم PassthroughSubject أو @IBAction مغلفة في Publisher عبر Future أو Subject.
الضغط العكسي (backpressure) هو آلية للتحكم في سرعة التدفق: يخبر Subscriber الـ Publisher عبر Demand بعدد العناصر التي يمكنه معالجتها. إذا كان Demand = .max(1)، ينتظر Publisher الطلب قبل إرسال القيمة التالية. هذا يمنع تجاوز سعة المخزن المؤقت عند عدم تطابق سرعات المنتج والمستهلك.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا