Menyimpan logika bisnis di Swift Combine. Bagian 1

Menggabungkan Data Berorientasi





Terjemahan artikel disiapkan khusus untuk siswa dari kursus lanjutan "Pengembang iOS" .





Dalam seri posting sebelumnya , kami berhasil membangun platform di atas SwiftUI, yang dengannya Anda dapat dengan bebas mengamati urutan nilai yang melewati penerbit Combine.

Kami juga menciptakan serangkaian contoh menunjukkan beberapa Menggabungkan operator default yang mampu memodifikasi dan mengubah nilai-nilai dalam urutan seperti filtersebagai map, dropdan scan. Selain itu, kami telah menghadirkan beberapa operator yang menghubungkan ( Zipdan CombineLatest) atau menyatukan ( Mergedan Append) urutan.

Pada titik ini, beberapa dari Anda mungkin bosan karena harus mengatur dan memelihara begitu banyak kode untuk setiap contoh (setidaknya saya sudah lelah). Lihat berapa banyak dari mereka yang ada di repositori combin-magic-swiftui di folder tutorial? Masing-masing contoh adalah representasi dari SwiftUI. Masing-masing dari mereka hanya mentransfer satu atau beberapa penerbit ke StreamView, dan StreamViewmenandatangani penerbit di klik tombol.

Oleh karena itu, saya harus dapat membuat daftar penerbit di program startup dan menggunakan kembali secara terprogram StreamView, seperti pada tangkapan layar di bawah ini.



Namun, masalah dengan solusi ini adalah skalabilitas, saat Anda harus membuat banyak penerbit.

Solusi saya untuk masalah ini adalah entah bagaimana menyimpan penerbit ini. Jika saya bisa membuat serialisasi entah bagaimana caranya, saya bisa menyimpannya. Jika saya berhasil menyimpannya, saya tidak hanya dapat memodifikasinya tanpa mengubah kode, tetapi saya juga dapat membaginya dengan perangkat lain yang mendukung Combine .

Penyimpanan dan transfer operator Gabungkan


Sekarang mari kita lihat tujuan kita di sini secara lebih spesifik. Karena kami memiliki daftar aliran dan operator dalam format Publisher, kami ingin dapat menyimpannya dalam segala jenis penyimpanan - misalnya, pada hard disk atau dalam database.

Jelas, kami juga harus dapat mengkonversi data yang disimpan kembali ke penerbit, tetapi selain itu, kami ingin dapat bertukar, mentransfer, dan mendistribusikan penerbit ini dengan operator dari satu tempat ke tempat lain.

Setelah kami membuat struktur seperti itu, seperti yang Anda duga, dalam lingkungan terdistribusi, layanan terpusat dapat mulai mengelola logika komputasi untuk sekelompok klien.



Struktur yang Dapat Dikodekan


Jadi bagaimana kita melakukan ini? Kami akan mulai dengan mengembangkan struktur yang serializable dan deserializable. Protokol Swift Codable memungkinkan kita melakukan ini melalui JSONEncoderdan JSONDecoder. Selain itu, struktur harus benar mewakili data dan perilaku untuk unit nilai terkecil dalam aliran, hingga rantai operator yang kompleks.

Sebelum beralih untuk memahami komponen apa yang diperlukan untuk struktur yang akan kita buat, mari kita ingat aliran utama yang kita buat di seri posting sebelumnya .

Aliran angka




Ini adalah aliran termudah; Namun, jika Anda melihat lebih dalam, Anda akan melihat bahwa ini bukan hanya urutan array. Setiap blok bundar memiliki operator penundaan sendiri ( delay), yang menentukan waktu aktual kapan harus dikirim. Setiap nilai dalam Combine terlihat seperti ini:

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

Dan secara umum, semuanya terlihat seperti ini:

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)

Setiap nilai ditunda selama sedetik, dan pernyataan yang sama ditambahkan ke nilai berikutnya delay.

Karena itu, kami belajar dua hal dari pengamatan kami.

  1. Aliran bukanlah unit terkecil dalam struktur. Yang terkecil adalah nilai stream.
  2. Setiap nilai stream dapat memiliki operator tanpa batas yang mengontrol kapan dan nilai apa yang dikirimkan.

Buat StreamItem Anda


Karena nilai stream dan operatornya adalah unit terkecil, kami mulai dengan membuat strukturnya. Mari kita panggil dia StreamItem.

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

StreamItemtermasuk nilai stream dan array operator. Menurut persyaratan kami, kami ingin dapat menyimpan semuanya dalam struktur sehingga keduanya value, dan StreamItemmematuhi protokol Codable.

Nilai streaming harus universal untuk mengakomodasi semua jenis nilai.

Buat StreamModel Anda


Kami akan membahas struktur untuk operator nanti. Mari kita hubungkan array StreamItemke StreamModel.

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

StreamModelberisi larik StreamItems. StreamModeljuga memiliki properti pengidentifikasi, nama, dan deskripsi. Sekali lagi, semua yang ada di StreamModeldalamnya harus dapat dikodekan untuk penyimpanan dan distribusi.

Buat struktur operator


Seperti yang kami sebutkan sebelumnya, operator delaydapat mengubah waktu transmisi StreamItem.

enum Operator {
 case delay(seconds: Double)
}

Kami menganggap operator delaysebagai enumerasi ( enum) dengan satu nilai terkait untuk menyimpan waktu tunda.

Tentu saja, enumerasi Operatorjuga harus cocok Codable, yang mencakup pengkodean dan dekode nilai terkait. Lihat implementasi penuh di bawah ini.

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)
        }
    }

}

Kami sekarang memiliki struktur yang baik untuk mewakili aliran sekuensial ini, yang menghasilkan nilai dari 1 hingga 4 dengan interval penundaan kedua.

l
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)

Ubah StreamModel menjadi Publisher


Sekarang kita telah membuat turunan dari aliran; namun, jika kami tidak mengubahnya menjadi penerbit, semuanya akan menjadi tidak berarti. Mari mencoba.

Pertama-tama, setiap model operator mengacu pada operator Combine yang sebenarnya, yang harus ditambahkan ke penerbit ini dan dikembalikan ke penerbit yang dioperasikan.

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()
  }
 }
}

Saat ini hanya ada satu jenis operator - delay. Kami akan menambahkan lebih banyak seiring berjalannya waktu.

Sekarang kita dapat mulai menggunakan penerbit untuk semua orang 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
}
}

Kami mulai dengan nilai Just, menggeneralisasikannya menggunakan metode eraseToAnyPublisher, dan kemudian menggunakan penerbit dari semua operator terkait.

Pada tingkat StreamModelkami mendapatkan penerbit dari seluruh aliran.

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()
 }
}

Anda menebaknya dengan benar: kami menggunakan metode ini appenduntuk menggabungkan penerbit.

Visualisasi, pengeditan, dan sekali lagi visualisasi aliran


Sekarang kita cukup mendekode penerbit, mentransfer dan membuat StreamView (lihat bagaimana kami melakukannya di posting sebelumnya ). Dan yang tak kalah pentingnya: sekarang kita cukup mengedit StreamModel, menambahkan yang lain StreamItemdengan nilai baru dan bahkan membagikan model ini dengan perangkat lain melalui Internet.

Lihat demo di bawah ini. Sekarang kita dapat membuat perubahan ke aliran tanpa mengubah kode.



Bab selanjutnya: Filter Serialisasi / Deserialisasi dan Operator Peta


Pada bagian selanjutnya, kita akan menambahkan lebih banyak operator ke enumerasi operator dan mulai menerapkannya pada level utas.

Sampai nanti, Anda dapat menemukan kode sumber di sini di repositori kombinasi-sihir-swifui di folder taman bermain-gabungan.

Kami menunggu komentar Anda dan diundang ke webinar terbuka dengan topik "Aplikasi-iOS di SwiftUI menggunakan Kotlin Mobile Multiplatform".

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


All Articles