Combine是苹果2019推出的响应式框架,用来处理随时间变化的事件,Combine有3个要素
- Publishers:产生值,遵循 Publisher 协议的对象能发送随时间变化的值序列。协议中有两个关联类型:Output 是产生值的类型;Failure 是异常类型
- Operators:是特殊的方法,它能被 Publishers 调用并且返回相同的或者不同的 Publisher。Operator 描述了对一个值进行修改、增加、删除或者其他操作的行为。你可以通过链式调用将这些操作组合在一起,进行复杂的运算
- 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 订阅。