You don't need to interact with any of this directly unless you're doing
something very advanced. Many of these are only exposed as public so that
advanced users don't have to reimplement helpers that we've already implemented,
and others are exposed to enable unique use-cases such as embedding SwiftCrossUI
view graphs inside existing non-SwiftCrossUI apps.
It's a good idea to refer to the source code while reading this, as not all of the APIs mentioned are made public.
The entry point of SwiftCrossUI apps is the implementation of App/main()
provided by the App protocol. That function defers to
_App.run().
_App.run() starts the backend's main loop, and does the rest of the setup once
the backend is ready. The rest of the setup phase involves:
Environment properties on the app struct via
updateDynamicProperties(of:previousValue:environment:)State properties on the app struct using Mirror and
observeAsUIUpdater(backend:action:) (which handles basic update debouncing)Scene types (structs conforming to Scene) each have their own node type
(conforming to SceneGraphNode). These node types are classes and store any
persistent state associated with the scene (e.g. the window pointer or the
ViewGraph associated with the scene).
Scenes here are just anything that exists at the level above views (e.g. windows and app menus).
The root of the view graph is the ViewGraph class. It stores a root node and
some information required to glue scenes and view graphs together (such as the
last known window size). ViewGraph is a pretty thin wrapper around
ViewGraphNode and essentially just hides some of the more internal details
that scenes don't need to worry about.
Each view generally has an associated ViewGraphNode. This is where the
view's state is persisted, and is where any fancy layout caching happens. The
view graph node's lifecycle is also what View/onAppear(perform:) and
View/onDisappear(perform:) are based on; for example,
View/onDisappear(perform:) runs when a node deinits.
ViewGraphNodes store their children using types conforming to the
ViewGraphNodeChildren protocol. The exact type is determined by the
associated view. For example, TupleView3 stores its three child nodes ---
child0, child1, and child2 --- using TupleViewChildren3, while
ForEach stores its children in the ForEachViewChildren class which is
designed for storing a homogenous array of child nodes. It's the responsibility
of View/computeLayout(_:children:proposedSize:environment:backend:)
implementations to propagate updates on to any relevant child nodes.
View updates happen in two phases; the layout phase and the commit phase.
The layout phase allows views to efficiently be queried for their layout at
multiple different sizes without too much overhead. During the layout phase,
ViewGraphNode can avoid querying its associated view for a potentially
expensive layout computation if it believes that it can already satisfy the
query using basic assumptions about view layout behaviour and the results of
previous layout updates (see ViewGraphNode.resultCache and
ViewGraphNode.currentResult).
The commit phase simply applies the result of the last layout computation to the view's underlying widget.
There are two types of view updates; top-down and bottom-up. Top-down updates occur when a view's parent view has updated for some reason (be that a state update, or the parent's parent updating, or some other reason). Bottom-up updates happen when a view's child updated due to a state change and the child changed size due to that update. Bottom-up updates continue propagating upwards until a view doesn't change size (meaning that we can avoid notifying its parent).
The most famous requirement of View is View/body; this is the property
you know and love from SwiftUI. It's sufficient to just implement this property
when you're implementing regular views.
The following requirements are used to implement internal views such as
Button:
term View/children(backend:snapshots:environment:): This method
produces the ViewGraphNodeChildren instance that the view would like to
use to store its children. The return type is an existential because otherwise
we would need to expose an associatedtype Children: ViewGraphNodeChildren
requirement to users just trying to write regular views using the
View/body requirement. (There is an internal TypeSafeView protocol that
adds this requirement.)
term View/layoutableChildren(backend:children:): This method is only
implemented by the TupleViewN types. It essentially just gets the view's
children as an array of LayoutSystem/LayoutableChilds, the type that
LayoutSystem works with when computing stack layouts.
term View/asWidget(_:backend:): This method is generally pretty
simple; just use the backend instance to produce the widget type the view
wants to use. Views only ever have a single associated widget instance; views
that want to change their underlying widget can use an intermediate container
widget to satisfy this requirement.
This method shouldn't configure the widget at all, that's handled by
View/computeLayout(_:children:proposedSize:environment:backend:) and
View/commit(_:children:layout:environment:backend:)
(which are guaranteed to be called between View/asWidget(_:backend:)
and the first time the view appears on screen).
term View/computeLayout(_:children:proposedSize:environment:backend:):
This method is the meat of most view implementations; its role is to compute
view layouts. It may be called multiple times before the layout system settles
on a result.
term View/commit(_:children:layout:environment:backend:): This
method updates the widgets to be displayed on-screen. It recieves the most
recent result of View/computeLayout(_:children:proposedSize:environment:backend:)
as the layout parameter.
Internally, we have the ElementaryView and TypeSafeView protocols (which
extend View) and are just nicer versions of the View protocol useful for
certain purposes. ElementaryView is for views with no children, and
TypeSafeView is identical to View but with a Children associated type.
If a view or modifier wants to change the environment for all child views, it
does this in
View/computeLayout(_:children:proposedSize:environment:backend:) by
passing a modified copy of the environment to child nodes when calling their
update methods.
Preferences are similar to environment values, except they propagate up the
view graph instead of down. For instance, the PreferenceValues/onOpenURL
preference is used to propagate external URL handlers to the top level to be
registered with the backend.
Preferences are propagated as part of the ViewLayoutResult type. Container
views can pass multiple ViewLayoutResult instances to
ViewLayoutResult/init(size:childResults:participateInStackLayoutsWhenEmpty:preferencesOverlay:)
to have their preferences merged automatically into a single PreferenceValues
instance.
Dynamic properties are properties of View- or App-conforming structs
that conform to the DynamicProperty protocol --- for example, State or
Environment.
There are two methods used to update these properties when view or app bodies
are recomputed: the primary one calculates the offsets to the properties at
runtime, while the fallback method uses Mirror to set the properties. Choosing
which method to use, as well as actually performing the update, is handled by
DynamicPropertyUpdater.
This method uses the DynamicKeyPath type, which can construct a "key path" at
runtime given an instance of the type and the current value of the property in
question. DynamicKeyPath performs some withUnsafeBytes(of:_:) magic to find
the property's offset within the type.
The fallback method (which was the only method before the
doc:Layout-performance PR was merged) simply uses Mirror to query the type's
properties, check them for conformance to DynamicProperty, and update the
values. It's only used when computing offsets for the primary method fails
(usually because multiple properties of the type have the same bit-level
representation of their current values).
It can be up to 1500 times slower than the primary method (with the difference decreasing as more stateful properties are added); this is why the primary method is preferred wherever possible.
LayoutSystem
EitherView
EmptyView
OptionalView
HotReloadableView
ViewSize
ViewLayoutResult
ProposedViewSize
AnyWidget
PublishedMarkerProtocol
DynamicProperty
ObservableProperty
SnapshottableProperty
doc:View-graph
doc:Scene-graph
doc:Adding-new-backend-methods