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
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.