You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Embedded mode was recently introduced as a subset of Swift for constrained environments, such as the Web where bundle size has to be really small. With Embedded Wasm, we can get binary sizes of ~100 KB for simple apps. Though not perfect, it’s still a huge improvement over the > 7 MB of current Tokamak web apps.
However, there are some challenges with the SwiftUI public API and Tokamak’s internal implementation. For one, SwiftUI publicly uses KeyPath (e.g. for custom environment values) which are unavailable in Embedded. This is a complex issue and will be discussed below in more depth. SwiftUI also uses metatype identifiers for identity and type metadata for AnyHashable. Further, Tokamak relies on type metadata internally. We use reflection to find dynamic properties in views. We make heavy use of existentials for type erasure and to change between different implementation (e.g. AnyColorBox). And we also rely on type casting (often with existentials) for things like finding primitive views in the FiberRenderer. Finally, Tokamak relies on modules like OpenCombine and Foundation which do not currently support Embedded mode.
There are thankfully a couple of solutions. Most type erasers can be rewritten to be backed by closures instead of existentials. When existentials are used to provide different implementations, we can simply write an enum with all possible implementations which should also be more efficient. Also, instead of relying on type casting to traverse the view tree, we can add requirements directly to the View protocol and rewrite the renderer. Finally, features like reflection and metatype object identifiers can be replaced with macros. To retain compatibility with SwiftUI (where View conformance doesn’t require a macro), we could also use a pre-build plugin that automatically adds the macro attribute to View and other stateful-type declarations.
KeyPaths are quite complex to implement. They use non-final classes which are unsupported in Embedded Swift and it’s unclear if the metadata they require will ever be part of Embedded mode either. Instead, when we encounter a key path in .environmentValue(\.myKeyPath, newValue) we could transform myKeyPath into a custom object that is Hashable and offers a setter and getter. This transformation could happen with a macro that could be implicitly added by the aforementioned pre-build plugin. This is by no means a clean solution so please feel free to propose other solutions.
To make all these changes, a lot of internal components will have to change. Though it will be challenging, it is also an opportunity to simplify the codebase, which among other issues has two renderers. I propose that based on the current FiberRenderer, we re-architect a simple renderer that not only works in Embedded, but also goes further in terms of implementing SwiftUI’s layout views and features (such as alignment guides). This rewrite should also strive to have more comprehensive testing.
I currently have a prototype implementation of the renderer itself with support for Embedded and more expansive layout operations than the current Fiber renderer. There are a lot of steps left:
Support targeted updates (instead of remounting the entire node tree)
2. Expose different Layout operations as modifiers
3. Implement overlay and background layouts
4. Implement GeometryReader with coordinate systems
5. Implement preferences and layout values
6. Add support for visual effects (brightness, shadow, etc.)
7. Write macros for types using dynamic properties, metatype identity, and for transforming key paths
8. Rewire Button, Shape and other common views to use the new renderer
The text was updated successfully, but these errors were encountered:
@filip-sakel do you have the prototype implementation up somewhere? I'd be interested in having a look at how targeted updates (and preferences/layout values) could fit in.
Embedded mode was recently introduced as a subset of Swift for constrained environments, such as the Web where bundle size has to be really small. With Embedded Wasm, we can get binary sizes of ~100 KB for simple apps. Though not perfect, it’s still a huge improvement over the > 7 MB of current Tokamak web apps.
However, there are some challenges with the SwiftUI public API and Tokamak’s internal implementation. For one, SwiftUI publicly uses
KeyPath
(e.g. for custom environment values) which are unavailable in Embedded. This is a complex issue and will be discussed below in more depth. SwiftUI also uses metatype identifiers for identity and type metadata forAnyHashable
. Further, Tokamak relies on type metadata internally. We use reflection to find dynamic properties in views. We make heavy use of existentials for type erasure and to change between different implementation (e.g.AnyColorBox
). And we also rely on type casting (often with existentials) for things like finding primitive views in the FiberRenderer. Finally, Tokamak relies on modules like OpenCombine and Foundation which do not currently support Embedded mode.There are thankfully a couple of solutions. Most type erasers can be rewritten to be backed by closures instead of existentials. When existentials are used to provide different implementations, we can simply write an enum with all possible implementations which should also be more efficient. Also, instead of relying on type casting to traverse the view tree, we can add requirements directly to the
View
protocol and rewrite the renderer. Finally, features like reflection and metatype object identifiers can be replaced with macros. To retain compatibility with SwiftUI (where View conformance doesn’t require a macro), we could also use a pre-build plugin that automatically adds the macro attribute toView
and other stateful-type declarations.KeyPaths are quite complex to implement. They use non-final classes which are unsupported in Embedded Swift and it’s unclear if the metadata they require will ever be part of Embedded mode either. Instead, when we encounter a key path in
.environmentValue(\.myKeyPath, newValue)
we could transformmyKeyPath
into a custom object that is Hashable and offers a setter and getter. This transformation could happen with a macro that could be implicitly added by the aforementioned pre-build plugin. This is by no means a clean solution so please feel free to propose other solutions.To make all these changes, a lot of internal components will have to change. Though it will be challenging, it is also an opportunity to simplify the codebase, which among other issues has two renderers. I propose that based on the current
FiberRenderer
, we re-architect a simple renderer that not only works in Embedded, but also goes further in terms of implementing SwiftUI’s layout views and features (such as alignment guides). This rewrite should also strive to have more comprehensive testing.I currently have a prototype implementation of the renderer itself with support for
Embedded
and more expansive layout operations than the current Fiber renderer. There are a lot of steps left:Layout
operations as modifiersGeometryReader
with coordinate systemsbrightness
,shadow
, etc.)Button
,Shape
and other common views to use the new rendererThe text was updated successfully, but these errors were encountered: