swiftreactiveswift-keypath

How can I update this class to support binding a keypath value to a function?


I have a class that allows me to declare a property as Bindable

let user: Bindable<User> = Bindable(someUser)
user.update(with: someNewUser)
......
user.bind(\.name, to: label, \.text)

Doing so allows changes to be bound to a UI element directly.

This is based on an article found here

import Foundation

final class Bindable<Value> {
    private var observations = [(Value) -> Bool]()
    private var lastValue: Value?

    init(_ value: Value? = nil) {
        lastValue = value
    }
}

extension Bindable {
    func update(with value: Value) {
        lastValue = value
        observations = observations.filter { $0(value) }
    }
}

extension Bindable {
    // Non Optionals
    func bind<O: AnyObject, T>(_ sourceKeyPath: KeyPath<Value, T>, to object: O, _ objectKeyPath: ReferenceWritableKeyPath<O, T>) {
        addObservation(for: object) { object, observed in
            let value = observed[keyPath: sourceKeyPath]
            object[keyPath: objectKeyPath] = value
        }
    }
    
    
    
    // Optionals
    func bind<O: AnyObject, T>(_ sourceKeyPath: KeyPath<Value, T>, to object: O, _ objectKeyPath: ReferenceWritableKeyPath<O, T?>) {
        addObservation(for: object) { object, observed in
            let value = observed[keyPath: sourceKeyPath]
            object[keyPath: objectKeyPath] = value
        }
    }
}

private extension Bindable {
    func addObservation<O: AnyObject>(for object: O, handler: @escaping (O, Value) -> Void) {
        // If we already have a value available, give the  handler access to it directly.
        lastValue.map { handler(object, $0) }

        // Each observation closure returns a Bool that indicates
        // whether the observation should still be kept alive,
        // based on whether the observing object is still retained.
        observations.append { [weak object] value in
            guard let object = object else { return false }

            handler(object, value)
            return true
        }
    }
}

What I would like to do is also be able to bind a property to a function.

The current syntax for binding values is something like -

user.bind(\.name, to: label, \.text)

But I'd like to extend this so perhaps that property at that keypath can invoke a method.

someting like -


func doSomethingWithProp(_ prop: String) {
    // do something
}

user.bind(\.name, to: doSomething)

In this case doSomething may invoke a helper for NSAttributedString and accepts the name prop as an argument to be used in that helper.

I have seen something similar in RxSwift using bind(onNext: ....).

I tried to do this using -

    func bind<O: AnyObject, T, P>(_ sourceKeyPatch: KeyPath<Value, T>, to onNext: @escaping (P) -> Void) {
        addObservation(for: onNext) { onNext, observed in
             let value = observed[keyPath: sourceKeyPath]
             onNext(value)
         }
    }

Bit I get the follow errors -

Generic parameter 'O' is not used in function signature

Generic parameter 'O' could not be inferred


Solution

  • This Bindable approach expects there to be some observing object, but you don't have one. That said, it doesn't actually care what that object is. It's just something passed back to the handler. So you could handle this in an extension this way, by using self as a placeholder object:

    func bind<T>(_ sourceKeyPath: KeyPath<Value, T>, onNext: @escaping (T) -> Void) {
        addObservation(for: self) { object, observed in
            let value = observed[keyPath: sourceKeyPath]
            onNext(value)
         }
    }
    

    That said, this feels a little messy, so I might redesign Bindable to support this natively, and build object binding on top of it. Make the private addObservation do a bit less, by just calling a handler:

    private extension Bindable {
        func addObservation(handler: @escaping (Value) -> Bool) { // <== Require a bool here
            lastValue.map { handler($0) }
    
            observations.append { handler($0) }  // <== Just call the handler
        }
    }
    

    And make all the public methods do a bit more checking about object, so the private extension doesn't have to know about it.:

    extension Bindable {
        // Non Optionals
        func bind<O: AnyObject, T>(_ sourceKeyPath: KeyPath<Value, T>, to object: O, _ objectKeyPath: ReferenceWritableKeyPath<O, T>) {
            addObservation { [weak object] observed in
                guard let object = object else { return false }  // <== Have to check object here instead
                let value = observed[keyPath: sourceKeyPath]
                object[keyPath: objectKeyPath] = value
                return true
            }
        }
    
        // Optionals
        func bind<O: AnyObject, T>(_ sourceKeyPath: KeyPath<Value, T>, to object: O, _ objectKeyPath: ReferenceWritableKeyPath<O, T?>) {
            addObservation { [weak object] observed in
                guard let object = object else { return false }
                let value = observed[keyPath: sourceKeyPath]
                object[keyPath: objectKeyPath] = value
                return true
            }
        }
    
        // Function
        func bind<T>(_ sourceKeyPath: KeyPath<Value, T>, onNext: @escaping (T) -> Void) {
            addObservation { observed in
                let value = observed[keyPath: sourceKeyPath]
                onNext(value)
                return true
             }
        }
    }
    

    There's probably some more refactoring you could do here to reduce some of the code duplication, but the key point would be to make the primitive handler do less.

    Note that in iOS 13+, this should be done with Combine instead. It does this all in a more powerful way.