LoginSignup
7

More than 5 years have passed since last update.

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

Posted at

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7