togo

= ひとりごと to go

zumuya の人による机の上系情報サイト

Swift の KeyPath を使った Binding

  • Apple
  • 開発

Cocoa Bindings は Objective-C 時代にできたものだから String でプロパティのキーパスを指定するけど、 Swift 4 の KeyPath と Generics を組み合わせればすっきりしたコードで書けるのではないかと思いついたので実験。

protocol KeyPathBasedBinding {}
extension KeyPathBasedBinding
{
    /// When the returned object is deinited or invalidated, it will stop binding.
    /// - Parameter targetKeyPath: The KeyPath for target property. Target property must be observable through KVO.
    func bind<Value, Target>(_ keyPath: ReferenceWritableKeyPath<Self, Value>, to targetKeyPath: KeyPath<Target, Value>, of target: Target) -> NSKeyValueObservation where Self: AnyObject, Target: _KeyValueCodingAndObserving
    {
        return target.observe(targetKeyPath, options: [.initial, .new], changeHandler: { [weak self] (sender, change) in
            self?[keyPath: keyPath] = target[keyPath: targetKeyPath]
        })
    }

    /// When the returned object is deinited or invalidated, it will stop binding.
    /// - Parameter targetKeyPath: The KeyPath for target property. Target property must be observable through KVO.
    func bind<Value, Target>(_ keyPath: ReferenceWritableKeyPath<Self, Value>, to targetKeyPath: KeyPath<Target, Optional<Value>>, of target: Target, nullValue: Value) -> NSKeyValueObservation where Self: AnyObject, Target: _KeyValueCodingAndObserving
    {
        return target.observe(targetKeyPath, options: [.initial, .new], changeHandler: { [weak self] (sender, change) in
            self?[keyPath: keyPath] = (target[keyPath: targetKeyPath] ?? nullValue)
        })
    }
}
extension NSObject: KeyPathBasedBinding {}

Cocoa Bindings だと bind(_,to:,withKeyPath:,options) の第一引数には自身のプロパティのキーもしくは特別に用意された NSBindingName の定数を使用することができるけど、今回再現しているのは前者だけだ。

使用例

class Nya: NSObject
{
    @objc dynamic var name: String = ""
}
func test1()
{
    let button = NSButton()
    button.title = "Apple"
    assert(button.title == "Apple")

    let nya = Nya()
    nya.name = "Orange"

    let binding = button.bind(\NSButton.title, to: \Nya.name, of: nya)
    defer { binding.invalidate() }
    assert(button.title == "Orange")

    nya.name = "Banana"
    assert(button.title == "Banana")
}
func test2()
{
    let workspace = NSWorkspace.shared
    let nya = Nya()

    let binding = nya.bind(\Nya.name, to: \NSWorkspace.frontmostApplication?.localizedName, of: workspace, nullValue: "null")
    defer { binding.invalidate() }
    print(nya.name)
}

test1()
test2()

KeyPath ベースになった observe(...) と同様、必要なくなるまでは返り値を保持するのをお忘れなく。

Share

リンクも共有もお気軽に。記事を書くモチベーションの向上に役立てます。