Skip to content

Instantly share code, notes, and snippets.

@amine2233
Forked from AliSoftware/Bindings.swift
Created October 22, 2020 13:24
Show Gist options
  • Save amine2233/d5a23a949144ee69e282cbfcfc00a457 to your computer and use it in GitHub Desktop.
Save amine2233/d5a23a949144ee69e282cbfcfc00a457 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 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<Value> {
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<NewValue>(_ keyPath: WritableKeyPath<Value, NewValue>) -> XBinding<NewValue> {
return XBinding<NewValue>(
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<Subject>(dynamicMember keyPath: WritableKeyPath<Self.Value, Subject>) -> XBinding<Subject> {
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<Value> { 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>
let streetNumBinding = example2.$person.address.number // XBinding<Int>
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<Value>: XBindingConvertible {
var wrappedValue: Value // the storage for the value
var binding: XBinding<Value> {
// 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: XState(getValue: { self.storage }, setValue: { self.storage = $0 })
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 `XState` exposes an `XBinding`, `$person` will be a `XBinding` (and not an `XState`) here.
*/
func run() {
print(person.name)
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(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 PropertyWrappers, 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` but`SomeModel` 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<Value>: 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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment