-
-
Save AliSoftware/ecb5dfeaa7884fc0ce96178dfdd326f8 to your computer and use it in GitHub Desktop.
/*: | |
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: 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` 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() | |
Yep I think that's an apt description indeed!
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
@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 😉
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 })
}
}
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 :)
@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.
Great stuff. One question though, this doesn’t actually do anything related to notifying the bound object that the value changed right?
@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 😉
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.
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)
Thanks for this - nice to get some insight into Bindings.
This might be a bit cheeky to ask here....
Is anyone able to answer my question on Stackoverflow:
[https://stackoverflow.com/questions/58421913/how-can-i-cast-an-binding-in-swift]
Hi. Do you have an idea of how SwiftUI handles passing Binding<Value>
from parent to a child view? I mean here:
struct Hello: View {
@Binding var text: String
var body: some View {
Text(text)
}
}
struct ContentView: View {
@State var text: String = "Hello!"
var body: some View {
Hello(text: $text)
}
}
$text
in parent view is of type Binding<String>
but in the child view, property text
is of type String
(and wrappedValue
of the binding should also be of type String
), how they pulled this off? I could not replicate such behavior...
If I'm understanding your question correctly:
- In
ContentView
, your_text
(the Property Wrapper) is of typeState<String>
, astext
is declared as@State var text: String = …
- The
State
property wrapper has aprojectedValue
of typeBinding
. That's what makes$text
(which access the projected value of the wrapper, not the wrapper itself) be aBinding<String>
- So when you pass
Hello(text: $text)
that passes$text
– which is theState<String>
's.projectedValue
which is aBinding<String>
- to the constructor ofHello
. So it uses theHello.init(_ _text: Binding<String>)
constructor and passes to it the binding derived from theContentView
's$text
(aka_text.projectedValue
)
So long story short, this is made possible by the fact that a State
's projectedValue
returns a Binding
to that storage. See how I've implemented it here above. This is what makes Hello(text: $text)
pass the binding to that @State var text
property to the Hello
instance.
HTH
So it uses the
Hello.init(_ _text: Binding<String>)
...
This is the part I was missing, thank you! From what I understand I cannot call such constructor manually from my code, does this mean that SwiftUI uses some API which is not available to other developers?
I haven't tried it in practice, but since you have a struct Hello: View
with no init
declared explicitly, it should synthesise a member-wise init
for you, as Swift does for every struct
(not something specific to SwiftUI there), and since the only member of that struct is text
and is a Binding
, I'm guessing such an init
taking a Binding<String>
as parameter is synthesised by Swift here too
In any case, in Hello(text: $text)
, the $text
– being the projectedValue
of the State
text property – is a Binding
, so this expression calls the Hello.init(text: Binding<String>)
method. For this to work, such an init(text: Binding<String>)
has to exist on Hello
and since you didn't explicitly write it, it's likely synthesised by the compiler, which makes sense since Swift already does that for any other struct
.
Yes, you're correct, just tried to mimic this with custom property wrapper and all works just as you described. Did not work previously because of my mistake.
Hello, one other things @State in swift ui does is that it keeps the same values across multiple instantiations of the same View. What piece of code from your gist helps you achieve this behavior?
Good point. I'm not entirely sure this part is possible to reproduce with manual code though. It might be a part of SwiftUI where some magic is handled by the compiler and the runtime for us 🤔
(tbh I've written this gist a while ago when SwiftUI and property wrappers were brand new; my goal was to understand what those new State/Binding beasts were — as they were completely new concepts back then — but never intended to try and recreate the full exact implementation. So there's probably plenty more details and other behaviors of Binding/State missing on that gist compared to the real deal 😅)
Thanks for the writeup, it really helped me understand Binding and State.
I know my comment comes a little late, but one thing I still don't understand is how a struct with a binding property can be initialised with a binding, although that same property, when accessed within the struct, is still considered its underlying type, i.e.
struct MasterView: View {
@State private var message: String = ""
var body: some View {
// Here I pass in message as a binding, which works.
DetailView(message: $message)
}
}
struct DetailView: View {
@Binding var message: String
// But if I create a custom initializer and do the same, I get a compile error
init(message: Binding<String>) {
// Compile error: Cannot assign value of type 'Binding<String>' to type 'String'
self.message = message
}
var body: some View {
TextField("Message", text: $message)
}
}
@mikkelse This is not specific to Bindings and States, but to how Property Wrappers work in general.
Here by annotating your instance variable message
with any property wrapper (like @State
or @Binding
in your specific example), the compiler will actually generate:
- A private
_message
instance variable which will point to the Property Wrapper itself (i.e. the instance ofBinding
orState
here) - A
message
computed property which will return thewrappedValue
exposed by that property wrapper (i.e. compiler will basically generatevar message: String { get { _message.wrappedValue } set { _message.wrappedValue = newValue } }
). - A
$message
computed variable, which will return to theprojectedValue
exposed by the Property Wrapper.- In many cases when you implement your own
@propertyWrapper
type and provide aprojectedValue
for it, you implement it to just returnself
, aka the Property Wrapper itself. In such cases, when you then annotate afoo
instance with that property wrapper, then$foo
and_foo
ends up being the same (except that_foo
is private only accessible from within the enclosing type wherefoo
is also declared, while$foo
would have the same accessibility level as thefoo
instance variable itself), but that's because for that specific PW, the projectedValue of the PW returns the PW itself. - But in some cases the
projectedValue
of a@propertyWrapper
is implemented to return something else. That's the case for the@propertyWrapper struct State
, which returns thebinding
as its projected value. In that case,_message
is still the instance ofState
, but$message
is an instance ofBinding
exposed by thatState
- In many cases when you implement your own
Here in your case:
- For
DetailView(message: $message)
,$message
is the projectedValue of the_message: State
property wrapper, and aState
'sprojectedValue
is implemented in such a way that it projects aBinding
. So$message
is the projectedValue of your State, and as such is of typeBinding
, so that works with thatDetailView(message:)
expects - But for
self.message = message
in yourDetailView.init
implementation, yourmessage: Binding<String>
is (rightfully) of typeBinding
, but the left-hand sideself.message
of your expression accesses the compiler-generatedmessage: String
computed property, the one that exposes theBinding
(from the@Binding var message: String
) declaration)'swrappedValue
, so theString
itself.- That's what makes Property Wrappers nice and kind of magic, because the whole point of using
@ThePropertyWrapper
annotations (i.e.@Binding var message: String
) instead of declaring your variable being directly of the type of the wrapper (i.e.var message: Binding<String>
) is that it makes it transparent when you callself.message
, and makesself.message
directly return aString
which makes it niced to manipulate the wrapped value in a more natural way — as opposed to having to useself.message.wrappedvalue
if you had declared itvar message: Binding<String>
- The way you declared that instance variable —
@Binding var message: String
— on yourDetailView
is still the proper and expected way to declare Binding instance vars in SwiftUIViews
, so that part is totally correct - But then when you want to initialize all the stored variables of your
DetailView
in yourinit
, what you actually need to initialize is not theself.message
(which is a compiler-generated computed property, remember, even if that is meant to be as transparent as possible by the compiler when you use property wrappers and makes us tend to forget it is), but theself._message
instead — which is the actual, private instance variable that the compiler would generate with theBinding<String>
type
- That's what makes Property Wrappers nice and kind of magic, because the whole point of using
In other words, when you write @Binding var message: String
to declare your instance variable in DetailView
, the compiler will actually generate the following code:
struct DetailView: VIew {
// The underlying storage for the Binding property wrapper instance itself
private var _message: Binding<String>
// Convenience computed property to easily access the propertyWrapper's `wrappedValue` transparently in your code
var message: String {
get { _message.wrappedValue }
set { _message.wrappedValue = newValue }
}
// Convenience computed property to easiliy access the propertyWrapper's `projectedValue` transparently in your code
// Note that this is pseudo-code as you wouldn't be allowed to declare an instance variable starting with `$` manually yourself (but the compiler allows itself to do it for code it generates for a propertyWrapper)
// Also note that for the `@propertyWrapper struct Binding`, it happens that the type of `projectedValue` (denoted here by `Binding.ProjectedValue` type) is itself also `Binding` (and a `Binding`'s `projectedValue` value returns `self`)
// But that's not always the case (e.g. `State.ProjectedValue` is `Binding`), hence why I've made this more generic to make it clear what this relies on
var $message: Binding.ProjectedValue<String> {
get { _message.projectedValue }
set { _message.projectedValue = newValue }
}
…
Which is why DetailView
's init
needs to initialize _message
(its only non-computed instance variable), not message
(which is a computed one, which you wouldn't be able to access at that point anyway given _message
would not yet be fully initialized at that point in the code, so neither would self._message.wrappedValue
(which self.message
is merly kind of an "alias" for)
Hope that clarifies things!
If you want to learn more about Property Wrappers in general, I suggest you watch my various talk I gave about the concept in a past FrenchKit conference here 🙂
Hey man, thank you so much for putting in the effort to spell it out for me, truly appreciate it. I was with you all of the way, but actually your last paragraph is what brings me almost home, namely that _message
of cause is the only non-computed variable that init needs to initialise. So I guess a memberwise initialiser is created that accepts a Binding<String>
to initialise _message, but, still _message is private and under normal circumstances such an initialiser would not be possible. Seems like there's still some compiler synthesization magic here?
For example, a struct like this, does not result in a valid memberwise initialiser due to _message being inaccessible due to private protection level.
struct DetailView {
private var _message: Binding<String>
}
I will check out your talks, thanks!
@mikkelse This is correct, there is also compiler magic for memberwise initializers. The compiler knows when it auto-generates those memberwise inits to treat instance variables annotated with property wrappers specially, having the parameter of the init(…)
be without the _
but still initialize those _message
instance variables (even though they are private)!
See this dedicated paragraph in the original Swift Evolution Proposal if you're curious about the details 🙂
Cool, the puzzle is now solved for me!
Again, thanks so much for the effort, it's been a great resource for me trying to understand how it works.
Will check out your links 🙏
I am wondering how SwiftUI is able to detect when a https://github.com/State variable has been changed and the corresponding view needs to be updated. Does the https://github.com/State propertyWrapper gets a reference to some sort of manager and notifies it when its setValue gets called?
@donaldbeebi That I am not sure either to be honest. I think that's a bit more magic handled internally by SwiftUI.
My guess is that in practice, the @State
propertyWrapper might be implemented not using the usual wrappedValue
property, but by using the (private/non-official) "static subscript" accessor for implementing a Property Wrapper.
Basically, usually to implement a @propertyWrapper
, you have to implement a wrappedValue
property, and that's what gets used when the call site uses the property with its original name (not prefixing it with _
or $
).
But there's an alternative way of implementing the getter/setter of a property wrapper, which is mentioned here in the Swift Evolution Proposal. In theory that alternative way is under "Future Directions" and thus is not officially supported yet… but in practice, it is already implemented (it's just that, it not being official and kinda "under the table" API, there's no guarantee that its syntax won't change before it gets turned official, similar to internal underscored methods).
That "static subscript" implementation of a property wrapped allows you to access the "enclosing instance" of the property annotated with the property wrapper. In essence that means that if you declare a @State private var myProp
inside a struct MyView: View
, then the @State
property wrapper would be able to access the MyView
instance that encloses / contains your state variable declaration thanks to that static subscript implementation. This can be very powerful and is probably how the @State
property wrapper is telling the enclosing View
about the existence of the State
variable, and "registers" itself with the View
to tell it to refresh itself when the State changes. You can read more about this on SwiftBySundell and SwiftLee's blogs.
So, my guess is that they might have taken advantage of this (non-official, internal) thing to implement the View refresh mechanism in what might look something like this?
@propertyWrapper struct State<Value>: BindingConvertible {
class Storage {
var value: Value
init(initialValue: Value) { self.value = initialValue }
}
private var storage: Storage
// wrappedValue is not used when we also provide a static subscript, as the static subscript implementation takes precedence
var wrappedValue: Value {
get { fatalError("In theory this will never be called, as the static subscript will be used instead") }
nonmutating set { fatalError("In theory this will never be called, as the static subscript will be used instead") }
}
static subscript<ContainerView: View>(
_enclosingInstance viewInstance: ContainerView,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<ContainerView, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<ContainerView, Self>
) -> Value {
get { viewInstance[keyPath: storageKeyPath].storage.value }
set {
viewInstance[keyPath: storageKeyPath].storage.value = newValue
// Signal the View instance in some way that it needs to be redrawn
viewInstance._setNeedsRefresh() // hypothetical internal method, no idea if that's how its done in practice or what such method would be called if it even exists
}
}
…
}
To be honest I'm not sure that's really how they do it (and I'm starting to have doubts now because I think when you implement a @propertyWrapper
using the static subscript
, it then can only be applied to reference types and not value types… but View
subclasses are typically struct
… so maybe that wouldn't work after all…). There's definitively some more magic and internals going on there 😉
I am wondering how SwiftUI is able to detect when a @State variable has been changed and the corresponding view needs to be updated. Does the @State propertyWrapper gets a reference to some sort of manager and notifies it when its setValue gets called?
The OpenCombine relies on ABI stability to detect the object change. See the discussion here
https://forums.swift.org/t/implementing-a-protocol-default-implementation-analogous-to-that-of-observableobject/49815/3
@haifengkao Interesting, thanks for the find!
This is great @AliSoftware, thanks! 🙌
I also attempted to re-create these property wrappers and cross-checked them with yours.
I was interested in the State
being a struct
in SwiftUI. I first opted for class
, similar to you I think.
I am wondering if the State
can be a struct
because it wraps something similar to a CurrentValueSubject or something.
It might explain how the View
s are able to observe its changes.
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?