Combine 系列
- Swift Combine 从入门到精通一
- Swift Combine 发布者订阅者操作者 从入门到精通二
- Swift Combine 管道 从入门到精通三
- Swift Combine 发布者publisher的生命周期 从入门到精通四
- Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五
- Swift Combine 订阅者Subscriber的生命周期 从入门到精通六
- Swift 使用 Combine 进行开发 从入门到精通七
- Swift 使用 Combine 管道和线程进行开发 从入门到精通八
- Swift Combine 使用 sink, assign 创建一个订阅者 从入门到精通九
- Swift Combine 使用 dataTaskPublisher 发起网络请求 从入门到精通十
- Swift Combine 用 Future 来封装异步请求 从入门到精通十一
- Swift Combine 有序的异步操作 从入门到精通十二
- Swift Combine 使用 flatMap 和 catch错误处理 从入门到精通十三
- Swift Combine 网络受限时从备用 URL 请求数据 从入门到精通十四
- Swift Combine 通过用户输入更新声明式 UI 从入门到精通十五
- Swift Combine 级联多个 UI 更新,包括网络请求 从入门到精通十六
- Swift Combine 合并多个管道以更新 UI 元素 从入门到精通十七
1. 通过包装基于 delegate 的 API 创建重复发布者
目的: 将 Apple delegate API 之一包装为 Combine 管道来提供值。
Future 发布者非常适合包装现有代码以发出单个请求,但它不适用于产生冗长或可能无限量输出的发布者。
Apple 的 Cocoa API 倾向于使用对象/代理模式,你可以选择接收任意数量的不同回调(通常包含数据)。 其中一个例子是在 CoreLocation 库中,提供了许多不同的数据源。
如果你想在管道中使用此类 API 之一提供的数据,你可以将对象包装起来,并使用 passthroughSubject 来暴露发布者。 下面的示例代码显示了一个包装 CoreLocation 中 CLManager 的对象并通过 UIKit 的 ViewController 消费其数据的示例。
UIKit-Combine/LocationHeadingProxy.swift
import Foundation
import Combine
import CoreLocation
final class LocationHeadingProxy: NSObject, CLLocationManagerDelegate {
let mgr: CLLocationManager // 1
private let headingPublisher: PassthroughSubject<CLHeading, Error> // 2
var publisher: AnyPublisher<CLHeading, Error> // 3
override init() {
mgr = CLLocationManager()
headingPublisher = PassthroughSubject<CLHeading, Error>()
publisher = headingPublisher.eraseToAnyPublisher()
super.init()
mgr.delegate = self // 4
}
func enable() {
mgr.startUpdatingHeading() // 5
}
func disable() {
mgr.stopUpdatingHeading()
}
// MARK - delegate methods
/*
* locationManager:didUpdateHeading:
*
* Discussion:
* Invoked when a new heading is available.
*/
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
headingPublisher.send(newHeading) // 6
}
/*
* locationManager:didFailWithError:
* Discussion:
* Invoked when an error has occurred. Error types are defined in "CLError.h".
*/
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
headingPublisher.send(completion: Subscribers.Completion.failure(error)) // 7
}
}
- CLLocationManager 作为 CoreLocation 的一部分,是被包装的核心。 因为要使用该框架,它有其他方法需要被调用,因此我将它暴露为一个 public 的只读属性。 这对于先请求用户许可然后使用位置 API 很有用,框架将该位置 API 暴露为一个在
CLLocationManager
上的方法。 - 使用一个具有我们要发布的数据类型的 private 的
PassthroughSubject
实例,来提供我们的类内部访问以转发数据。 - 一个
public
的属性publisher
将来自上面的subject
的发布者暴露给外部以供订阅。 - 其核心是将该类指定为
CLLocationManager
实例的代理,在该实例初始化的尾端进行设置。 - CoreLocation API 不会立即开始发送信息。 有些方法需要调用才能启动(并停止)数据流,这些方法被包装并暴露在此
LocationHeadingProxy
对象上。 大多数发布者都设置为订阅并根据订阅驱动消费,因此这有点不符合发布者如何开始生成数据的规范。 - 在定义代理和激活
CLLocationManager
后,数据将通过在 CLLocationManagerDelegate 上定义的回调提供。 我们为这个包装的对象实现了我们想要的回调,并在其中使用passthroughSubject.send()
将信息转发给任何现有的订阅者。 - 虽然没有严格要求,但代理提供了
Error
上报回调,因此我们也将其包括在示例中通过passthroughSubject
转发。
2. UIKit-Combine/HeadingViewController.swift
import UIKit
import Combine
import CoreLocation
class HeadingViewController: UIViewController {
var headingSubscriber: AnyCancellable?
let coreLocationProxy = LocationHeadingProxy()
var headingBackgroundQueue: DispatchQueue = DispatchQueue(label: "headingBackgroundQueue")
// MARK - lifecycle methods
@IBOutlet weak var permissionButton: UIButton!
@IBOutlet weak var activateTrackingSwitch: UISwitch!
@IBOutlet weak var headingLabel: UILabel!
@IBOutlet weak var locationPermissionLabel: UILabel!
@IBAction func requestPermission(_ sender: UIButton) {
print("requesting corelocation permission")
let _ = Future<Int, Never> { promise in // 1
self.coreLocationProxy.mgr.requestWhenInUseAuthorization()
return promise(.success(1))
}
.delay(for: 2.0, scheduler: headingBackgroundQueue) // 2
.receive(on: RunLoop.main)
.sink { _ in
print("updating corelocation permission label")
self.updatePermissionStatus() // 3
}
}
@IBAction func trackingToggled(_ sender: UISwitch) {
switch sender.isOn {
case true:
self.coreLocationProxy.enable() // 4
print("Enabling heading tracking")
case false:
self.coreLocationProxy.disable()
print("Disabling heading tracking")
}
}
func updatePermissionStatus() {
let x = CLLocationManager.authorizationStatus()
switch x {
case .authorizedWhenInUse:
locationPermissionLabel.text = "Allowed when in use"
case .notDetermined:
locationPermissionLabel.text = "notDetermined"
case .restricted:
locationPermissionLabel.text = "restricted"
case .denied:
locationPermissionLabel.text = "denied"
case .authorizedAlways:
locationPermissionLabel.text = "authorizedAlways"
@unknown default:
locationPermissionLabel.text = "unknown default"
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// request authorization for the corelocation data
self.updatePermissionStatus()
let corelocationsub = coreLocationProxy
.publisher
.print("headingSubscriber")
.receive(on: RunLoop.main)
.sink { someValue in // 5
self.headingLabel.text = String(someValue.trueHeading)
}
headingSubscriber = AnyCancellable(corelocationsub)
}
}
CoreLocation
的特点之一是要向用户请求访问数据的许可。 启动此请求的 API 将立即返回,但即使用户允许或拒绝请求,它并不提供任何详细信息。CLLocationManager
类包括信息,并在想要获取信息时将其作为类方法暴露给外部,但未提供任何信息来了解用户何时或是否响应了请求。 由于操作不提供任何返回信息,我们将整数提供给管道作为数据,主要表示已发出请求。- 由于没有明确的方法来判断用户何时会授予权限,但权限是持久的,因此在尝试获取数据之前,我们简单地使用了
delay
操作符。 此使用只会将值的传递延迟两秒钟。 - 延迟后,我们调用类方法,并尝试根据当前提供的状态的结果更新界面中的信息。
- 由于
CoreLocation
需要调用方法来明确启用或禁用数据,因此将我们发布者 proxy 的方法连接到了一个UISwitch
的IBAction
开关上。 - 方位数据在本
sink
订阅者中接收,在此示例中,我们将其写到文本 label 上。
参考
https://heckj.github.io/swiftui-notes/index_zh-CN.html
代码
https://github.com/heckj/swiftui-notes