SwiftのProtocolに対してKVOを行う
前回から引き続きKVOについての投稿です.
SwiftにおけるKVOは対象のpropertyの制限 (@objc dynamic
) は広く知られていますが, Protocolに対してKVOを行うのもハードルが高かったりします.
delegate
を自分で実装した方が早い気もしますが, Property Binding的なことをしようとすると, KVOの方が直感的なコードを書けるので悩ましいところです.
この記事では, Swift Protocolに対してKVOをする方法を紹介します.
KVOに対応したProtocol定義
@objc protocol ObservableRunnable {
@objc dynamic var isRunning: Bool { get }
}
@objc dynamic
を明示する為に, @objc protocol
で実装する必要があります.
Swift EnumやTupleといった物が使用できなくなるので, KVOに対応する必要があるものだけを切り出したProtocolとするのが良いでしょう.
protocol Runnable: NSObject, ObservableRunnable {
func start()
func stop()
}
その他の定義は継承したProtocolを定義し, そちらで定義するのが良いでしょう.
本来であれば@objc protocol
側にNSObject
の継承が必須であることを明示したいのですが, @objc protocol
はクラスの継承を明示する方法がありません. (Inheritance from non-protocol type 'NSObject'
となります).
一方で, Swift protocolは記述できるので, ここにNSObject
の継承が必須であることを明示しています.
ちなみに, NSObject
が継承されていないと以下のようなランタイムエラーになります.
Unrecognized selector -[Sample.CustomRunner addObserver:forKeyPath:options:context:]
class CustomRunner: NSObject, Runnable {
@objc dynamic var isRunning: Bool = false
func start() {
isRunning = true
}
func stop() {
isRunning = false
}
}
クラスの実装はこのようになります.
ネストしたkeyPathでKVOを行う
private var runner: Runnable = CustomRunner()
@objc private var observableRunner: ObservableRunnable & NSObject {
return runner
}
override func viewDidLoad() {
super.viewDidLoad()
observations = [
observe(\.observableRunner.isRunning, options: [.new]) { (_, changed) in
print("1: \(changed.newValue!)")
},
]
}
ネストしたkeyPathを使ってKVOをする場合は簡単です.
KVO可能に見えるように@objc
をつけたpropertyを用意し, そのpropertyに対してKVOをするだけです.
しかし, この方法だと前回の記事のクラッシュが発生する可能性があるので, ネストしないkeyPathで実装する方法に変えていきます.
ネストしないkeyPathでKVOを行う
observableRunner.observe(\.isRunning, options: [.new]) { (_, changed) in
print("2: \(changed.newValue!)")
},
このように書けば上手くいきそうですが, これはコンパイルエラーになります.
Member 'observe' cannot be used on value of protocol type 'NSObject & ObservableRunnable'; use a generic constraint instead
Genericsが解決できないようです.
これを解決するやり方が, stackoverflowにポストされていたので, これをベースに実装すると以下のようになります.
private func observeRunner<T: ObservableRunnable & NSObject, Value>(
_ object: T, keyPath: KeyPath<T, Value>, options: NSKeyValueObservingOptions = [],
changeHandler: @escaping (T, NSKeyValueObservedChange<Value>) -> Void
) -> NSKeyValueObservation {
return object.observe(keyPath, options: options, changeHandler: changeHandler)
}
observeRunner(observableRunner, keyPath: \.isRunning, options: [.new]) { (_, changed) in
print("3: \(changed.newValue!)")
}
Generics的には全く同じことのはずにも関わらず, こちらはコンパイルが通ります.
これでProtocolに対して安全にKVOできそうです.
今回のコードはここに公開しています.
参考文献
記事が気に入ったらチップを送ることができます!
You can give me a cup of coffee :)
Kyash ID: soranoba
Amazon: Wish List
GitHub Sponsor: github.com/sponsors/soranoba
PayPal.Me: paypal.me/soranoba
(Updated: )