-
-
Save andrekandore/29dcbde3e14728aca2e3cba05ddcb081 to your computer and use it in GitHub Desktop.
Two-way binding in iOS using KVO
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: Playground - noun: a place where people can play | |
import UIKit | |
/* Scroll to the bottom for examples */ | |
typealias WritableObjectKeyPath<O: NSObject, V: Equatable> = (object: O, keyPath: WritableKeyPath<O, V>) | |
typealias ReadOnlyObjectKeyPath<O: NSObject, V: Equatable> = (object: O, keyPath: KeyPath<O, V>) | |
func bind<O: NSObject, O2: NSObject, V: Equatable>(source: ReadOnlyObjectKeyPath<O, V>, to target: WritableObjectKeyPath<O2, V>) -> NSKeyValueObservation? | |
{ | |
let obs = source.object.observe(source.keyPath) { | |
[weak targetObject = target.object] (sourceObject: O, change) in | |
guard var targetObject = targetObject else { return } | |
let sVal = sourceObject[keyPath: source.keyPath] | |
let tVal = targetObject[keyPath: target.keyPath] | |
if sVal != tVal { | |
print("changing from \(tVal) to \(sVal)") | |
targetObject[keyPath: target.keyPath] = sVal | |
} | |
} | |
return obs | |
} | |
typealias ValueTransformer<Input, Output> = (Input) -> Output | |
typealias WritableObjectKeyPathTransformer<O: NSObject, V: Equatable, V2: Equatable> = (object: O, keyPath: WritableKeyPath<O, V2>, transformer: ValueTransformer<V, V2>) | |
func bindWithTransformer<O: NSObject, O2: NSObject, V: Equatable, V2:Equatable>(source: ReadOnlyObjectKeyPath<O, V>, to target: WritableObjectKeyPathTransformer<O2, V, V2>) -> NSKeyValueObservation? | |
{ | |
let obs = source.object.observe(source.keyPath) { | |
[weak targetObject = target.object] (sourceObject: O, change) in | |
guard var targetObject = targetObject else { return } | |
let tVal = targetObject[keyPath: target.keyPath] | |
let sVal = sourceObject[keyPath: source.keyPath] | |
if target.transformer(sVal) != tVal { | |
print("changing from \(tVal) to \(target.transformer(sVal))") | |
targetObject[keyPath: target.keyPath] = target.transformer(sVal) | |
} | |
} | |
return obs | |
} | |
infix operator >> | |
infix operator << | |
infix operator <> | |
func >><O: NSObject, O2: NSObject, V: Equatable> (lhs: ReadOnlyObjectKeyPath<O, V>, rhs: WritableObjectKeyPath<O2, V>) -> NSKeyValueObservation? | |
{ | |
return bind(source: lhs, to: rhs) | |
} | |
func <<<O: NSObject, O2: NSObject, V: Equatable> (lhs: WritableObjectKeyPath<O, V>, rhs: ReadOnlyObjectKeyPath<O2, V>) -> NSKeyValueObservation? | |
{ | |
return bind(source: rhs, to: lhs) | |
} | |
func <><O: NSObject, O2: NSObject, V: Equatable> (lhs: WritableObjectKeyPath<O, V>, rhs: WritableObjectKeyPath<O2, V>) -> [NSKeyValueObservation]? | |
{ | |
let readOnlylhs = (lhs.object, lhs.keyPath as KeyPath<O, V>) | |
let readOnlyrhs = (rhs.object, rhs.keyPath as KeyPath<O2, V>) | |
guard let b1 = bind(source: readOnlyrhs, to: lhs), | |
let b2 = bind(source: readOnlylhs, to: rhs) else { return nil } | |
return [b1, b2] | |
} | |
infix operator >-> | |
infix operator <-< | |
infix operator <-> | |
func >-><O: NSObject, O2: NSObject, V: Equatable, V2: Equatable> (lhs: ReadOnlyObjectKeyPath<O, V>, rhs: WritableObjectKeyPathTransformer<O2, V, V2>) -> NSKeyValueObservation? | |
{ | |
return bindWithTransformer(source: lhs, to: rhs) | |
} | |
func <-<<O: NSObject, O2: NSObject, V: Equatable, V2: Equatable>(lhs: WritableObjectKeyPathTransformer<O2, V, V2>, rhs: ReadOnlyObjectKeyPath<O, V>) -> NSKeyValueObservation? | |
{ | |
return bindWithTransformer(source: rhs, to: lhs) | |
} | |
func <-><O: NSObject, O2: NSObject, V: Equatable, V2: Equatable> (lhs: WritableObjectKeyPathTransformer<O, V2, V>, rhs: WritableObjectKeyPathTransformer<O2, V, V2>) -> [NSKeyValueObservation]? | |
{ | |
let readOnlylhs = (lhs.object, lhs.keyPath as KeyPath<O, V>) | |
let readOnlyrhs = (rhs.object, rhs.keyPath as KeyPath<O2, V2>) | |
guard let b1 = bindWithTransformer(source: readOnlyrhs, to: lhs), | |
let b2 = bindWithTransformer(source: readOnlylhs, to: rhs) else { return nil } | |
return [b1, b2] | |
} | |
/* -------------- Examples ------------- */ | |
@objcMembers class Cake: NSObject { | |
dynamic var name: String | |
dynamic var random: Int = 42 | |
init(name: String) { | |
self.name = name | |
} | |
} | |
@objcMembers class CakeView: NSObject { | |
dynamic var nameTextField: UITextField | |
dynamic var isVanilla: Bool | |
override init() { | |
self.nameTextField = UITextField() | |
self.nameTextField.text = "" | |
self.isVanilla = false | |
} | |
} | |
let cake1 = Cake(name: "chocolate") | |
let cakeView1 = CakeView() | |
let binding1 = (cake1, \Cake.name) >> (cakeView1, \CakeView.nameTextField.text!) | |
let binding2 = (cake1, \Cake.name) >-> (cakeView1, \CakeView.isVanilla, { $0 == "vanilla" }) | |
print("Changing cake to vanilla") | |
cake1.name = "vanilla" | |
print("cakeView's textField now says: \(cakeView1.nameTextField.text!)") | |
print("cakeView's isVanilla is now now: \(cakeView1.isVanilla)") | |
print("Changing cake to strawberry") | |
cake1.name = "strawberry" | |
print("cakeView's textField now says: \(cakeView1.nameTextField.text!)") | |
print("cakeView's isVanilla is now now: \(cakeView1.isVanilla)") | |
/* For fun, you can try | |
- changing the binding1 from >> to << or <-> and changing cakeView to see what happens | |
- changing vars to let to see what happens | |
- changing \Cake.name to \Cake.random to see what happens | |
How does this all work? | |
- Arrow operators are syntactic sugar for calling the bind method, which is in turn | |
syntactic sugar for calling observe. | |
- Observe applies the change to the bound object if it doesn't match the observer | |
Why is this nice? | |
- the repetitive boilerplate is abstracted away | |
- clean syntax | |
- compile time guarantees to make sure the types match, the variable are mutable | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment