Hello, Swift community!
I've embarked on a quest to reverse-engineer SwiftUI (specifically, the data flow architecture). I really like how incredibly concise and ergonomic and powerful and flexible it is, so I'd like to implement my own data flow mechanism with EnvironmentKey
(moving data down the hierarchy), PreferenceKey
(moving data up the hierarchy), State
(accessing internal data), and Binding
(accessing external data).
I'd like to open up a discussion in an effort to get you guys to pitch in and help me develop an implementation of this awesome data flow architecture for everyone to use! I'll be updating this post with new information as it comes in and I'll also create a GitHub repository with the reference implementation and link it here. Any and all help (comments, suggestions, insight, ideas, code snippets, ...) are highly appreciated!
I've looked through the swiftinterface
file for SwiftUI as well as the official documentation for SwiftUI and here's the result of my research so far:
View
SwiftUI.View
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@_typeEraser(AnyView) public protocol View {
static func _makeView(view: SwiftUI._GraphValue<Self>, inputs: SwiftUI._ViewInputs) -> SwiftUI._ViewOutputs
static func _makeViewList(view: SwiftUI._GraphValue<Self>, inputs: SwiftUI._ViewListInputs) -> SwiftUI._ViewListOutputs
@available(iOS 14.0, OSX 10.16, tvOS 14.0, watchOS 7.0, *)
static func _viewListCount(inputs: SwiftUI._ViewListCountInputs) -> Swift.Int?
associatedtype Body : SwiftUI.View
var body: Self.Body { get }
}
SwiftUI._ViewInputs
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _ViewInputs {
}
SwiftUI._ViewOutputs
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _ViewOutputs {
}
SwiftUI._ViewListInputs
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _ViewListInputs {
}
SwiftUI._ViewListOutputs
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _ViewListOutputs {
}
SwiftUI._ViewListCountInputs
@available(iOS 14.0, OSX 10.16, tvOS 14.0, watchOS 7.0, *)
public struct _ViewListCountInputs {
}
It looks like the View's
makeView
, makeViewList
and makeViewListCount
methods are used to render the view hierarchy recursively. My best guess is that the _DynamicPropertyBuffer
is initialized before the view hierarchy is rendered and is propagated down the call chain. The open question is: How exactly does the Environment
property wrapper get a hold of that _DynamicPropertyBuffer
...
DynamicProperty
SwiftUI.DynamicProperty
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol DynamicProperty {
static func _makeProperty<V>(in buffer: inout SwiftUI._DynamicPropertyBuffer, container: SwiftUI._GraphValue<V>, fieldOffset: Swift.Int, inputs: inout SwiftUI._GraphInputs)
mutating func update()
}
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension DynamicProperty {
public static func _makeProperty<V>(in buffer: inout SwiftUI._DynamicPropertyBuffer, container: SwiftUI._GraphValue<V>, fieldOffset: Swift.Int, inputs: inout SwiftUI._GraphInputs)
public mutating func update()
}
SwiftUI._DynamicPropertyBuffer
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _DynamicPropertyBuffer {
}
SwiftUI._Graph
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _Graph {
}
SwiftUI._GraphValue
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _GraphValue<Value> : Swift.Equatable {
public subscript<U>(keyPath: Swift.KeyPath<Value, U>) -> SwiftUI._GraphValue<U> {
get
}
public static func == (a: SwiftUI._GraphValue<Value>, b: SwiftUI._GraphValue<Value>) -> Swift.Bool
}
SwiftUI._GraphInputs
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _GraphInputs {
}
EnvironmentKey
SwiftUI.EnvironmentKey
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol EnvironmentKey {
associatedtype Value
static var defaultValue: Self.Value { get }
}
I figured out the essence of EnvironmentKey
: The conforming type itself is probably wrapped in an ObjectIdentifier
and used as a key into a Dictionary
whose Value
is Any
, which is then force-cast to EnvironmentKey.Value
and if a Value
doesn't exist for a given EnvironmentKey.Type
, the Key.defaultValue
is returned. Judging by the naming, that Dictionary
is probably accessible through _DynamicPropertyBuffer
.
EnvironmentValues
SwiftUI.EnvironmentValues
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct EnvironmentValues : Swift.CustomStringConvertible {
public init()
public subscript<K>(key: K.Type) -> K.Value where K : SwiftUI.EnvironmentKey {
get
set
}
public var description: Swift.String {
get
}
}
EnvironmentValues
probably has a private init
that takes a _DynamicPropertyBuffer
and enables access to the Value
of EnvironmentKey
stored in the _DynamicPropertyBuffer
.
EnvironmentObject
SwiftUI.EnvironmentObject
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct EnvironmentObject<ObjectType> : SwiftUI.DynamicProperty where ObjectType : Combine.ObservableObject {
@dynamicMemberLookup @frozen public struct Wrapper {
internal let root: ObjectType
public subscript<Subject>(dynamicMember keyPath: Swift.ReferenceWritableKeyPath<ObjectType, Subject>) -> SwiftUI.Binding<Subject> {
get
}
}
@inlinable public var wrappedValue: ObjectType {
get {
guard let store = _store else { error() }
return store
}
}
@usableFromInline
internal var _store: ObjectType?
@usableFromInline
internal var _seed: Swift.Int = 0
public var projectedValue: SwiftUI.EnvironmentObject<ObjectType>.Wrapper {
get
}
@usableFromInline
internal func error() -> Swift.Never
public init()
public static func _makeProperty<V>(in buffer: inout SwiftUI._DynamicPropertyBuffer, container: SwiftUI._GraphValue<V>, fieldOffset: Swift.Int, inputs: inout SwiftUI._GraphInputs)
}
I haven't researched EnvironmentObject
yet.
Environment
SwiftUI.Environment
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct Environment<Value> : SwiftUI.DynamicProperty {
@usableFromInline
@frozen internal enum Content {
case keyPath(Swift.KeyPath<SwiftUI.EnvironmentValues, Value>)
case value(Value)
}
@usableFromInline
internal var content: SwiftUI.Environment<Value>.Content
@inlinable public init(_ keyPath: Swift.KeyPath<SwiftUI.EnvironmentValues, Value>) {
content = .keyPath(keyPath)
}
@inlinable public var wrappedValue: Value {
get {
switch content {
case let .value(value):
return value
case let .keyPath(keyPath):
// not bound to a view, return the default value.
return EnvironmentValues()[keyPath : keyPath]
}
}
}
@usableFromInline
internal func error() -> Swift.Never
public static func _makeProperty<V>(in buffer: inout SwiftUI._DynamicPropertyBuffer, container: SwiftUI._GraphValue<V>, fieldOffset: Swift.Int, inputs: inout SwiftUI._GraphInputs)
}
The Environment
property wrapper probably starts off with the keyPath
case
for its Content
, but during the call to update()
(which it inherits from DynamicProperty
) it collapses into the value
case
in order to be accessible. Judging by the fact that the keyPath
and value
cases are mutually exclusive (the Environment
property wrapper doesn't remember which KeyPath
it got the value from), I'm guessing that the Environment
wrapper is copied to some sort of parallel hierarchy every time it has to be evaluated before collapsing it and calling the View
's body
property. This hypothesis is somewhat reinforced by the fact that View
is heavily advertised as value-type only protocol (since this would guarantee that copying a view hierarchy copies all instances of Environment
, so that those copies can be collapsed). This (and many other observations) has led me to believe that the View
's body
should never be called explicitly, since there is a lot of internal invariants that need to be met in order for the access to DynamicProperty
-conforming property wrappers to be safe and well-behaved.
@Paul_Hudson has uploaded a wonderful video to YouTube titled Global Variable Oriented Programming – Reimplementing SwiftUI's environment for UIKit. Thank you very much, Paul!
StateObject
SwiftUI.StateObject
@available(iOS 14.0, OSX 10.16, tvOS 14.0, watchOS 7.0, *)
@frozen @propertyWrapper public struct StateObject<ObjectType> : SwiftUI.DynamicProperty where ObjectType : Combine.ObservableObject {
@usableFromInline
@frozen internal enum Storage {
case initially(() -> ObjectType)
case object(SwiftUI.ObservedObject<ObjectType>)
}
@usableFromInline
internal var storage: SwiftUI.StateObject<ObjectType>.Storage
@inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType) {
storage = .initially(thunk)
}
public var wrappedValue: ObjectType {
get
}
public var projectedValue: SwiftUI.ObservedObject<ObjectType>.Wrapper {
get
}
public static func _makeProperty<V>(in buffer: inout SwiftUI._DynamicPropertyBuffer, container: SwiftUI._GraphValue<V>, fieldOffset: Swift.Int, inputs: inout SwiftUI._GraphInputs)
}
I haven't researched State
yet.
PreferenceKey
SwiftUI.PreferenceKey
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol PreferenceKey {
associatedtype Value
static var defaultValue: Self.Value { get }
static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
static var _includesRemovedValues: Swift.Bool { get }
static var _isReadableByHost: Swift.Bool { get }
}
PreferenceKey
is a lot like EnvironmentKey
, except since the data is traveling up the view hierarchy instead of down, the conforming type should provide a way of collapsing multiple preference values into one before the combined value is propagated further up the view hierarchy.
State
SwiftUI.State
@available(iOS 14.0, OSX 10.16, tvOS 14.0, watchOS 7.0, *)
@frozen @propertyWrapper public struct StateObject<ObjectType> : SwiftUI.DynamicProperty where ObjectType : Combine.ObservableObject {
@usableFromInline
@frozen internal enum Storage {
case initially(() -> ObjectType)
case object(SwiftUI.ObservedObject<ObjectType>)
}
@usableFromInline
internal var storage: SwiftUI.StateObject<ObjectType>.Storage
@inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType) {
storage = .initially(thunk)
}
public var wrappedValue: ObjectType {
get
}
public var projectedValue: SwiftUI.ObservedObject<ObjectType>.Wrapper {
get
}
public static func _makeProperty<V>(in buffer: inout SwiftUI._DynamicPropertyBuffer, container: SwiftUI._GraphValue<V>, fieldOffset: Swift.Int, inputs: inout SwiftUI._GraphInputs)
}
I haven't researched State
yet.
Binding
SwiftUI.Binding
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper @dynamicMemberLookup public struct Binding<Value> {
public var transaction: SwiftUI.Transaction
internal var location: SwiftUI.AnyLocation<Value>
fileprivate var _value: Value
public init(get: @escaping () -> Value, set: @escaping (Value) -> Swift.Void)
public init(get: @escaping () -> Value, set: @escaping (Value, SwiftUI.Transaction) -> Swift.Void)
public static func constant(_ value: Value) -> SwiftUI.Binding<Value>
public var wrappedValue: Value {
get
nonmutating set
}
public var projectedValue: SwiftUI.Binding<Value> {
get
}
public subscript<Subject>(dynamicMember keyPath: Swift.WritableKeyPath<Value, Subject>) -> SwiftUI.Binding<Subject> {
get
}
}
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension Binding {
public func transaction(_ transaction: SwiftUI.Transaction) -> SwiftUI.Binding<Value>
public func animation(_ animation: SwiftUI.Animation? = .default) -> SwiftUI.Binding<Value>
}
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension Binding : SwiftUI.DynamicProperty {
public static func _makeProperty<V>(in buffer: inout SwiftUI._DynamicPropertyBuffer, container: SwiftUI._GraphValue<V>, fieldOffset: Swift.Int, inputs: inout SwiftUI._GraphInputs)
}
I haven't researched Binding
yet.