Inside SwiftUI (About @State)
SwiftUI was announced at this WWDC and we've tried it for a few days. This WWDC was the moment everything changed for me.
But SwiftUI is still a private source software as well as UIKit and we need to develop applications while speculating the behavior of it.
There is some features which I felt is a mystery in SwiftUI. I investigated them and internal implementation of SwiftUI.
(This post is just a my prediction)
@State
@State var value: Int
You may write this many time while doing tutorials of SwiftUI. Do you understand how State
works?
This feature uses
Property Delegates introduced from Swift5.1 and it allows to delegate the implementation of getter and setter to another instance (In this case "another instance" means State
).
State
is a struct defined in SwiftUI using @propertyDelegate
. If State
is changed, View
will be re-rendered.
First, let's implement State
by myself!
@propertyDelegate struct State<Value> { var storage: Value var value: Value { get { storage } set { storage = newValue renderView() } } init(initialValue value: Value) { self.storage = value } func renderView() { // ??? } } struct ContentView: View { @State var text: String }
It's almost easy to implement but I couldn't figure out how re-render View
. SwiftUI seems not re-render the all Views but some particular views whose state is modified. So I need to link ContentView
and text: State<String>
to update the view.
Internal structure of State
Let's dump the state instance to investigate internal structure of State
by using dump
.
struct ContentView: View { var text = State<String>(initialValue: "Hello") init() { print("Init:") dump(text) } var body: some View { print("Body:") dump(text) return Text("Hello, world") } }
I dumped text
in init
and body
but their outputs are different.
Init: ▿ SwiftUI.State<Swift.String> - _value: "Hello" - _location: nil Body: ▿ SwiftUI.State<Swift.String> - _value: "Hello" ▿ _location: Optional(SwiftUI.StoredLocation<Swift.String>) ▿ some: SwiftUI.StoredLocation<Swift.String> #0 - super: SwiftUI.AnyLocation<Swift.String> - super: SwiftUI.AnyLocationBase ▿ viewGraph: Optional(SwiftUI.ViewGraph) ...
This result shows ViewGraph
appears after init
but before body
. It seems there is a way to inject ViewGraph
into State
outside of View.
Since ViewGraph
is an internal type of SwiftUI, I can only speculate it but it seems ViewGraph
manages tree structure of View. If this ViewGraph
can render View
selectively, it's easy to imitate the re-rendering system.
Then, let's fill the body of renderView
.
@propertyDelegate struct State<Value> { var storage: Value + var viewGraph: ViewGraph? var value: Value { get { storage } set { storage = newValue renderView() } } init(initialValue value: Value) { self.storage = value } + func setViewGraph(_ viewGraph: ViewGraph) { + self.viewGraph = viewGraph + } func renderView() { + viewGraph.render() } }
It became clear that how re-render view but it's still uncertain that how inject the ViewGraph
into ContentView
and make the relationship between State
and View
.
@State
filed belongs to View but there is no way to access text: State<String>
through View
protocol because the field name is not bound by the protocol. For example, it can be easy to inject if State
field is bound by the protocol as follows code.
But actually, fields can be named freely.
protocol ViewGraphInjectable { var state: State<Value> { get } } struct ContentView: View, ViewGraphInjectable { @State var state: String }
Reflection API
The only way to get fields without protocol I first thought is Mirror
. It's very simple that getting fields of View using Mirror
and if there is State
, call setViewGraph
.
But does SwiftUI really use Mirror
? If Mirror
is used, CustomReflectable.customMirror
should be called and print "Mirror is used!".
struct ContentView: View, CustomReflectable { @State var text: String var customMirror: Mirror { print("Mirror is used!") return Mirror(reflecting: self) } }
But customMirror
is not called. For checking, I tried to set breakpoint at swift_reflectionMirror_count
which is runtime function used by Mirror
but it also doesn't break.
This result indicates that SwiftUI get fields without using Mirror
.
Reflection without Mirror
There is still a way to get fields without using Mirror
. It's using metadata.
If you don't know metadata, you can learn it by my try! Swift presentation.
Metadata has Field Descriptor which contains accessors for fields of the type. It's possible to get fields by using it.
My various experiments result AttributeGraph.framework
uses metadata internally. AttributeGraph.framework
is a private framework that SwiftUI use internally for constructing ViewGraph
.
You can see it by the symbols of the framework.
$ nm /System/Library/PrivateFrameworks/AttributeGraph.framework/AttributeGraph
There is AG::swift::metadata_visitor::visit_field
in the list of symbols. i didn't analysis the whole of assembly code but the name implies that AttributeGraph
use visitor pattern to parse metadata. If you set break point, it surely hit.
Why not use Mirror
So why is metadata used instead of Mirror
? I think it's for performance.
Mirror.children
is represented as [(label: String, value: Any)]
to hold any type of value. But Any
wraps the actual value and when you use it, Any
is unwrapped every time. SwiftUI uses View
many times and the overheads can be critical problem.
On the other hand, Using raw pointer through Field Descriptor
doesn't affect performance seriously.
Flow to update View
- Find
State
ofView
usingField Descriptor
- Inject
ViewGraph
intoState
- Render
View.body
State
is changedState
notify theViewGraph
to update view- Re-render
View.body
Only Apple knows the actual implementation. But it's certain that AttributeGraph.framework
has its own reflection system.
I said "I'm looking forward to your great libraries using metadata" in try! Swift but I had never thought Apple do it, I think Apple started to use metadata because ABI stability was built since Swift5. In fact, ABI stability brings us great benefits!
I'll write an article about DynamicViewProperty
by next week.