Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // 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()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement