Re-implementation of @Binding and @State (from SwiftUI) myself to better understand it

May 3, 2022 · View on GitHub

/*: This is a concept 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,

⚠️ This is not supposed to be a reference implementation nor cover all subtleties of the real Binding and State types. The only purpose of this playground is to show how re-implementing them myself has helped me understand the whole thing better (especially the Property Wrappers, their projectedValue, the relationship between State and Binding, and the magic behind the @dynamicMemberLookup + @propertyWrapper combination which allows $someState.foo.bar to work magically) */

//: ## A Binding is just something that encapsulates getter+setter to a property

@propertyWrapper struct XBinding { var wrappedValue: 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
}

var projectedValue: Self { self }

}

//: ----------------------------------------------------------------- //: ### 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:

 ````
 private var _x1 = XBinding<Int>(getValue: { x1Storage }, setValue: { x1Storage = \$0 })
 var x1: Int {
   get { _x1.wrappedValue } // which in turn ends up using the getValue closure
   set { _x1.wrappedValue = newValue } // which in turn ends up using the setValue closure
 }
 var $x1: XBinding<Int> {
   get { _x1.projectedValue } // which in our case is just the same as _x1 since a XBinding's projectedValue has been defined to return itself; but at least $x1 is internal, not private like _x1
   set { _x1.projectedValue = newValue }
 }
 ````
 */

func run() {
    print("Before:", "x1Storage =", x1Storage, "x1 =", x1) // Before: x1Storage = 42 x1 = 42
    x1 = 37 // calls `x1.set` which calls `_x1.wrappedValue = 42` which calls `_x1.setValue(42)` (via its `nonmutating set`) which ends up doing `x1Storage = 42` under the hood. Pfew.
    print("After:", "x1Storage =", x1Storage, "x1 =", x1) // After: x1Storage = 37 x1 = 37
    // ok not that useful so far, but now you know the basics of how a Binding works. Now let's see why they can be useful.
}

} 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: CustomStringConvertible { var number: Int var street: String var description: String { "(number), (street)" } }

struct Person { var name: String var address: Address }

var personStorage = Person(name: "Olivier", address: Address(number: 13, street: "Playground Street"))

struct Example2 { @XBinding(getValue: { personStorage }, setValue: { personStorage = $0 }) var person: Person /*: Translated by the compiler to:

   ````
   var _person = XBinding<Person>(getValue: { personStorage }, setValue: { personStorage = \$0 })
   var  person: Person { get { _person.wrappedValue   } set { _person.wrappedValue   = newValue } }
   var $person: Person { get { _person.projectedValue } set { _person.projectedValue = newValue } }
   ````
 */
func run() {
    print(person.name) // "Olivier"
    print(_person.wrappedValue.name) // Basically the same as above, just more verbose
}

} let example2 = Example2() example2.run()

//: Ok, still not so useful so far, be now… 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 inner 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(_ keyPath: WritableKeyPath<Value, NewValue>) -> XBinding { return XBinding( getValue: { self.wrappedValue[keyPath: keyPath] }, setValue: { self.wrappedValue[keyPath: keyPath] = $0 } ) } }

let nameBinding = example2.$person.map(.name) // We now have a binding to the name property inside the Person nameBinding.wrappedValue = "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 too)

//: ----------------------------------------------------------------- //: ## @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(dynamicMember keyPath: WritableKeyPath<Self.Value, Subject>) -> XBinding { return XBinding( getValue: { self.binding.wrappedValue[keyPath: keyPath] }, setValue: { self.binding.wrappedValue[keyPath: keyPath] = $0 } ) } }

//: XBinding is one of those types on which we want that @dynamicMemberLookup feature: extension XBinding: XBindingConvertible { var binding: XBinding { self } // well for something already a Binding, just use itself! }

//: And now e2.$person.name just access the e2.$person: XBinding<Person> first, then use the magic of //: @dynamicMemberLookup when trying to access .name on it (using subscript(dynamicMember: \.name) under the hood) //: to return a new XBinding<String> – which is now representing the access to the .name property of the Person (instead of the Person itself). //: //: That's how it's made possible to have e2.$foo.bar.baz "propagate" the Binding from one parent property to be a new Binding //: to the child properties. $ is not some magic compiler operator interpreting the whole expression as a Binding like I first thought – and maybe you too – //: when I saw the SwiftUI call site code samples at WWDC. No, it's just using @dynamicMemberLookup to make the magic happen instead. print(example2.person) // Person(name: "NewName", address: 13, Playground Street)) print(type(of: example2.person.name))//XBinding<String>letstreetNumBinding=example2.person.name)) // XBinding<String> let streetNumBinding = example2.person.address.number // XBinding streetNumBinding.wrappedValue = 42 print(example2.person) // Person(name: "NewName", address: 42, Playground Street))

//: ----------------------------------------------------------------- //: ## We don't want to declare storage ourselves: introducing @State

//: 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, so let's abstract this and wrap that one level further

//: XState will wrap both the storage for the value, and a XBinding to it @propertyWrapper class XStateV1: XBindingConvertible { var wrappedValue: Value // the storage for the value var binding: XBinding { // the binding to get/set the stored value XBinding(getValue: { self.wrappedValue }, setValue: { self.wrappedValue = $0 }) }

init(wrappedValue value: Value) {
    self.wrappedValue = value
}

var projectedValue: XBinding<Value> { binding }

}

//: > This is a simplistic implementation to show the relationship between State and Binding. //: > In practice there's more to it, especially in SwiftUI there's some more things to notify when the state has changed to redraw the UI that //: > I didn't go into details here. See the comments on that gist to discuss more about it.

//: And now we don't need to declare both the personStorage and the @Binding var person property – we can use @State var person and have it all at once. struct Example3 { @XStateV1 var person = Person(name: "Bob", address: Address(number: 21, street: "Builder Street")) /*: This is translated by the compiler to:

   ````
   var _person: XStateV1(wrappedValue: Person(name: "Bob", address: Address(number: 21, street: "Builder Street")))
   var  person: Person   { get { _person.wrappedValue   } set { _person.wrappedValue   = newValue } }
   var $person: XBinding { get { _person.projectedValue } set { _person.projectedValue = newValue } }
   ````
 > Note that since `projectedValue` of `XStateV1` exposes an `XBinding`, `$person` will be a `XBinding` (and not an `XState`) here.
*/

func run() {
    print(person.name) // Person(name: "Bob", address: __lldb_expr_17.Address(number: 21, street: "Builder Street"))

    let streetBinding: XBinding<String> = $person.address.street
    person = Person(name: "Crusty", address: Address(number: 1, street: "WWDC Stage"))
    streetBinding.wrappedValue = "Memory Lane"
    print(person) // Person(name: "Crusty", address: __lldb_expr_17.Address(number: 1, street: "Memory Lane"))
}

} Example3().run()

/*: It's important to note that $foo does not just always return a binding to foo in all cases – this $ is not a magic token that turns a property into a binding as some might have thought at first.

Instead, $foo is to access the projectedValue of the PropertyWrapper attached to foo. True, it so happens that:

  • the projectedValue of XBinding is indeed an XBinding (it returns self)
  • the projectedValue of XState is also an XBinding (built on the fly to return a binding to the wrappedValue)

But this is just a coincidence of those two types both returning XBindings for their projectedValue, given the way that we decided to implement projectedValue on XBinding and XState.

For other Property Wrappers, the projectedValue might be of another type and $ would mean something else depending on the wrapper (e.g. the projectedValue exposed by a @Published in Combine is a Publisher, not a Binding) */

//: ----------------------------------------------------------------- //: # The End //: …or almost. //: //: > Continue reading if you want more info about some advanced questions which came later in my journey or via Gist comments below. //: -----------------------------------------------------------------

//: ----------------------------------------------------------------- //: ## How XState breaks if you happen to have a type with a property coincidentally named wrappedValue (very unlikely though)

/*: There's a tricky edge case which can happen if you use @XState var model: SomeModel butSomeModel has a property coincidentally named wrappedValue In that case, $model.wrappedValue will not give you a new binding to that wrappedValue like you might expect, but return the object the binding is pointing to instead.

This is because XBinding itself also have a real wrappedValue property (so that it can be declared as @propertyWrapper). Which means that even if $model returns an XBinding as you expect, since XBinding has a proper wrappedValue property itself, then $model.wrappedValue will return the value of that real wrappedValue property, and won't go thru the subscript(dynamicMember:)/@dynamicMemberLookup route.

This is not really an issue since wrappedValue should be rarely used as a name for properties in your regular types in practice. But this caused issues with early implentations of Property Wrappers (called propertyDelegates back then) – as the magic property name required to make a type a @propertyDelegate was named value back then before they renamed those to @propertyWrapper and wrappedValue. Since value was a way more common property name in other types like SomeModel, that was more likely to cause hidden bugs. But thankfully, they renamed this before the last revision, so the special case should be way less likely now.

I'm still keeping this contrieved example around since that's one step I had to go thru when understanding how the @propertyWrapper + State + @dynamicMemberLookup magic came together back when I initially went thru those discovery path */

struct Expression { var wrappedValue: Int var nonSpecialProp: Int }

struct Example4 { @XStateV1 var expr = Expression(wrappedValue: 42, nonSpecialProp: 1337)

func run() {
    let bindingToExprValue2 = $expr.nonSpecialProp
    type(of: bindingToExprValue2) // XBinding<Int>
    let notABindingToExprValue = $expr.wrappedValue
    type(of: notABindingToExprValue) // Expression
    let bindingToExprValue = $expr[dynamicMember: \.wrappedValue]
    type(of: bindingToExprValue) // XBinding<Int>
}

} Example4().run()

//: ----------------------------------------------------------------- //: ## nonmutating set //: Ok, but in Apple's API, State is a struct with a nonmutating setter. How did they achieve that then? //: Well, just with one additional level of indirection, wrapping the class into a struct allows that trick:

@propertyWrapper struct XState: XBindingConvertible { class Storage { var value: Value init(initialValue: Value) { self.value = initialValue } } private var storage: Storage

var wrappedValue: Value {
    get { self.storage.value }
    nonmutating set { self.storage.value = newValue }
}
var binding: XBinding<Value> {
    XBinding(getValue: { self.wrappedValue }, setValue: { self.wrappedValue = \$0 })
}

init(wrappedValue value: Value) {
    self.storage = Storage(initialValue: value)
}

var projectedValue: XBinding<Value> { binding }

}

//: And now we can use the same example as before, except @XState is now backed by a struct

struct Example5 { @XState var expr = Expression(wrappedValue: 42, nonSpecialProp: 1337)

func run() {
    let bindingToExprValue2 = $expr.nonSpecialProp
    type(of: bindingToExprValue2) // XBinding<Int>
    let notABindingToExprValue = $expr.wrappedValue
    type(of: notABindingToExprValue) // Expression
    let bindingToExprValue = $expr[dynamicMember: \.wrappedValue]
    type(of: bindingToExprValue) // XBinding<Int>
}

} Example5().run()