Objective-C
Xcode
iOS
MVVM
Swift

Swift4 KVOを使ってRxSwiftみたいにDataBindingする

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の値を監視してみます。

参考 Swift4のKVOに感動した。

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
}
}
}


ViewModel.swift


final class ViewModel: NSObject {
@objc private (set) dynamic var bindString: String? = ""
}


ViewController.swift

    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