حفظ منطق الأعمال في Swift Combine. الجزء الأول

دمج البيانات الموجهة





تم إعداد ترجمة المقال خصيصًا لطلاب الدورة المتقدمة "iOS Developer" .





في السلسلة السابقة من المنشورات ، نجحنا في بناء نظام أساسي فوق SwiftUI ، يمكنك من خلاله مراقبة تسلسل القيم التي تمر عبر الناشر Combine.

نحن أيضا خلق سلسلة من الأمثلة التي تبين العديد الجمع بين مشغلي الافتراضية التي هي قادرة على تعديل وتحويل القيم في تسلسل مثل filterكما map، dropو scan. بالإضافة إلى ذلك ، قدمنا ​​عددًا قليلاً من عوامل التشغيل التي تربط ( Zipو CombineLatest) أو توحد ( Mergeو Append) التسلسل.

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

لذلك ، يجب أن أتمكن من إنشاء قائمة بالناشرين برمجيًا عند بدء تشغيل التطبيق وإعادة استخدامه StreamView، كما في لقطة الشاشة أدناه.



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

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

الجمع بين التخزين ونقل المشغلين


الآن دعونا نلقي نظرة على أهدافنا هنا بشكل أكثر تحديدًا. نظرًا لأن لدينا قائمة بالتدفقات والعوامل بتنسيق Publisher، نود أن نكون قادرين على حفظها في أي نوع من التخزين - على سبيل المثال ، على قرص ثابت أو في قاعدة بيانات.

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

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



هيكل قابل للتشفير


فكيف نفعل هذا؟ سنبدأ بتطوير هيكل قابل للتسلسل ونزع التسلسل. Codable يتيح لنا بروتوكول Swift القيام بذلك من خلال JSONEncoderو JSONDecoder. علاوة على ذلك ، يجب أن يمثل الهيكل البيانات والسلوكيات بشكل صحيح لأصغر وحدة قيمة في التدفق ، حتى السلاسل المعقدة من المشغلين.

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

دفق الأرقام




هذا هو أسهل تيار ؛ ومع ذلك ، إذا نظرت أعمق ، ستلاحظ أن هذا ليس مجرد سلسلة من المصفوفات. كل من الكتل المستديرة لها عامل تأخير خاص بها ( delay) ، والذي يحدد الوقت الفعلي الذي يجب إرساله فيه. تبدو كل قيمة في Combine كما يلي:

Just(value).delay(for: .seconds(1), scheduler: DispatchQueue.main)

وبشكل عام ، يبدو كل هذا كما يلي:

let val1 = Just(1).delay(for: .seconds(1), scheduler:   DispatchQueue.main)
let val2 = Just(2).delay(for: .seconds(1), scheduler: DispatchQueue.main)
let val3 = ....
let val4 = ....
let publisher = val1.append(val2).append(val3).append(val4)

يتم تأخير كل قيمة لمدة ثانية ، ويتم إضافة نفس العبارة إلى القيمة التالية delay.

لذلك ، نتعلم شيئين من ملاحظاتنا.

  1. الدفق ليس أصغر وحدة في الهيكل. الأصغر هو قيمة الدفق.
  2. يمكن أن يكون لكل قيمة دفق عوامل تشغيل غير محدودة تتحكم في متى يتم إرسال القيمة وما هي القيمة.

إنشاء StreamItem الخاص بك


نظرًا لأن قيمة الدفق ومشغليه هي أصغر وحدة ، فإننا نبدأ بإنشاء هيكلها. دعنا ندعو لها StreamItem.

struct StreamItem<T: Codable>: Codable {
 let value: T
 var operators: [Operator]
}

StreamItemيتضمن قيمة الدفق ومجموعة من العوامل. وفقًا لمتطلباتنا ، نريد أن نكون قادرين على الحفاظ على كل شيء في الهيكل بحيث كلاهما value، StreamItemوالامتثال للبروتوكول Codable.

يجب أن تكون قيمة الدفق عامة لاستيعاب أي نوع من القيمة.

إنشاء StreamModel الخاص بك


سنناقش هيكل المشغلين في وقت لاحق. لنقم بتوصيل الصفيف StreamItemبـ StreamModel.

struct StreamModel<T: Codable>: Codable, Identifiable {
 var id: UUID
 var name: String?
 var description: String?
 var stream: [StreamItem<T>]
}

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

إنشاء هيكل عامل


كما ذكرنا سابقًا ، delayيمكن للمشغلين تغيير وقت الإرسال StreamItem.

enum Operator {
 case delay(seconds: Double)
}

نعتبر عامل التشغيل delayتعدادًا ( enum) بقيمة مرتبطة واحدة لتخزين وقت التأخير.

بالطبع ، Operatorيجب أن يتطابق التعداد أيضًا Codable، والذي يتضمن القيم ذات الصلة بالترميز وفك التشفير. انظر التنفيذ الكامل أدناه.

enum Operator {
    case delay(seconds: Double)
}

extension Operator: Codable {

    enum CodingKeys: CodingKey {
        case delay
    }

    struct DelayParameters: Codable {
        let seconds: Double
    }

    enum CodingError: Error { case decoding(String) }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let delayParameters = try? container.decodeIfPresent(DelayParameters.self, forKey: .delay) {
            self = .delay(seconds: delayParameters.seconds)
            return
        }
        throw CodingError.decoding("Decoding Failed. \(dump(container))")
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .delay(let seconds):
            try container.encode(DelayParameters(seconds: seconds), forKey: .delay)
        }
    }

}

لدينا الآن بنية جيدة لتمثيل هذا التدفق المتسلسل ، الذي يولد القيم من 1 إلى 4 بفاصل زمني تأخير ثان.

ل
et streamA = (1...4).map { StreamItem(value: $0,
operators: [.delay(seconds: 1)]) }
let serialStreamA = StreamModel(id: UUID(), name: "Serial Stream A",
description: nil, stream: streamA)

تحويل StreamModel إلى الناشر


الآن قمنا بإنشاء مثيل من الدفق ؛ ومع ذلك ، إذا لم نحولها إلى ناشر ، فسيكون كل شيء بلا معنى. لنحاول.

بادئ ذي بدء ، يشير كل نموذج عامل إلى عامل الجمع الفعلي ، الذي يجب إضافته إلى هذا الناشر وإعادته إلى الناشر الذي يتم تشغيله.

extension Operator {
func applyPublisher<T>(_ publisher: AnyPublisher<T, Never>) -> AnyPublisher<T, Never> {
  switch self {
    case .delay(let seconds):
    return publisher.delay(for: .seconds(seconds), scheduler: DispatchQueue.main).eraseToAnyPublisher()
  }
 }
}

في الوقت الحالي هناك نوع واحد فقط من عامل التشغيل - delay. سنضيف المزيد ونحن نمضي.

الآن يمكننا البدء في استخدام الناشرين للجميع StreamItem.

extension StreamItem {
 func toPublisher() -> AnyPublisher<T, Never> {
   var publisher: AnyPublisher<T, Never> =
                  Just(value).eraseToAnyPublisher()
   self.operators.forEach {
      publisher = $0.applyPublisher(publisher)
   }
  return publisher
}
}

نبدأ بالقيمة Just، ونعممها باستخدام الطريقة eraseToAnyPublisher، ثم نستخدم الناشرين من جميع عوامل التشغيل ذات الصلة.

على المستوى ، StreamModelنحصل على ناشر الدفق بأكمله.

extension StreamModel {
 func toPublisher() -> AnyPublisher<T, Never> {
   let intervalPublishers =
        self.stream.map { $0.toPublisher() }
   var publisher: AnyPublisher<T, Never>?
   for intervalPublisher in intervalPublishers {
     if publisher == nil {
       publisher = intervalPublisher
       continue
     }
     publisher =
        publisher?.append(intervalPublisher).eraseToAnyPublisher()
   }
   return publisher ?? Empty().eraseToAnyPublisher()
 }
}

لقد خمنت أنه صحيح: نستخدم الطريقة appendلدمج الناشرين.

التصور والتحرير والتصور مرة أخرى لتيار


الآن يمكننا ببساطة فك شفرة الناشر ، ونقل وإنشاء StreamView (انظر كيف فعلنا ذلك في المنشورات السابقة ). وأخيرًا وليس آخرًا: الآن يمكننا ببساطة تعديل StreamModelوإضافة أخرى StreamItemبقيم جديدة وحتى مشاركة هذا النموذج مع أجهزة أخرى عبر الإنترنت.

شاهد العرض التوضيحي أدناه. الآن يمكننا إجراء تغييرات على الدفق بدون تغيير الرمز.



الفصل التالي: تسلسل / إلغاء تسلسل المرشحات ومشغلي الخرائط


في الجزء التالي ، سنقوم بإضافة المزيد من عوامل التشغيل إلى تعداد عامل التشغيل والبدء في تطبيقها على مستوى مؤشر الترابط.

حتى المرة القادمة ، يمكنك العثور على شفرة المصدر هنا في مستودع comb-magic-swifui في مجلد ملعب اللعب.

نحن في انتظار تعليقاتك وندعوك إلى ندوة عبر الإنترنت مفتوحة حول موضوع "تطبيق iOS على SwiftUI باستخدام Kotlin Mobile Multiplatform".

Source: https://habr.com/ru/post/undefined/


All Articles