-
-
Save MihaelIsaev/b921599c8ed6fcb58c85e1a059347004 to your computer and use it in GitHub Desktop.
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
// 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