kateinoigakukunのブログ

思考垂れ流しObserver

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

  1. Find State of View using Field Descriptor
  2. Inject ViewGraph into State
  3. Render View.body
  4. State is changed
  5. State notify the ViewGraph to update view
  6. 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.