在Swift Combine中节省业务逻辑。第1部分

面向数据的组合





本文的翻译是专为高级课程“ iOS Developer”的学生准备的





在之前的系列文章中,我们成功地在SwiftUI之上构建了一个平台,您可以通过它自由观察通过发布者 Combine 传递的值序列

我们还创建一系列的实施例展示几种联合缺省操作符是能够改变并且在这样的序列转换值filter作为mapdropscan。此外,我们还介绍了一些连接(ZipCombineLatest)或统一(MergeAppend)序列的运算符

在这一点上,你们中的某些人可能已经厌倦了为每个示例组织和维护如此多的代码(至少我已经很累了)。看看教程文件夹中的Combine-magic-swiftui存储库中有多少个?每个示例都是SwiftUI的表示。他们每个人只需将一个或几个发布者转移到StreamView,然后StreamView单击按钮即可对发布者进行签名。

因此,我应该能够在应用程序启动和重用时以编程方式生成发布者列表StreamView,如下面的屏幕快照所示。



但是,当您需要创建许多发布者时,此解决方案的问题是可伸缩性。

我对这个问题的解决方案是以某种方式存储这些发布者。如果我可以某种方式序列化它们,则可以保存它们。如果我设法保存它们,我不仅可以在不更改代码的情况下对其进行修改,还可以与其他支持Combine的设备共享它们

操作员的存储和转移


现在让我们在这里更具体地了解我们的目标。由于我们具有格式不同的流和运算符的列表Publisher,因此我们希望能够将它们保存在任何类型的存储中,例如保存在硬盘或数据库中。

显然,我们还需要能够将存储的数据转换回发布者,但此外,我们希望能够与运营商将这些发布者交换,转移和分发的位置从另一个地方转移到另一个地方。

正如您可能已经猜到的那样,在我们建立了这样的结构之后,集中式服务便可以开始管理一组客户端的计算逻辑。



可编码结构


那么我们该怎么做呢?我们将从开发可序列化和可反序列化的结构开始。Swift协议Codable 允许我们通过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


稍后我们将讨论运营商的结构。让我们将数组连接StreamItemStreamModel

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

StreamModel包含的数组StreamItemStreamModel还具有标识符,名称和描述属性。同样,其中的所有内容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转换为Publisher


现在,我们创建了流的实例;但是,如果我们不将其转换为发布者,那么一切都将毫无意义。我们试试吧。

首先,每个运算符模型均指实际的Combine运算符,应将其添加到此发布者并返回给操作的发布者。

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具有新值的附加值,甚至可以通过Internet与其他设备共享此模型。

请参见下面的演示。现在,我们可以更改流而不更改代码。



下一章:序列化/反序列化过滤器和映射运算符


在下一部分中,我们将向运算符枚举添加更多运算符,并开始在线程级别应用它们。

在下一次之前,您可以在Combine-Playground文件夹中的Combine-magic-swifui存储库找到源代码

我们正在等待您的评论,并邀请您参加主题为“使用Kotlin Mobile Multiplatform的SwiftUI上的iOS应用程序” 的开放式网络研讨会

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


All Articles