Skip to content

Instantly share code, notes, and snippets.

@MihaelIsaev
Forked from AliSoftware/Bindings.swift
Created June 14, 2019 02:51
Show Gist options
  • Save MihaelIsaev/b921599c8ed6fcb58c85e1a059347004 to your computer and use it in GitHub Desktop.
Save MihaelIsaev/b921599c8ed6fcb58c85e1a059347004 to your computer and use it in GitHub Desktop.
Re-implementation of @binding and @State (from SwiftUI) myself to better understand it
// This is a re-implementation of the @Binding and @State property wrappers from SwiftUI
// The only purpose of this code is to implement those wrappers myself just to understand how they work internally and why they are needed
// Re-implementing them myself has helped me understand the whole thing better
//: # A Binding is just something that encapsulates getter+setter to a property
@propertyDelegate
struct XBinding<Value> {
var value: Value {
get { return getValue() }
nonmutating set { setValue(newValue) }
}
private let getValue: () -> Value
private let setValue: (Value) -> Void
init(getValue: @escaping () -> Value, setValue: @escaping (Value) -> Void) {
self.getValue = getValue
self.setValue = setValue
}
}
//: -----------------------------------------------------------------
//: ## Simple Int example
// We need a storage to reference first
private var x1Storage: Int = 42
// (Note: Creating a struct because top-level property wrappers don't work well at global scope in a playground
// – globals being lazy and all)
struct Example1 {
@XBinding(getValue: { x1Storage }, setValue: { x1Storage = $0 })
var x1: Int
/* The propertyWrapper translates this to
var $x1 = XBinding<Int>(getValue: { x1Storage }, setValue: { x1Storage = $0 })
var x1: Int {
get { return _x1.value } // which in turn ends up using the getValue closure
set { _x1.value = newValue } // which in turn ends up using the setValue closure
}
*/
func run() {
print("Before:", "x1Storage =", x1Storage, "x1 =", x1) // Before: x1Storage = 42 x1 = 42
x1 = 37
print("After:", "x1Storage =", x1Storage, "x1 =", x1) // After: x1Storage = 37 x1 = 37
}
}
Example1().run()
// This works, but as you can see, we had to create the storage ourself in order to then create a @Binding
// Which is not ideal, since we have to create some property in one place (x1Storage),
// then create a binding to that property separately to reference and manipulate it via the Binding
// We'll see later how we can solve that.
//: -----------------------------------------------------------------
//: ## Manipulating compound types
// In the meantime, let's play a little with Bindings. Let's create a Binding on a more complex type:
struct Address {
var street: String
}
struct Person {
var name: String
var address: Address
}
var personStorage = Person(name: "Olivier", address: Address(street: "Playground Street"))
struct Example2 {
@XBinding(getValue: { personStorage }, setValue: { personStorage = $0 })
var person: Person
/* Translated to: */
// var $person = XBinding<Person>(getValue: { personStorage }, setValue: { personStorage = $0 })
// var person: Person { get { $person.value } set { $person.value = newValue } }
func run() {
print(person.name) // "Olivier"
print($person.value.name) // Basically the same as above, just more verbose
}
}
let example2 = Example2()
example2.run()
// Ok, that's not so useful so far, be what if we could now `map` to inner properties of the Person
// i.e. what if I now want to transform the `Binding<Person>` to a `Binding<String>` now pointing to the `name` property?
//: -----------------------------------------------------------------
//: # Transform Bindings
// Usually in monad-land, we could declare a `map` method on XBinding for that
// Except that here we need to be able to both get the name from the person... and be able to set it too
// So instead of using a `transform` like classic `map`, we're gonna use a WritableKeyPath to be able to go both directions
extension XBinding {
func map<NewValue>(_ keyPath: WritableKeyPath<Value, NewValue>) -> XBinding<NewValue> {
return XBinding<NewValue>(
getValue: { self.value[keyPath: keyPath] },
setValue: { self.value[keyPath: keyPath] = $0 }
)
}
}
let nameBinding = example2.$person.map(\.name) // We now have a binding to the name property inside the Person
nameBinding.value = "NewName"
print(personStorage.name) // "NewName"
// But why stop there? Instead of having to call `$person.map(\.name)`, wouldn't it be better to call $person.name directly?
// Let's do that using @dynamicMemberLookup. (We'll add that via protocol conformance so we can reuse this feature easily on other types later)
//: -----------------------------------------------------------------
//: # dynamicMemberLoopup
//: Add dynamic member lookup capability (via protocol conformance) to forward any access to a property to the inner value
@dynamicMemberLookup protocol XBindingConvertible {
associatedtype Value
var binding: XBinding<Self.Value> { get }
subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Self.Value, Subject>) -> XBinding<Subject> { get }
}
extension XBindingConvertible {
public subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Self.Value, Subject>) -> XBinding<Subject> {
return XBinding(
getValue: { self.binding.value[keyPath: keyPath] },
setValue: { self.binding.value[keyPath: keyPath] = $0 }
)
}
}
// XBinding is one of those types on which we want that dynamicMemberLookup feature:
extension XBinding: XBindingConvertible {
var binding: XBinding<Value> {
return self
}
}
// And now e2.$person.name transforms the `e2.$person: XBinding<Person>` into a `XBinding<String>`
// which is now bound to the `.name` property of the Person
print(type(of: example2.$person.name)) // XBinding<String>
let streetBinding: XBinding<String> = example2.$person.address.street
streetBinding.value = "Xcode Avenue"
print(example2.person) // Person(name: "NewName", address: __lldb_expr_17.Address(street: "Xcode Avenue"))
//: -----------------------------------------------------------------
//: # We don't want to declare storage ourselves
//: Ok this is all good and well, but remember our issue from the beginning? We still need to declare the storage for the value ourselves
//: Currently we had to declare personStorage and had to explicitly say how to get/set that storage when defining our XBinding
//: That's no fun, let's wrap that one level further
// XState will wrap both the storage for the value, and a Binding to it
@propertyDelegate class XState<Value>: XBindingConvertible {
var value: Value
var binding: XBinding<Value> { delegateValue }
init(initialValue value: Value) {
self.value = value
}
var delegateValue: XBinding<Value> {
XBinding(getValue: { self.value }, setValue: { self.value = $0 })
}
}
// And now we don't need to declare both the personStorage and the @Binding person property, we can use @State person and have it all
struct Example3 {
@XState var person = Person(name: "Bob", address: Address(street: "Builder Street"))
// Note that since `delegateValue` (renamed wrapperValue in the SE proposal) expses an XBinding, $person will be a XBinding, not an XState here
// So this is translated to:
// var $person: XBinding(getValue: { self.storage }, setValue: { self.storage = $0 })
// var person: Person { get { $person.value } set { $person.value = newValue } }
func run() {
print(person.name)
let streetBinding: XBinding<String> = $person.address.street
person = Person(name: "Crusty", address: Address(street: "WWDC Stage"))
streetBinding.value = "Memory Lane"
print(person) // Person(name: "Crusty", address: __lldb_expr_17.Address(street: "Memory Lane"))
}
}
Example3().run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment