Swift4でKVOが使いやすくなったのは知ってたのですが、結局触らずに1年以上たってしまいました。
最近になりiOSエンジニアとして再び働くことになったため、勉強がてらに少し触ってみようと、個人開発のアプリでKVOを使ったDataBindingを試してみたのでアウトプットします。
使用例
@objcMembers class ViewModel: NSObject {
private (set) dynamic var count = 0
private (set) dynamic var title: String? = ""
}
private var bag = DisposableBag()
private let viewModel = ViewModel()
private let label = UILabel()
func bindToViewModel() {
viewModel.subscribe(\.count) { [weak self] count in
self?.label.text = "count: \(count)"
}.dispose(by: bag)
viewModel
.bind(\.title, to: label, at: \.text)
.dispose(by: bag)
}
Swift4のKVO
まず普通にSwift4のKVOを使ってViewModelの値を監視してみます。
final class ViewModel: NSObject {
@objc private (set) dynamic var count = 0
}
final class ViewController: UIViewController {
private var observations: [NSKeyValueObservation] = []
private let viewModel = ViewModel()
private let countLabel = UILabel()
func bindToViewModel() {
let countObservations = viewModel.observe(\.count, options: .new) { [weak self] _, change in
guard let `self` = self, let newValue = change.newValue else {
return
}
self.countLabel.text = "count: \(count)"
}
observations.append(countObservations)
}
}
結構スッキリかけるようになったんですね。
ただ、NSObjectを継承したりdynamic修飾子を使わないといけないのはしょうがないとしても
もう少し簡潔にかけたら嬉しいと思います。
Extensionを実装してみる
extension NSObjectProtocol where Self: NSObject {
func observe<Value>(_ keyPath: KeyPath<Self, Value>, changeHandler: @escaping (Value) -> ()) -> NSKeyValueObservation {
return observe(keyPath, options: .new) { _, change in
guard let newValue = change.newValue else { return }
changeHandler(newValue)
}
}
}
func bindToViewModel() {
let countObservations = viewModel.observe(\.count) { [weak self] count in
self?.countLabel.text = "count: \(count)"
}
observations.append(countObservations)
}
optionの引数とオプショナルバインディングがなくなり少しスッキリしました。
DisposableBagクラスを追加してインターフェースをRxSwiftみたいにする
ただ監視をつずけるためにはobservationを保持しないといけないので少し冗長に感じます。
DisposableBagクラスを作成して、こいつに保持してもらうようにします。
final class DisposableBag {
private var observations: [NSKeyValueObservation] = []
func append(_ observation: NSKeyValueObservation) {
observations.append(observation)
}
}
次にNSKeyValueObservationにDisposableプロトコルをつけて
DisposableBagに追加できるようにします。
protocol Disposable: class {
func dispose(by bag: DisposableBag)
}
extension NSKeyValueObservation: Disposable {
func dispose(by bag: DisposableBag) {
bag.append(self)
}
}
戻り値をDisposableに修正します。ついでにメソッド名もsubscribeに変えてみます
extension NSObjectProtocol where Self: NSObject {
func subscribe<Value>(_ keyPath: KeyPath<Self, Value>, changeHandler: @escaping (Value) -> ()) -> Disposable {
return observe(keyPath, options: [.new]) { _, change in
guard let newValue = change.newValue else { return }
changeHandler(newValue)
}
}
}
private var bag = DisposableBag()
private let viewModel = ViewModel()
func bindToViewModel() {
viewModel.subscribe(\.count) { [weak self] count in
self?.label.text = "count: \(count)"
}.dispose(by: bag)
}
RxSwiftぽくなったと思います。
bindメソッドを実装してみる。
extension NSObjectProtocol where Self: NSObject {
func bind<Target, Value>(_ fromKeyPath: KeyPath<Self, Value>,
to target: Target,
at targetKeyPath: ReferenceWritableKeyPath<Target, Value>
) -> Disposable {
return subscribe(fromKeyPath) { newValue in
target[keyPath: targetKeyPath] = newValue
}
}
}
final class ViewModel: NSObject {
@objc private (set) dynamic var bindString: String? = ""
}
private var bag = DisposableBag()
private let viewModel = ViewModel()
private let label = UILabel()
func bindToViewModel() {
viewModel
.bind(\.bindString, to: label, at: \.text)
.dispose(by: bag)
}
こんな感じで一応bindすることができました。ただオプショナルつけないとType of expression is ambiguous without more context
ってなってビルド通らなくて微妙でした。軽く調べた感じSwiftのバグとか出たけど良くわかりませんでした。
終わりに
ちょこっとExtension書くだけでKVOを使ってRxSwiftっぽくDataBindingできるようになった?と思います。
一応最終的なコードをGistにのっけておきます。変更箇所が何点かあります。
https://gist.github.com/churabou/21dc2500fd8b7e6956e06abfe6e5d124