دمج MVVMs في تطبيقات UIKit و SwiftUI لمطوري UIKit



نحن نعلم أن ObservableObjecالطبقات t مع @Publishedخصائصها تم إنشاؤها Combineخصيصًا View Modelفي SwiftUI. ولكن View Modelيمكن استخدام نفس الشيء تمامًا في UIKitتنفيذ الهندسة المعمارية  MVVM، على الرغم من أنه في هذه الحالة سيتعين علينا "ربط" ( bind) UIالعناصر يدويًا @Published بخصائص View Model. سوف تفاجأ ، ولكن بمساعدة Combineهذا يتم تنفيذ بضعة أسطر من التعليمات البرمجية. بالإضافة إلى ذلك ، بالالتزام بهذه الأيديولوجية عند تصميم UIKitالتطبيقات ، ستنتقل لاحقًا بدون ألم SwiftUI.

والغرض من هذه المقالة هو لتظهر مع مثال بسيط بدائي كيف يمكن تنفيذ بأناقة و MVVMالعمارة UIKitمع  Combine. على النقيض من ذلك ، نظهر استخدام نفسهView Model ج SwiftUI.

ستناقش المقالة تطبيقين بسيطين يسمحان لك بتحديد أحدث معلومات الطقس لمدينة معينة من موقع OpenWeatherMa p. ولكن UIسيتم إنشاء أحدهما بالتطبيق SwiftUI، والآخر بالمساعدة UIKit. بالنسبة للمستخدم ، ستبدو هذه التطبيقات متشابهة تقريبًا.



الرمز موجود على جيثب .

UIستحتوي واجهة المستخدم ( ) على UI عنصرين فقط : حقل نص لدخول المدينة وتسمية لعرض درجة الحرارة. مربع النص لدخول المدينة هو INPUT ( Input) النشط ، وعلامة درجة الحرارة هي EXIT ( Output) السلبي .  

الدور View Model في الهندسة MVVMهو أنه يأخذ INPUT (s) من View(أو ViewControllerإلى UIKit) ، وينفذ منطق الأعمال للتطبيق ويمرر OUTPUTS مرة أخرى إلى  View(أو ViewControllerإلى UIKit) ، وربما تقديم هذه البيانات بالتنسيق المطلوب.

خلق  View Modelمع Combineبغض النظر عن نوع من منطق الأعمال - متزامن أو غير متزامن - هو بسيط جدا إذا كنت تستخدم ObservableObject فئة مع لها @Publishedخصائص.

API OpenWeatherMap


على الرغم من أن خدمة  OpenWeatherMap   تسمح لك بتحديد معلومات الطقس واسعة النطاق للغاية ، إلا أن نموذج البيانات التي نهتم بها سيكون بسيطًا للغاية ، إلا أنها توفر معلومات تفصيلية  WeatherDetailحول الطقس الحالي في المدينة المحددة وتقع في ملف  Model.swift : على



الرغم من أننا في هذه المهمة المحددة ، سنهتم فقط بدرجة الحرارة temp، والتي في الهيكل  Main، يوفر النموذج معلومات تفصيلية كاملة عن الطقس الحالي كهيكل جذري  WeatherDetail، معتقدًا أنه في المستقبل ستحتاج إلى توسيع إمكانيات هذا التطبيق. البنية WeatherDetail قابلة للتشفير ، وهذا سيسمح لنا بفك تشفير JSONالبيانات حرفياً في النموذج مع سطرين فقط من التعليمات البرمجية  .  يجب أن يكون

الهيكل  WeatherDetailأيضًاIdentifiableإذا كنا نريد أن تجعل من السهل على أنفسنا في المستقبل لعرض مجموعة من التنبؤات الجوية  [WeatherDetail] لعدة أيام مقدما في شكل قائمة  List من SwiftUI. يعد هذا أيضًا فارغًا لتطبيق الطقس الحالي الأكثر تعقيدًا في المستقبل. Identifiableيتطلب البروتوكول وجود الممتلكات id,التي لدينا بالفعل ، لذلك لن تكون هناك حاجة إلى جهود إضافية منا.

عادة ، تقدم الخدمات ، بما في ذلك خدمة  OpenWeatherMap ، جميع أنواع الخدمات URLs للحصول على الموارد التي نحتاجها. تتيح لنا خدمة  OpenWeatherMapURLs إحضار معلومات تفصيلية حول الطقس الحالي أو التوقعات لمدة 5 أيام في مدينة معينة city. في هذا التطبيق ، سنكون مهتمين فقط بمعلومات الطقس الحالية ولهذه الحالةURLمحسوبة باستخدام الوظيفة absoluteURL (city: String):



API لخدمة OpenWeatherMap ، سنضعها في  ملف WeatherAPI.swift . سيكون الجزء المركزي منه طريقة لاختيار معلومات الطقس التفصيلية  WeatherDetailفي المدينة  city:

  • fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>

في سياق الإطار ، Combine لا تُرجع هذه الطريقة معلومات الطقس التفصيلية WeatherDetailفحسب ، بل تُرجع أيضًا  "الناشر" المقابل Publisher. AnyPublisher<WeatherDetail, Never>لا يقوم "ناشرنا" بإرجاع أي خطأ - Neverوفي حالة استمرار حدوث خطأ في أخذ العينات أو الترميز ، فإن النائب يعود  WeatherDetail.placeholderبدون أي رسائل إضافية حول سبب الخطأ. 

ضع في اعتبارك بمزيد من التفصيل الطريقة  fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>التي تحدد  معلومات الطقس التفصيلية للمدينة من موقع OpenWeatherMap city ولا تعرض أي خطأ Never:



  1. بناء على اسم المدينة، ونحن  city النموذج URL باستخدام وظيفة absoluteURL(city:city)لطلب معلومات مفصلة الطقس  WeatherDetail،
  2. «» dataTaskPublisher(for:), Output (data: Data, response: URLResponse),   Failure - URLError,
  3. map { } (data: Data, response: URLResponse)  data
  4. JSON  data ,  WeatherDetail, ,
  5. - «»  catch (error ... )  «» WeatherDetail.placeholder,
  6. main , UI,
  7. «» «» eraseToAnyPublisher() AnyPublisher.

وهكذا حصل "الناشر" غير المتزامن على AnyPublisher"لا تقلع" من تلقاء نفسه ؛ ولا يسلم أي شيء حتى "يشترك" فيه شخص ما. سنستخدمها في  ObservableObject فصل يلعب دورًا View Modelفي  SwiftUIكلٍ من و UIKit

إنشاء نموذج عرض


ل View Modelإنشاء فئة بسيطة جدا  TempViewModelأن تنفذ بروتوكول ObservableObject مع اثنين من  @Published الخصائص:  



  1. الأولى  @Published var city: Stringهي المدينة (يمكنك تسميتها بشكل مشروط مدخل ، حيث يتم تنظيم قيمتها من قبل المستخدم في View) ،  
  2. والثاني  @Published var currentWeather = WeatherDetail.placeholder هو الطقس في هذه المدينة في الوقت الحالي (يمكننا استدعاء هذه الخاصية EXIT بشكل مشروط ، حيث يتم الحصول عليها عن طريق جلب البيانات من موقع  OpenWeatherMap ).

بمجرد قيامنا بتعيين  @Published خاصية  city، يمكننا البدء في استخدامها كخاصية بسيطة  cityو "ناشر"  $city.

في الفصل الدراسي  TempViewModel، لا يمكنك فقط الإعلان عن الخصائص التي تهمنا ، ولكن أيضًا وصف منطق الأعمال لتفاعلها. تحقيقا لهذه الغاية ، عند تهيئة مثيل الفصل الدراسي  TempViewModel ، init?يمكننا إنشاء "اشتراك" يعمل طوال "دورة حياة" الفصل بأكمله  TempViewModelوإعادة إنتاج اعتماد الطقس الحالي  currentWeather على المدينة  city.

للقيام بذلك ، Combineنقوم بتوسيع السلسلة من إدخال "الناشر" $city إلى إخراج "الناشر" AnyPublisher<WeatherDetail, Never>، الذي تكون قيمته الطقس الحالي. بعد ذلك ، "نشترك" فيه بمساعدة "مشترك" assign (to: \.currentWeather, on: self) والحصول على القيمة المطلوبة للطقس الحالي  currentWeather باعتبارها خاصية "الإخراج"  @Published .

يجب علينا سحب السلسلة ليس فقط من العقارات city، وبالتحديد من "الناشرين" $cityالذين سيشاركون في الإنشاء UI وهناك سنقوم بتغييرها.

كيف سنفعل ذلك؟

لدينا بالفعل وظيفة في ترسانتنا fetchWeather (for city: String)موجودة في الفصل  WeatherAPIوتعيد "الناشر" AnyPublisher<WeatherDetail, Never> بمعلومات الطقس التفصيلية اعتمادًا على المدينة  city، ولا يمكننا استخدام قيمة "الناشر" بطريقة أو بأخرى إلا  $cityلتحويلها إلى حجة لهذه الوظيفة.

 انتقل إلى الناشر المناسب  fetchWeather (for city: String) ل  Combineمساعدتنا المشغل  flatMap:



مشغلflatMapإنشاء "ناشر" جديد بناءً على البيانات التي تم تلقيها من "الناشر" السابق.

بعد ذلك ، "نشترك" في هذا "الناشر" المستلم حديثًا بمساعدة "مشترك" بسيط للغاية  ونعين assign (to: \.currentWeather, on: self)القيمة المستلمة من "الناشر" إلى @Publishedالموقع  currentWeather:



لقد أنشأنا للتو init( )"ناشر" ASYNCHRONOUS و "مشترك" فيه ، مما نتج عنه AnyCancellable"اشتراك" ".

AnyCancellable يسمح "الاشتراك" للمتصل بإلغاء "الاشتراك" في أي وقت وعدم تلقي القيم من "الناشر" ، ولكن علاوة على ذلك ، بمجرد  AnyCancellableمغادرة "الاشتراك" لنطاقه ، يتم تحرير الذاكرة التي يشغلها "الناشر". لذلك ، بمجرد init( ) اكتماله ، سيتم حذف هذا "الاشتراك" من قبل النظام ARC، وليس لديك الوقت لتعيين المعلومات غير المتزامنة حول الطقس الحالي المستلم مع تأخير زمني  currentWeather. لحفظ هذا "الاشتراك" ، من الضروري إنشاء init()متغير خارجي var cancellableSetيحافظ على AnyCancellable" اشتراكنا " في هذا المتغير طوال "دورة حياة" مثيل الفصل بأكمله  TempViewMode

و AnyCancellable"الاشتراك" في متغير يتم تخزينها cancellableSetباستخدام مشغل  store ( in: &self.cancellableSet):



ونتيجة لذلك، فإن "الاشتراك" سيتم الحفاظ عليها في جميع أنحاء كامل "دورة الحياة" مثيل فئة  TempViewModel. يمكننا تغيير قيمة الناشر حسب الرغبة $city، وسيكون الطقس الحالي currentWeather لهذه المدينة تحت تصرفنا دائمًا .

لتقليل عدد مكالمات الخادم عند كتابة المدينة city، يجب ألا نستخدم مباشرة "ناشر" الخط باسم المدينة  $city، ولكن نسخته المعدلة مع عوامل التشغيل debounceو removeDuplicates:



يتم debounce استخدام عامل التشغيل  للانتظار حتى ينتهي المستخدم من كتابة المعلومات الضرورية على لوحة المفاتيح ، وعندها فقط يقوم بالمهمة كثيفة الموارد مرة واحدة.

وبالمثل ، removeDuplicatesلن ينشر العامل القيم إلا إذا كانت تختلف عن أي قيم سابقة. على سبيل المثال، إذا تدخل المستخدم أولا john، ثم joe، وبعد ذلك مرة أخرى john، ونحن سوف تلقي johnمرة واحدة فقط. هذا يساعد على جعل بلدنا UIأكثر كفاءة.

إنشاء واجهة مستخدم باستخدام SwiftUI


الآن بعد أن حصلنا عليها View Model، دعنا نبدأ UI. في البداية SwiftUI، ثم في UIKit.

في أننا Xcodeإنشاء مشروع جديد مع SwiftUIوفي هيكل الناتج أننا ContentView  وضع لنا  View Modelبمثابة @ObservedObject متغير model. استبدل  Text ("Hello, World!") العنوان  Text ("WeatherApp")، أضف مربع نص لدخول المدينة  TextField ("City", text: self.$model.city) وتسمية لعرض درجة الحرارة:



استخدمنا مباشرة قيم متغيرنا model: TempViewModel(). استخدمنا في مربع النص لدخول المدينة $model.city، وفي الملصق لعرض درجة الحرارة - model.currentWeather.main?.temp.

الآن، أية تغييرات على  @Published وخصائص تؤدي إلى "إعادة رسم" View:



ويكفل هذا من حقيقة أن بلدنا View Model هو@ObservedObject، وهذا هو، AUTOMATIC "ملزمة" ( binding) @Publishedلدينا خصائص View Modelوعناصر واجهة المستخدم ( UI) ونفذت . مثل هذا "التجليد" التلقائي ممكن فقط في SwiftUI.

إنشاء واجهة مستخدم باستخدام UIKit


ماذا تفعل بهذا UIKit؟ إنه ليس هناك  @ObservedObject. في  UIKit سنقوم بتنفيذ "الربط" ( binding) يدويًا. هناك العديد من الطرق للقيام بهذا "الربط اليدوي":

  • Key-Value Observing أو KVO: آلية للاستخدام  key pathsلمراقبة الممتلكات وتلقي إشعار بتغييرها.
  • البرمجة التفاعلية التفاعلية أو FRP: استخدام إطار Combine.
  • Delegation: استخدام طرق التفويض لإرسال إعلام بتغيير قيمة الخاصية.
  • Boxing: didSet { } , .

نظرًا لعنوان المقالة ، سنعمل بشكل طبيعي في هذا المجال Combine. في UIKitالتطبيق ، سوف نوضح مدى سهولة جعل "الربط اليدوي" باستخدام Combine.

في UIKitالتطبيق ، سيكون لدينا أيضًا UI عنصران: UITextFieldلدخول المدينة UILabelولعرض درجة الحرارة. في ViewControllerطبيعتنا سيكون لدينا Outletهذه العناصر:





في شكل متغير عادي viewModel، لدينا نفس العنصر كما View Modelفي القسم السابق:



قبل القيام بـ "الربط اليدوي" Combine، دعنا نجعل حقل النص UITextFieldحليفنا و "ناشر" المحتوى الخاص بنا text:



سيتيح لنا ذلك viewDidLoadتنفيذ "الربط اليدوي" بسهولة بالغة باستخدام الوظيفةbinding ():



والواقع أننا "اشتراك" إلى "الناشر" cityTextField.textPublisherباستخدام "المشترك" بسيط جدا  assign (to: \.city, on: viewModel)وتعيين النص المكتوب من قبل المستخدم في مربع النص cityTextFieldلدينا "المدخلات"  @Publishedممتلكات cityبلدنا View Model.

بالإضافة إلى ذلك ، نقوم بإجراء تغييرات في اتجاه آخر: "نشترك" في @Publishedخاصية  "الإخراج"  $currentWeather بمساعدة "المشترك" sink وإغلاقه receiveValue، ونشكل قيمة درجة الحرارة ونعينها للملصق temperatureLabel.

تلقى في  viewDidLoad "الاشتراك" يتم تخزينها في متغير var cancellableSet. بعد إنشائها مرة واحدة ، نسمح لهم بالتصرف طوال "دورة الحياة" بأكملها لطبقة الفصل ViewControllerجنبًا إلى جنب مع "الاشتراك" في موقعنا View Modelتنفيذ كل منطق الأعمال للتطبيق.

بالمناسبة ، ObservableObjectلا يعمل البروتوكول مع UIKit، لكنه لا يتدخل. UIKit لا مبالٍ تمامًا بالبروتوكول ObservableObject، ومن حيث المبدأ ، يمكن إزالته  View Modelفي UIKit التطبيقات:



لكننا لن نقوم بذلك ، لأننا نريد أن نبقيه دون تغيير View Modelلكل من التطبيق الحالي UIKitوربما التطبيقات المستقبلية SwiftUI.

هذا كل شئ. الرمز موجود على جيثب .

استنتاج

إطار رد الفعل وظيفي Combineيسمح لك لتنفيذ ببساطة شديدة ودقة في MVVMالهندسة المعمارية على حد سواء SwiftUIفي UIKitوفي شكل كود مفهومة وقابلة للقراءة.

الروابط:

دمج + UIKit + MVVM
باستخدام برنامج Combine
iOS MVVM التعليمي: إعادة البيع من MVC
MVVM مع الدمج التعليمي لنظام iOS

PS إذا كنت تريد رؤية بعض معلومات الطقس ، فأنت بحاجة إلى التسجيل في OpenWeatherMap  والحصول على API key. لن تستغرق هذه العملية أكثر من دقيقتين.

All Articles