تكييف حل عملك الحالي مع SwiftUI. الجزء 3. العمل مع الهندسة المعمارية

يوم جيد للجميع! معكم ، أنا ، أنا زاركوفا ، مطور جوّال رائد في Usetech.

نواصل تفكيك تعقيدات SwiftUI. يمكن العثور على الأجزاء السابقة في الروابط:

الجزء 1
الجزء 2

اليوم سنتحدث عن ميزات الهيكل ، وكيفية نقل منطق الأعمال الحالي وتضمينه في تطبيق SwiftUI.

يعتمد دفق البيانات القياسي في SwiftUI على تفاعل طريقة العرض ونموذج معين يحتوي على خصائص ومتغيرات الحالة ، أو أي متغير حالة. لذلك ، من المنطقي أن MVVM هو الشريك المعماري الموصى به لتطبيقات SwiftUI. تقترح شركة Apple استخدامه جنبًا إلى جنب مع إطار Combine ، الذي يقدم تعريفًا توضيحيًا لـ Api SwiftUI لمعالجة القيم بمرور الوقت. يقوم ViewModel بتنفيذ بروتوكول ObservableObject ويتصل كبرنامج ObservedObject بعرض معين.



يتم تعريف خصائص النموذج القابلة للتعديل على أنهاPublished.

class NewsItemModel: ObservableObject {
    @Published var title: String = ""
    @Published var description: String = ""
    @Published var image: String = ""
    @Published var dateFormatted: String = ""
}

كما هو الحال في MVVM الكلاسيكي ، يتواصل ViewModel مع نموذج البيانات (أي منطق الأعمال) وينقل البيانات في نموذج أو عرض آخر.

struct NewsItemContentView: View {
    @ObservedObject var moder: NewsItemModel
    
    init(model: NewsItemModel) {
        self.model = model 
    }
    //... - 
}

يميل MVVM ، مثل أي نمط آخر تقريبًا ، إلى الازدحام
والتكرار. يعتمد ViewModel الزائد دائمًا على مدى تمييز منطق الأعمال وتلخيصه. يتم تحديد حمل العرض من خلال تعقيد اعتماد العناصر على متغيرات الحالة والتحولات إلى طريقة عرض أخرى.

في SwiftUI ، ما تتم إضافته هو أن العرض هو بنية ، وليس فئة ، وبالتالي لا يدعم الوراثة ، مما يجبر ازدواج التعليمات البرمجية.
إذا لم يكن هذا في التطبيقات الصغيرة أمرًا بالغ الأهمية ، فعند نمو الوظائف وتعقيد المنطق ، يصبح التحميل الزائد أمرًا بالغ الأهمية ، ويعيق كمية كبيرة من النسخ واللصق.

دعونا نحاول استخدام نهج الشفرة النظيفة والبنية النظيفة في هذه الحالة. لا يمكننا التخلي عن MVVM تمامًا ، بعد كل شيء ، فإن DataFlow SwiftUI مبني عليه ، لكن إعادة بنائه قليلاً.

تحذير!

إذا كان لديك حساسية من المقالات المتعلقة بالهندسة المعمارية ، ويتحول رمز التنظيف إلى الداخل من عبارة ، قم بالتمرير لأسفل فقرتين.
هذا ليس رمز نظيف تمامًا من العم بوب!


نعم ، لن نأخذ كود Clean Uncle Bob's في أنقى صوره. بالنسبة لي ، هناك الكثير من الهندسة فيها. سنتخذ فقط فكرة.

الفكرة الرئيسية من الكود النظيف هي إنشاء الكود الأكثر قابلية للقراءة ، والذي يمكن بعد ذلك توسيعه وتعديله بدون ألم.

هناك عدد غير قليل من مبادئ تطوير البرمجيات التي يوصى بالالتزام بها.



يعرفها الكثير من الناس ، ولكن لا يحبها الجميع ولا يستخدمها الجميع. هذا موضوع منفصل لل holivar.

لضمان نقاء الشفرة ، من الضروري على الأقل تقسيم الشفرة إلى طبقات ووحدات وظيفية ، واستخدام الحل العام للمشكلات وتنفيذ تجريد التفاعل بين المكونات. وتحتاج على الأقل إلى فصل رمز واجهة المستخدم عن ما يسمى بمنطق الأعمال.

بغض النظر عن النمط المعماري المحدد ، يتم فصل منطق العمل مع قاعدة البيانات والشبكة ومعالجة وتخزين البيانات عن واجهة المستخدم ووحدات التطبيق نفسها. في نفس الوقت ، تعمل الوحدات مع تنفيذ الخدمات أو المخازن ، والتي بدورها تصل إلى الخدمة العامة لطلبات الشبكة أو تخزين البيانات العامة. يتم تنفيذ تهيئة المتغيرات التي يمكنك من خلالها الوصول إلى خدمة أو أخرى في حاوية عامة معينة ، والتي تصل إليها وحدة التطبيق (منطق أعمال الوحدة) في النهاية.



إذا حددنا منطق الأعمال وقمنا بتلخيصه ، فيمكننا ترتيب التفاعل بين مكونات الوحدات كما نحب.

من حيث المبدأ ، تعمل جميع الأنماط الحالية لتطبيقات iOS على نفس المبدأ.



هناك دائما منطق الأعمال ، هناك بيانات. هناك أيضًا مدير اتصالات ، وهو المسؤول عن عرض وتحويل البيانات للإخراج وحيث يتم إخراج البيانات المحولة. والفرق الوحيد هو كيفية توزيع الأدوار بين المكونات.


لان نحن نسعى جاهدين لجعل التطبيق قابلاً للقراءة ، لتبسيط التغييرات الحالية والمستقبلية ، فمن المنطقي فصل جميع هذه الأدوار. لقد تم إبراز منطق أعمالنا بالفعل ، ويتم فصل البيانات دائمًا. تبقى المرسل والمقدم والعرض. ونتيجة لذلك ، نحصل على بنية تتكون من View-Interactor-Presenter ، حيث يتفاعل المتفاعل مع خدمات منطق الأعمال ، ويقوم المقدم بتحويل البيانات ويعطيها كنوع من ViewModel إلى طريقة العرض الخاصة بنا. بطريقة جيدة ، يتم أيضًا أخذ التنقل والتكوين من العرض إلى مكونات منفصلة.



نحصل على بنية VIP + R مع تقسيم الأدوار المثيرة للجدل إلى مكونات مختلفة.

دعونا نحاول إلقاء نظرة على مثال. لدينا تطبيق تجميع أخبار صغير
مكتوب بلغة SwiftUI و MVVM.



يحتوي التطبيق على 3 شاشات منفصلة مع منطقه الخاص ، أي 3 وحدات:


  • وحدة قائمة الأخبار ؛
  • وحدة شاشة الأخبار ؛
  • وحدة بحث الأخبار.

تتكون كل وحدة من وحدات العرض ViewModel ، التي تتفاعل مع منطق العمل المحدد ، و View ، والتي تعرض ما تبثه ViewModel إليها.



نحن نسعى جاهدين للتأكد من أن ViewModel معني فقط بتخزين البيانات الجاهزة للعرض. وهو الآن منخرط في كل من الوصول إلى الخدمات ومعالجة النتائج.

ننقل هذه الأدوار إلى مقدم العرض والمتفاعل ، الذي أنشأناه لكل
وحدة نمطية.



يقوم المتفاعل بتمرير البيانات المستلمة من الخدمة إلى مقدم العرض ، مما يملأ ViewModel الموجود المرتبط بـ View بالبيانات المعدة. من حيث المبدأ ، فيما يتعلق بفصل منطق الأعمال للوحدة ، كل شيء بسيط. 


انتقل الآن إلى عرض. دعونا نحاول التعامل مع الازدواجية القسرية. إذا كنا نتعامل مع نوع من التحكم ، فقد تكون أنماطه أو إعداداته. إذا كنا نتحدث عن العرض على الشاشة ، فهذا:

  • أنماط الشاشة
  • عناصر واجهة المستخدم المشتركة (LoadingView) ؛
  • تنبيهات المعلومات ؛
  • بعض الطرق العامة.

لا يمكننا استخدام الميراث ، ولكن يمكننا استخدام التكوين . من خلال هذا المبدأ يتم إنشاء جميع طرق العرض المخصصة في SwiftUI.

لذلك ، نقوم بإنشاء حاوية عرض ، ننقل فيها كل نفس المنطق ، وننقل العرض على الشاشة إلى مُهيئ الحاوية ثم نستخدمه كعرض محتوى داخل الجسم.

struct ContainerView<Content>: IContainer, View where Content: View {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
    }

    var body : some View {
        ZStack {
            content
            if (self.containerModel.isLoading) {
                LoaderView()
            }
        }.alert(isPresented: $containerModel.hasError){
            Alert(title: Text(""), message: Text(containerModel.errorText),
                 dismissButton: .default(Text("OK")){
                self.containerModel.errorShown()
                })
        }
    }

يتم تضمين العرض على الشاشة في ZStack داخل جسم ContainerView ، الذي يحتوي أيضًا على رمز لعرض LoadingView ورمز لعرض تنبيه المعلومات.

نحتاج أيضًا إلى برنامج ContainerView الخاص بنا لتلقي إشارة من ViewModel للعرض الداخلي وتحديث حالته. لا يمكننا الاشتراك عبرObserved على نفس النموذج
مثل العرض الداخلي ، لأننا سنسحب إشاراته.



لذلك ، ننشئ اتصالات معها من خلال نمط التفويض ، وللحالة الحالية للحاوية نستخدم ContainerModel الخاصة بها.

class ContainerModel:ObservableObject {
    @Published var hasError: Bool = false
    @Published var errorText: String = ""
    @Published var isLoading: Bool = false
    
    func setupError(error: String){
     //....
       }
    
    func errorShown() {
     //...
    }
    
    func showLoading() {
        self.isLoading = true
    }
    
    func hideLoading() {
        self.isLoading = false
    }
}

يطبق ContainerView بروتوكول IContainer ؛ يتم تعيين مرجع مثيل لنموذج العرض المضمن.

protocol  IContainer {
    func showError(error: String)
    
    func showLoading()
    
    func hideLoading()
}

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
        self.content.viewModel?.listener = self
    }
    //- 
}

تقوم طريقة العرض بتنفيذ بروتوكول IModelView لتغليف وصول النموذج وتوحيد بعض المنطق. نماذج لنفس الغرض تنفيذ بروتوكول IModel:

protocol IModelView {
    var viewModel: IModel? {get}
}

protocol  IModel:class {
   //....
    var listener:IContainer? {get set}
}

بعد ذلك ، في هذا النموذج بالفعل ، إذا لزم الأمر ، يتم استدعاء طريقة التفويض ، على سبيل المثال ، لعرض تنبيه به خطأ يتغير فيه متغير الحالة لنموذج الحاوية.

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    //- 
    func showError(error: String) {
        self.containerModel.setupError(error: error)
    }
    
    func showLoading() {
        self.containerModel.showLoading()
    }
    
    func hideLoading() {
        self.containerModel.hideLoading()
    }
}

يمكننا الآن توحيد عمل العرض بالتبديل إلى العمل من خلال ContainerView.
سيؤدي ذلك إلى تسهيل حياتنا بشكل كبير عند العمل مع تكوين الوحدات والملاحة التالية.
كيفية تكوين التنقل في SwiftUI وإجراء تكوين نظيف ، سنتحدث في الجزء التالي .

يمكنك العثور على شفرة المصدر للمثال هنا .

All Articles