Combine
概要
Combine は、WWDC 2019 で発表された非同期処理のための宣言的 API フレームワーク。Publisher が値を送出し、送出された値は様々な operator (map とか flatMap とか) によって加工、ハンドリングでき、最終的に Subscriber で結果を受け取る。 下記の handbook 凄すぎる... 大体これを読めば良さそう。
基本のき
code:swift
let pub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
受け取った要素を、即座に指定されたオブジェクトのプロパティに割り当てていく
例えば、受け取ったイベントをシンプルにロギングするなら、以下のような感じで書ける。
code:swift
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.sink(receiveCompletion: { print ($0) },
receiveValue: { print ($0) })
sink の receiveValue closure のなかで全ての操作を行うのは場合によってはしんどい。ので、Publisher に様々な operator を chain できる。使い慣れた map, flatMap, reduce などがある。例えば、前述の Notification からテキストフィールドの文字列だけ取り出したいなら、以下のようにかける。 code:swift
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.map( { ($0.object as! NSTextField).stringValue } )
.sink(receiveCompletion: { print ($0) },
receiveValue: { print ($0) })
さらに、結果を直接オブジェクトにバインドしたいなら、以下のようにかける。
code:swift
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.map( { ($0.object as! NSTextField).stringValue } )
.assign(to: \MyViewModel.filterString, on: myViewModel)
他にも、filter でフィルタしたり、debounce でイベント送出を遅延させたり (ユーザの入力完了を待つなどの目的で)、receive で受け取るスレッドを変更したりできる。
code:swift
let sub = NotificationCenter.default
.publisher(for: NSControl.textDidChangeNotification, object: filterField)
.map( { ($0.object as! NSTextField).stringValue } )
.filter( { $0.unicodeScalars.allSatisfy({CharacterSet.alphanumerics.contains($0)}) } )
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.receive(on: RunLoop.main)
.assign(to:\MyViewModel.filterString, on: myViewModel)
キャンセルしたい場合は、Subscriber の cancel() メソッドを呼び出してキャンセルできる。 sink はつないだ瞬間に値が流れ出してしまうため、1 つめをつないだ後に 2 つ目を繋ぐと、2 つ目の Subscriber が値を受け取れない可能性がある。そのような場合には ConnectablePublisher を間に挟んで subscribe する。connect() を明示的に呼び出すため値が流れ始めなくなる。 明示的に呼び出すのが逆に手間な場合は autoconnect() を挟むと値が即座に流れ始める。
Operator
Tips
型情報を削りたい
例えば、UserDefaults を protocol で隠蔽したい時。
code:swift
// 適当な実装クラス
class SettingStorage {
private let userDefaults = UserDefaults.standard
}
// KVOのCombine拡張を利用するために、UserDefaults自体にプロパティを拡張する
extension UserDefaults {
@objc
dynamic var isEnabled: Bool {
return self.bool(forKey: "myKey")
}
}
// 抽象化. ここでは UserDefaults は意識させたくない
protocol SettingStorageProtocol {
var isEnalbed: /* ここのPublisher型はどうする? */ { get }
}
extension SettingStorage: SettingStorageProtocol {
// MARK: - SettingStorageProtocol
var isEnabled: /* ここがNSObject.KeyValueObservingPublisher<UserDefaults, Bool>になる */ {
return self.userDefaults
.publisher(for: \.isEnabled)
}
}
Protocol には UserDefaults の情報を出したくない。つまり、型情報を削りたい。このような場合には eraseToAnyPublisher() を利用するのが良い。doc でも、抽象化の境界で利用するのが良いとの記述がある。 This form of type erasure preserves abstraction across API boundaries, such as different modules. When you expose your publishers as the AnyPublisher type, you can change the underlying implementation over time without affecting existing clients.
利用してみると以下のような感じ
code:swift
protocol SettingStorageProtocol {
var isEnabled: AnyPublisher<Bool, Never> { get }
}
extension SettingStorage: SettingStorageProtocol {
// MARK: - SettingStorageProtocol
var isEnabled: AnyPublisher<Bool, Never> {
return self.userDefaults
.publisher(for: \.isEnabled)
.eraseToAnyPublisher()
}
}
store(in:)