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 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()
@finestructure

This comment has been minimized.

Copy link

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

commented Jun 8, 2019

Yep I think that's an apt description indeed!

@guseducampos

This comment has been minimized.

Copy link

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

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

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

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

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.

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.