Swift Combine框架

3,410 阅读4分钟

Combine是苹果2019推出的响应式框架,用来处理随时间变化的事件,Combine有3个要素

  1. Publishers:产生值,遵循 Publisher 协议的对象能发送随时间变化的值序列。协议中有两个关联类型:Output 是产生值的类型;Failure 是异常类型
  2. Operators:是特殊的方法,它能被 Publishers 调用并且返回相同的或者不同的 Publisher。Operator 描述了对一个值进行修改、增加、删除或者其他操作的行为。你可以通过链式调用将这些操作组合在一起,进行复杂的运算
  3. Subscribers:Subscriber 是另一个协议。跟 Publisher 协议类似,它也有两个关联类型:Input 和 Failure。这两个类型必须和 Publisher 中的 Output 和 Failure 类型相对应。 目前 Combine 提供了两个内置的 subscribers: - sink:可以让我们提供一个 closure 来接收发出的值和结束事件。 - assign:可以让我们直接把发出的值绑定到数据模型或者 UI 控件的属性上,直接把最新的数据显示在 UI 上,不需要我们编写任何自定义代码。 如果这两个内置的 subscribers 无法满足需求,我们可以很容易地自定义 subscribers 使用Combine
let myNotification = Notification.Name("MyNotification")
let publisher = NotificationCenter.default
    .publisher(for: myNotification, object: nil)
let center = NotificationCenter.default
let subscription = publisher.sink { _ in
    print("Notification received from a publisher!")
}
center.post(name: myNotification, object: nil)
subscription.cancel() // 取消订阅

调用 cancel 之后,闭包里面的打印就不会再输出sink ;如果不调用 cancel,闭包就会不断的收到 publisher 发出的通知。

sink 还有另外一个重载方法,接收两个闭包(一个处理结束事件,另外一个处理值事件)。例如:

let just = Just("Hello world!")
_ = just
    .sink(
        receiveCompletion: {
            print("Received completion", $0)
    },
        receiveValue: {
            print("Received value", $0)
    })

使用 assign(to:on:) 订阅,我们可以直接把接收到的值直接赋值给支持 KVO 的对象的某个属性。例如:

class SomeObject {
    var value: String = "" {
        didSet {
            print(value)
        }
    }
}

let object = SomeObject()
let publisher = ["Hello", "world!"].publisher
publisher.assign(to: \.value, on: object)

当一个订阅结束,并且不想再从 publisher 那里接收任何事件,我们需要取消订阅以释放资源或者停止对应的活动继续进行。

在订阅 publisher 时会返回一个 AnyCancellable 实例,当我们需要时,可以利用这个实例来取消订阅。AnyCancellable 实现了 Cancellable 协议,这个协议有一个 cancel() 方法。 如果没有调用 cancel() 方法,订阅会一直持续,除非 publisher 发出了结束事件,或者正常的内存管理把这个订阅取消。

自定义subscriber

let publisher = (1...6).publisher

final class IntSubscriber: Subscriber {
    typealias Input = Int
    typealias Failure = Never

    func receive(subscription: Subscription) {
        subscription.request(.max(3))
    }

    func receive(_ input: Int) -> Subscribers.Demand {
        print("Received value", input)
        return .none
    }

    func receive(completion: Subscribers.Completion<Never>) {
        print("Received completion", completion)
    }
}

首先定义了一个整型 publisher。 定义了一个自定义的 IntSubscriber: 1)能接收 Int 类型的值; 2)Failure 类型是 Never 表示不接收错误; 3)receive(subscription:) 方法将会被 publisher 内部调用,在方法的实现中,通过调用 request(:) 设置 subscriber 能接收的值的最大数量; 4)receive(:) 方法的实现中,打印 input,并且返回 .none,意味着 subscriber 不会对 Demand 做出调整,等同于返回 .max(0); 5)receive(completion:) 方法实现中打印 completion。

let subscriber = IntSubscriber()
publisher.subscribe(subscriber)

Transform 操作符 collect()

["A", "B", "C", "D", "E"].publisher
    .collect()
    .sink(
        receiveCompletion: { print($0) }, receiveValue: { print($0) }
)

运行后结果如下:

["A", "B", "C", "D", "E"]
finished

映射,map(_:),这个 map 跟 Swift 标准的 map 非常类似,把从 publisher 接收到的值映射为另外一个值。例如:

let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
[123, 4, 56].publisher
    .map {
        formatter.string(for: NSNumber(integerLiteral: $0)) ?? ""
}
    .sink(receiveValue: { print($0) })

运行后,结果如下:

one hundred twenty-three
four
fifty-six

Combine实际使用例子: URLSession

let subscription = URLSession.shared
    .dataTaskPublisher(for: url)
    .map(\.data)
    .decode(type: MyType.self, decoder: JSONDecoder())
    .sink(
        receiveCompletion: { completion in
            if case .failure(let err) = completion {
                print("Retrieving data failed with error \(err)")
            }
    },
        receiveValue: { object in
            print("Retrieved object \(object)")
    })

Timers,你可以使用下面这种方式创建一个重复的 timer publisher:

let subscription = Timer
    .publish(every: 1.0, on: .main, in: .common)
    .autoconnect()
    .scan(0) { counter, _ in counter + 1 }
    .sink { counter in
        print("Counter is \(counter)")
    }

on 参数是指定 timer 的放在哪个 RunLoop。这里是 main RunLoop。 in 参数是指定 timer 在 run loop 的哪种模式下运行。这里是 .common run loop 模式。

Key-Value Observing

class TestObject: NSObject {
    @objc dynamic var integerProperty: Int = 0
}

let obj = TestObject()

let subscription = obj.publisher(for: \.integerProperty)
    .sink {
        print("integerProperty changes to \($0)")
    }

obj.integerProperty = 100
obj.integerProperty = 200

运行结果:

integerProperty changes to 0
integerProperty changes to 100
integerProperty changes to 200

Combing错误管理,Failure 类型为 Never 的 publisher 表示该 publisher 永远不会发出错误。Failure 类型为 Never 的 publisher 允许您集中精力使用 publisher 的值,同时确保 publisher 永远不会发出错误。

enum NameError: Error {
    case tooShort(String)
    case unknown
}

let names = ["Scott", "Marin", "Shai", "Florent"].publisher

names
    .tryMap { value -> Int in
        let length = value.count
        guard length >= 5 else {
            throw NameError.tooShort(value)
        }
        return value.count
    }
    .sink(
        receiveCompletion: { print("Completed with \($0)") },
        receiveValue: { print("Got value: \($0)") }
    )

运行结果如下:

Got value: 5
Got value: 5
Completed with failure(__lldb_expr_23.NameError.tooShort("Shai"))

一个Combine例子:

var subscriptions = Set<AnyCancellable>()

let photoService = PhotoService()

photoService
    .fetchPhoto(quality: .high) // 获取高质量的图片
    .retry(3) // 如果获取失败,则重试三次
    .catch { error -> PhotoService.Publisher in
        print("Failed fetching high quality, falling back to low quality")
            return photoService.fetchPhoto(quality: .low) // 获取低质量的图片
    }
    .replaceError(with: UIImage(named: "na.jpg")!)
    .sink(
        receiveCompletion: { print("\($0)") },
        receiveValue: { image in
            image
            print("Got image: \(image)")
    }
)
    .store(in: &subscriptions)  // 异步的订阅需要保存起来

首先通过 fetchPhoto(quality: .high) 获取高质量的图片。 如果获取失败,则通过 retry(3) 重试 3 次。 如果重试三次后还是失败,则 catch 会捕获到错误,然后通过 photoService.fetchPhoto(quality: .low) 去获取低质量的图片。 如果获取低质量的图片还是失败,replaceError 就会把错误替换成一个默认的图片,相当于我们在给 UIImageView 设置图片时,通常会设置一个 placeholder 图片。 最后通过 sink 订阅。