فریمورک Combine در سوئیفت — راهنمای شروع به کار

۱۵۲ بازدید
آخرین به‌روزرسانی: ۱۲ مهر ۱۴۰۲
زمان مطالعه: ۷ دقیقه
فریمورک Combine در سوئیفت — راهنمای شروع به کار

Combine فریمورک جدیدی است که از سوی اپل در کنفرانس WWDC سال 2019 معرفی شده است. این فریمورک یک API دستوری برای Swift ارائه می‌کند که امکان پردازش مقادیر در طی زمان را فراهم می‌سازد. به بیان دیگر چنان که خود اپل توضیح می‌دهد:

وظیفه Combine سفارشی‌سازی مدیریت رویدادهای ناهمگام از طریق ترکیب کردن عملگرهای پردازش رویداد است.

این وضعیت در ابتدا ممکن است گیج‌کننده باشد. معنای آن چندان روشن نیست. ممکن است از خود بپرسید با این فریمورک چه کار می‌توان کرد و چرا باید از آن استفاده کرد؟ با مطالعه این مقاله خواهید توانست به این پرسش‌ها پاسخ دهید.

Combine چیست؟

فریمورک Combine را می‌توان با فریمورک‌هایی مانند RxSwift و ReactiveSwift که به صورت رسمی ReactiveCocoa نامیده می‌شود مقایسه کرد.

این فریمورک امکان نوشتن کد تابعی-واکنشی را به وسیله API دستوری سوئیفت فراهم می‌سازد.

زبان‌های برنامه‌نویسی تابعی-واکنشی (ERP) امکان پردازش مقادیر را در طی زمان فراهم می‌سازند. نمونه‌هایی از این مقادیر شامل پاسخ‌های شبکه، رویدادهای اینترفیس کاربر و دیگر انواع داده‌های ناهمگام هستند.

مفاهیم اساسی Combine

در این بخش با مفاهیم اساسی Combine آشنا می‌شوید تا بتوانید طرز کار آن را درک کرده و شیوه استفاده از آن را بیاموزید. پیش از آن که وارد بررسی نمونه کدها بشویم، بهتر است ابتدا مقداری اطلاعات مقدماتی در اختیار داشته باشیم. بدین ترتیب می‌توانیم طرز کار کد و رفتار آن را بهتر درک کنیم.

ناشران و مشترکان

فریمورک Combine از مفهوم مشهور «ناشر و مشترک» پیروی می‌کند. اگر با RxSwift آشنا باشید، باید بدانید که:

  • ناشران همان observable-ها هستند.
  • مشترکان همان observer-ها هستند.

با این که نام‌ها فرق می‌کند اما هر دو معنای یکسانی دارند.

هر ناشر مقادیری را که می‌توانند تغییر پیدا کنند عرضه می‌کند و مشترک یا مشترکان همه این به‌روزرسانی‌ها را دریافت می‌کنند. این نکته را به خاطر داشته باشید که در ادامه زمانی که مشغول کار با Combine شویم، برخی مثال‌هایی از فریمورک Foundation را نیز مطرح خواهیم کرد.

فریمورک Foundation و Combine

فریمورک Foundation شامل اکستنشن‌های زیادی برای کار با Combine است. این فریمورک امکان کار با انواع متداولی که با آن‌ها آشنا هستید را فراهم می‌سازد.

مثال‌ها شامل موارد زیر هستند:

  • یک ناشر URLSessionTask که پاسخ‌های داده‌ی خطای درخواست را منتشر می‌کند.
  • عملگرها برای دیکود کردن آسان JSON.
  • ناشری برای یک Notification.Name خاص که نوتیفیکیشن منتشر می‌کند.

با بررسی مثالی از مورد آخر می‌توانیم مفهوم ناشر و مشترک را بهتر درک کنیم.

در نمونه کد زیر یک Publisher برای نوتیفیکیشن پست بلاگ جدید خود می‌سازیم:

1import Combine
2
3extension Notification.Name {
4    static let newBlogPost = Notification.Name("new_blog_post")
5}
6
7struct BlogPost {
8    let title: String
9    let url: URL
10}
11
12let blogPostPublisher = NotificationCenter.Publisher(center: .default, name: .newBlogPost, object: nil)

این ناشر به نوتیفیکیشن‌های ورودی برای نام نوتیفیکیشن newBlogPost گوش می‌دهد، اما این وضعیت تنها زمانی صورت می‌دهد که یک مشترک وجود داشته باشد.

ما برای مثال می‌توانیم یک lastPostTitleLabel ایجاد کنیم که مشترکان را به ناشران نسبت دهد:

1let lastPostLabel = UILabel()
2let lastPostLabelSubscriber = Subscribers.Assign(object: lastPostLabel, keyPath: \.text)
3blogPostPublisher.subscribe(lastPostLabelSubscriber)

اگر کد فوق را امتحان کنید، متوجه خواهید شد که کار نمی‌کند و خطای زیر را تولید می‌کند:

Instance method ‘subscribe’ requires the types ‘NotificationCenter.Publisher.Output’ (aka ‘Notification’) and ‘String?’ be equivalent

مشخصه متنی برچسب باید یک مقدار ?String دریافت کند در حالی که استریم یک Notification ارائه کرده است.

بنابراین باید از یک عملگر استفاده کنیم که از قبل با آن آشنا هستید و map نام دارد. با استفاده از این عملگر می‌توانیم مقدار خروجی را از Notification به نوع مورد نظر یعنی ?String تغییر دهیم:

1let blogPostPublisher = NotificationCenter.Publisher(center: .default, name: .newBlogPost, object: nil)
2    .map { (notification) -> String? in
3        return (notification.object as? BlogPost)?.title ?? ""
4    }

بدین ترتیب نمونه کد کامل زیر ایجاد می‌شود:

1import Combine
2
3extension Notification.Name {
4    static let newBlogPost = Notification.Name("new_blog_post")
5}
6
7struct BlogPost {
8    let title: String
9    let url: URL
10}
11
12let blogPostPublisher = NotificationCenter.Publisher(center: .default, name: .newBlogPost, object: nil)
13    .map { (notification) -> String? in
14        return (notification.object as? BlogPost)?.title ?? ""
15    }
16
17let lastPostLabel = UILabel()
18let lastPostLabelSubscriber = Subscribers.Assign(object: lastPostLabel, keyPath: \.text)
19blogPostPublisher.subscribe(lastPostLabelSubscriber)
20
21let blogPost = BlogPost(title: "Getting started with the Combine framework in Swift", url: URL(string: "https://www.avanderlee.com/swift/combine/")!)
22NotificationCenter.default.post(name: .newBlogPost, object: blogPost)
23print("Last post is: \(lastPostLabel.text!)")
24// Last post is: Getting started with the Combine framework in Swift

هر زمان که یک پست جدید منتشر می‌شود (Published)، مشترک (Subscriber) مقدار متنی خود را به‌روزرسانی می‌کند.

قواعد اشتراک

اکنون که مثال ساده‌ای از یک ناشر و یک مشترک را در فریمورک Combine دیدیم، نوبت آن رسیده است که به بررسی قواعدی که از یک فرایند اشتراک ناشی می‌شود بپردازیم:

  • تنها یک مشترک می‌تواند وجود داشته باشد.
  • می‌تواند هیچ مقدار یا چند مقدار انتشار پیدا کند.
  • حداکثر یک فرایند تکمیل فراخوانی خواهد شد.

همان طور که متوجه شدید، اشتراک‌ها می‌توانند همراه با رویداد تکمیل یافتن باشند، اما لزومی به این مسئله وجود ندارد. مثال Notification ما یک ناشر است که هرگز کامل نخواهد شد.

نمونه‌ای از ناشران تکمیل‌کننده، ناشر URLSessionTask است که یا با پاسخ داده‌ای یا با خطای درخواستی تکمیل می‌شود.

واقعیت این است که هر زمان خطایی روی یک استریم ایجاد شود، فرایند اشتراک لغو می‌شود. این وضعیت حتی در صورتی که امکان ارسال چند مقدار وجود داشته باشد برقرار است.

استفاده از Published@ برای اتصال مقادیر به تغییرات

اکنون که با مبانی فریمورک آشنا شدیم، می‌توانیم به بررسی کلیدواژه Published@ بپردازیم.

این کلیدواژه یک پوشش مشخصه است و یک ناشر به هر مشخصه اضافه می‌کند. مثال ساده‌ای از آن می‌تواند یک مقدار بولی باشد که به حالت فعال‌شده یک UIButton انتساب می‌دهیم:

1final class FormViewController: UIViewController {
2    
3    @Published var isSubmitAllowed: Bool = false
4
5    @IBOutlet private weak var acceptTermsSwitch: UISwitch!
6    @IBOutlet private weak var submitButton: UIButton!
7
8    override func viewDidLoad() {
9        super.viewDidLoad()
10        $isSubmitAllowed.receive(on: DispatchQueue.main).assign(to: \.isEnabled, on: submitButton)
11    }
12
13    @IBAction func didSwitch(_ sender: UISwitch) {
14        isSubmitAllowed = sender.isOn
15    }
16}

در ادامه کد فوق را گام به گام توضیح می‌دهیم:

  • UISwitch به تحریک متد didSwitch می‌پردازد و مقدار isSubmitAllowed را به مقادیر درست یا نادرست تغییر می‌دهد.
  • مقدار submitButton.isEnabled به مشخصه isSubmitAllowed وصل شده است.
  • هر تغییری در isSubmitAllowed به این مشخصه isEnabled روی صف اصلی انتساب می‌یابد گویی در حال کار روی رابط کاربری هستیم.

نخستین چیزی که باید توجه کنید، آن علامت دلار در ابتدای isSubmitAllowed است. این علامت امکان دسترسی به مقدار Publisher داخل پوشش را می‌دهد. از این جا می‌توان به همه عملگرها دسترسی یافت و یا چنان که در این مثال انجام دادیم در آن اشتراک پیدا کرد.

مدیریت حافظه در Combine

در RxSwift مفهومی به نام DisposeBag وجود دارد و فریمورک Combine نیز مجهز به AnyCancellable است.

این کلاس ()cancel را روی deinit فراخوانی می‌کند تا مطمئن شود که اشتراک‌ها زودتر خاتمه می‌یابند. بدون پیاده‌سازی این کلاس احتمالاً در چرخه‌های دائمی گیر می‌کنید.

اگر بخواهیم به مثال قبل بازگردیم، می‌توانیم به صورت زیر اضافه کنیم تا مطمئن شویم که اشتراک دکمه تحویل ما به طرز صحیحی آزاد شده است:

1final class FormViewController: UIViewController {
2
3    @Published var isSubmitAllowed: Bool = false
4    private var switchSubscriber: AnyCancellable?
5
6    @IBOutlet private weak var acceptTermsSwitch: UISwitch!
7    @IBOutlet private weak var submitButton: UIButton!
8
9    override func viewDidLoad() {
10        super.viewDidLoad()
11        switchSubscriber = $isSubmitAllowed.receive(on: DispatchQueue.main).assign(to: \.isEnabled, on: submitButton)
12    }
13
14    @IBAction func didSwitch(_ sender: UISwitch) {
15        isSubmitAllowed = sender.isOn
16    }
17}

چرخه عمر switchSubscriber به چرخه عمر FormViewController پیوند یافته است. هر زمان که کنترلر ویو آزاد شود، مشخصه نیز آزاد می‌شود و متد ()cancel فراخوانی خواهد شد.

انواع خطا و استریم‌ها

به محض این که کار با Combine را آغاز کنید، با خطاهایی در مورد عدم تطبیق انواع خطا مواجه خواهید شد.

هر ناشر شیوه شکست خود و نوع خطایی که می‌توانید دریافت کنید را توصیف می‌کند. همان‌طور که از عملگر map در مثال نوتیفیکیشن خود استفاده کردیم، می‌توان از عملگرهای مختلف برای بازیابی یا واکنش به خطاها استفاده کرد.

عملگرهای رایجی که می‌توان استفاده کرد به شرح زیر هستند:

  • ()assertNoFailure که نوع خطا را به Never تغییر می‌دهد و زمانی که خطایی رخ می‌دهد یک assert فراخوانی می‌کند.
  • ()mapError که امکان تغییر نوع خطا را به دست می‌دهد.
  • عملگرهای دیگر مانند retry ،catch ،abortOnError و replaceError.

دیباگ کردن استریم‌های Combine

دیباگ کردن زبان‌های تابعی-واکنشی می‌تواند دشوار باشد. در اغلب موارد با توضیح‌های طولانی از خطا و رد پشته‌های ناخوانا در Xcode مواجه می‌شویم.

Combine

در موارد زیادی همین مسئله موجب می‌شود که توسعه‌دهندگان از فریمورک‌هایی مانند RxSwift و ReactiveSwift استفاده نکنند. وقتی به Combine نگاه می‌کنیم، به نظر می‌رسد که اوضاع مشابهی وجود دارد.

خوشبختانه روش‌هایی برای دیباگ کردن در Combine با استفاده از عملگرهای زیر وجود دارد:

  • ()print برای پرینت کردن پیام‌های لاگ برای همه رویدادهای انتشار استفاده می‌شود.
  • ()breakpoint که وقتی یک کلوژر ارائه شده نیاز به توقف پردازش در دیباگر دارد، سیگنال debugger را ارسال می‌کند.
  • ()breakpointOnError که تنها در زمان دریافت یک سیگنال شکست ()breakpointOnError را ارسال می‌کند.

لیستی از همه عملگرهای ناشر

متأسفانه نوشتن همه عملگرهای ناشر و به‌روز نگه‌داشتن آن در این مقاله کار دشواری است. بنابراین برای دریافت لیست به‌روز می‌توانید به صفحه مستندات (+) مراجعه کنید.

برخی از این موارد را در تصویر زیر می‌توانید مشاهده کنید:

Combine

استفاده از Combine به همراه MVVM

فریمورک Combine برای کار ترکیبی با MVVM بسیار عالی است. در واقع این پارادایم روی فریمورک بهتر کار می‌کند.

در این نوشته قصد نداریم به بررسی گسترده این موضوع بپردازیم، اما یکی از مثال‌های قبلی را که به یک مثال MVVM تبدیل شده است، می‌توانید در ادامه مشاهده کنید:

1struct FormViewModel {
2    @Published var isSubmitAllowed: Bool = false
3}
4
5final class FormViewController: UIViewController {
6
7    private var switchSubscriber: AnyCancellable?
8    private var viewModel = FormViewModel()
9
10    @IBOutlet private weak var acceptTermsSwitch: UISwitch!
11    @IBOutlet private weak var submitButton: UIButton!
12
13    override func viewDidLoad() {
14        super.viewDidLoad()
15        switchSubscriber = viewModel.$isSubmitAllowed.receive(on: DispatchQueue.main).assign(to: \.isEnabled, on: submitButton)
16    }
17
18    @IBAction func didSwitch(_ sender: UISwitch) {
19        viewModel.isSubmitAllowed = sender.isOn
20    }
21}

چرا باید از Combine استفاده کنیم؟

اکنون که با مبانی اولیه Combine آشنا شدیم، سؤال مهمی که پیش می‌آید این است که چرا باید از آن استفاده کنیم؟ مستندات اپل در این خصوص چنین توضیح می‌دهند:

با به بارگیری Combine می‌توانید کد خود را راحت‌تر خوانده و نگهداری کنید، کد پردازش رویدادتان متمرکز می‌شود و تکنیک‌های مشکل‌زایی مانند کلوژرهای تو در تو و Callback-های سنتی حذف می‌شوند.

با این که نقل قول فوق کاملاً صحیح است، اما چنان که دیدیم دیباگ کردن در فریمورک Combine می‌تواند کاملاً دشوار باشد.

عیب دیگر این است که منحنی یادگیری این فریمورک شیب تندی دارد. نه تنها شما، بلکه همه همکارانتان باید آن را بیاموزید و با طرز کار آن آشنا شوید.

هیچ بعید نیست که در صورت عدم آشنایی با Combine در نهایت یک پروژه پر از استریم‌ها و مشترکان به دست آورید که دارای کدبیس دشواری است.

از این رو پیش از شروع به کار با Combine باید مطمئن شوید که آن را روی پروژه‌های نسبتاً کوچک آزموده‌اید.

این فریمورک را با همکاران خود به بررسی بگذارید و از خود بپرسید آیا برای کدتان به Combine نیاز دارید یا نه. همچنین باید مطمئن شوید که زیاده‌روی نمی‌کنید و در نهایت با پروژه‌ای پر از اتصال‌ها و کدی با دیباگ دشوار مواجه نمی‌شوید، زیرا این وضعیت در نهایت موجب کندی فرایند کارتان می‌شود.

پیشنهاد کلی ما این است که ابتدا راه‌حل‌های دیگر را بررسی کنید و در صورتی که با تغییر حالت‌های (State) زیاد مواجه هستید و نیازمند کد ناهمگام زیادی بودید، Combine را امتحان کنید.

با مطالعه این مقاله آماده شروع به کار با Combine می‌شوید. در این مطلب مفاهیم اساسی این فریمورک بررسی شدند و موارد زیاد دیگری نیز به بحث گذارده شد.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۳ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming
۱ دیدگاه برای «فریمورک Combine در سوئیفت — راهنمای شروع به کار»

سلام و عرض تشکر بابت توضیحات خوبتون راجع به combine
من باید یه پروژه تحقیقاتی راجع به combine در برنامه نویسی (مدیریت نرم افزار) برای ارائه به دانشگاه تهیه کنم اما متاسفانه منابع بسیار محدود و کمی راجع به این مبحث پیدا کردم و به مشکل خوردم.
سایت شما تنها سایت فارسی ای بود که راجع به combine توضیحاتی داده و بین سایت های انگلیسی نیز مرجع مناسب و مفصلی برای تحقیق پیدا نکردم.
یرای ارائه یه تحقیق 40 صفحه ای راجع به combine منابع بسیار کم ومحدودی وجود داره.
ممنون میشم اگه لطف کنید منابعی برای تحقیق راجع به این موضوع بهم معرفی کنید و یا خودتون توضیحات مفصل تری ارائه بدبد.

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *