SwiftChainingの解説記事その2です。
前回の記事はこちら -> Swiftでデータバインディングするライブラリを作った
iOSアプリでデータバインディングをするからには、UIと値を同期できないと始まりません。
今回は例として、UISwitchとバインディングする例を紹介します。
UISwitchとバインディングする
UISwitchのスイッチのON・OFFの状態であるisOnと、前回の記事で紹介したValueHolderの値をバインディングしてみます。コードは以下の通りです。
import UIKit
import Chaining
class ExampleViewController: UIViewController {
    @IBOutlet weak var mySwitch: UISwitch!
    
    lazy var isOnAdapter = { KVOAdapter(self.mySwitch, keyPath: \UISwitch.isOn )}()
    lazy var valueChangedAdapter = { UIControlAdapter(self.mySwitch, events: .valueChanged) }()
    
    let isOnHolder = ValueHolder(true)
    let pool = ObserverPool()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.isOnHolder.chain().sendTo(self.isOnAdapter).sync().addTo(self.pool)
        self.valueChangedAdapter.chain().map { $0.isOn }.sendTo(self.isOnHolder).end().addTo(self.pool)
    }
}
解説
KVOに対応したプロパティをSwiftChainingで扱えるようにするためのクラスがKVOAdapterです。
KVOAdapter(self.mySwitch, keyPath: \UISwitch.isOn )
対象となるオブジェクトとプロパティのkeyPathを渡して生成し、値を監視したり更新したりできます。
ValueHolder同士の場合と同じく、以下のようにsendToにKVOAdapterを渡すことでValueHolderからの値を受け取ることができます。
self.isOnHolder.chain().sendTo(self.isOnAdapter).sync()
逆にKVOAdapterからisOnの変更を受け取るのには、以下のように逆方向に繋げれば良いのですが、今回の場合、残念ながらうまくいきません。
// うまくいかない
self.isOnAdapter.chain().sendTo(self.isOnHolder).sync()
UISwitchをタップしてON・OFFの状態を変更したときはKVOでは通知されないので、UIControlのイベントを監視するようにします。
UIControlのイベントを監視するクラスとしてUIControlAdapterを用意しています。
UIControlAdapter(self.mySwitch, events: .valueChanged)
eventsで監視したいUIControlのイベントを指定します。
UIControlAdapterからイベントを監視してValueHolderに値を反映させるには以下のように書きます。
self.valueChangedAdapter.chain().map { $0.isOn }.sendTo(self.isOnHolder).end()
UIControlAdapterから送られる値はUIControl(ここではUISwitch)なので途中でmap関数を使ってisOnの値に変換してValueHolderが受け取れるようにしています。
このように、それぞれ変更があった時に双方向に値を送り合うことで、値を同期することができるという感じになります
SwiftChainingでは値の送信が循環しても延々とループにならないように内部でロックしているので、それこそchainしたオブジェクトをsendToにそのまま渡すようなことをしてもハングアップしたりはしません。
ObserverPool
ちなみに、今回のコードで出てきたクラスにObserverPoolというものがあります。
let pool = ObserverPool()
...
self.isOnHolder.chain().sendTo(self.isOnAdapter).sync().addTo(self.pool)
複数のObserverを保持しておかないといけない時に別々に管理するのは面倒なので、Observerをまとめて保持しておけるものとして用意しています。ObserverPoolを破棄したりinvalidate()を呼んだら、保持しているObserverがまとめて無効になります。
Observerの追加をObserverPool側の関数でやると書きにくかったので、syncやendの後にそのままaddTo()をつなげて書いて追加できるようにしています。
補足
なお今回紹介したコードだと、ValueHolderからUISwitchへ値を反映させる時にアニメーションをしなかったり、UISwitchのisOnをコードで直接変更した時にValueHolderへ値が反映されません。それらが必要であれば、さらにコードを追加・変更することになるでしょう。
SwiftChainingはUIKitをラップして簡単にするものではないので、このようなUIKitの対応は通常と変わらず考える必要があります。