Skip to content

Instantly share code, notes, and snippets.

@andrekandore
Forked from desugaring/keypaths.playground
Created June 9, 2018 00:12
Show Gist options
  • Save andrekandore/29dcbde3e14728aca2e3cba05ddcb081 to your computer and use it in GitHub Desktop.
Save andrekandore/29dcbde3e14728aca2e3cba05ddcb081 to your computer and use it in GitHub Desktop.
Two-way binding in iOS using KVO
//: 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