Reverse-Engineering SwiftUI

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.

15 Likes

I would suggest taking a look at the Open source Composable Architecture. which has emphasis in testability. About the Swift Composable Architecture category

Also take a look at GitHub - OpenCombine/OpenCombine: Open source implementation of Apple's Combine framework for processing values over time. since the one that SwiftUI uses is only available in Apple platforms.

Finally you can look at GitHub - Cosmo/OpenSwiftUI: WIP — OpenSwiftUI is an OpenSource implementation of Apple's SwiftUI DSL. but I don't see much movement there. Good luck!

Thank you very much for the references! I'll do my best to squeeze all relevant information from there and update the original post with my findings!

Did you ever post a repo of this? I'm interested in trying to make a Windows-based implementation so that any SwiftUI code can work on Windows. Thanks

Our team got public parts of the API implemented in Tokamak, including the state management property wrappers. I'm definitely interested in having a working Windows renderer, PRs are very welcome. We don't have full compatibility with underscored types and properties, so things like _ViewInputs won't work, but I haven't seen these used in real SwiftUI apps anyway.

5 Likes

They are in fact used of-course.
You can hack & slash the _make functions of certain SwiftUI constructs to dump & debug things.

1 Like

How exactly does the Environment property wrapper get a hold of that _DynamicPropertyBuffer ...

I think the buffer gets injected into all DynamicPropertys (including Environment) using the runtime/reflection.

Did you get anywhere with this? I'm doing my own clone and would love to share ideas!