Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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()
@finestructure

This comment has been minimized.

Copy link

finestructure commented Jun 8, 2019

That's super instructive.

It seems to me that bindings are like type-safe and in a sense structured pointers to storage, right? By structured I mean that the reference created via $ can be further drilled into via key paths.

And a state is a variable that comes prepared for vending such $ references, or makes them available.

Am I reading this right?

@AliSoftware

This comment has been minimized.

Copy link
Owner Author

AliSoftware commented Jun 8, 2019

Yep I think that's an apt description indeed!

@guseducampos

This comment has been minimized.

Copy link

guseducampos commented Jun 10, 2019

This is amazing, thanks!
just make me wonders how actually works in swiftUI, in your example State is a reference type but in swiftUI State is a value type (https://developer.apple.com/documentation/swiftui/state) how they keep tracking the changes when is passed to the get/set closure? since a copy of self is made it inside of the both closures

@AliSoftware

This comment has been minimized.

Copy link
Owner Author

AliSoftware commented Jun 10, 2019

@guseducampos yeah that's something I wondered about at first, trying to match their API and failing because of that nonmutating set.

But after a bit of thought, this is easily achievable by using one more level of indirection. I managed to achieve this in the end, but didn't update the gist because I thought it would add one level of complexity in the understanding and though process and wanted to keep the playground as a story of how I slowly realised what were each property wrapper for and why we needed them.

If you want to implement State as a value type like they did, just use an intermediate class Storage to hold the value, and make struct State use that class as storage instead of storing the value. That way you don't mutate the pointer to the class, but the class itself being mutable, you can change the inner value 😉

@AliSoftware

This comment has been minimized.

Copy link
Owner Author

AliSoftware commented Jun 10, 2019

This can look something like this:

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

    var value: Value {
        get { self.storage.value }
        nonmutating set { self.storage.value = newValue }
    }
    var binding: XBinding<Value> { delegateValue }

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

    var delegateValue: XBinding<Value> {
        XBinding(getValue: { self.value }, setValue: { self.value = $0 })
    }
}
@JJJensen

This comment has been minimized.

Copy link

JJJensen commented Jun 12, 2019

I would assume the internal state conforms to BindableObject, and uses didChange to actually invalidate the view hierarchy. But how it knows which part of the view hierarchy to invalidate (unless it just sends an "invalidate all views in all windows" notification) I do not know :)

@guseducampos

This comment has been minimized.

Copy link

guseducampos commented Jun 13, 2019

@AliSoftware Thanks for the explanation and the example! just wondering whether exist some performance issue backing a reference type inside of a struct and moving around without doing something like COW?

@JJJensen According to this thread on swift forums, looks like they are using some private framework written in C++ that collects swift runtime metadata and also is used to make runtime calls, I believe they are using that framework to keep tracking the changes in the hierarchy and then just invalidate the part of the hierarchy that changed instead of re-compute everything again.

@pfandrade

This comment has been minimized.

Copy link

pfandrade commented Aug 14, 2019

Great stuff. One question though, this doesn’t actually do anything related to notifying the bound object that the value changed right?

@AliSoftware

This comment has been minimized.

Copy link
Owner Author

AliSoftware commented Aug 14, 2019

@pfandrade no it doesn't. At the time of writing this gist the goal was mainly to understand what they were in concept and their difference in implementation behind the scenes, also because property delegates wrapper were quite new at the time.

But now that the Property Wrappers proposal has been finalised and merged, and that we understand SwiftUI better, that implementation is just an overview and is even outdated (delegateValue vs projectedValue etc) since some stuff has changed since 😉

I wouldn't rely on it nowadays except as a high-level of understanding the difference between Binding and State if you're the kind of person like me to which some code helps better than long explanations in docs 😉

@pfandrade

This comment has been minimized.

Copy link

pfandrade commented Aug 14, 2019

Thanks, I do think a piece of code is worth more than 1000 words. And I'm already playing with observables in a playground. Thanks again, this helped me get started faster.

@AliSoftware

This comment has been minimized.

Copy link
Owner Author

AliSoftware commented Oct 16, 2019

For those interested, I've finally updated this old gist to the new namings used by SE-258 (PropertyWrappers) now that the proposal is adopted and merged in Swift 5.

(initial gist was written during the Xcode 11 beta days when SE-258 was not finalized and @propertyWrapper/wrappedValue were still named @propertyDelegate/value until they updated it and froze the new names for Swift 5)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.