TokamakUI / Tokamak

SwiftUI-compatible framework for building browser apps with WebAssembly and native apps for other platforms
Apache License 2.0
2.62k stars 111 forks source link

Roadmap for Embedded Support #557

Open filip-sakel opened 5 months ago

filip-sakel commented 5 months ago

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:

mortenbekditlevsen commented 5 months ago

What about String? That's also not available in embedded Swift, is it?

kkebo commented 5 months ago

@mortenbekditlevsen No, String is already available in the main snapshot toolchain.

ahti commented 3 months ago

@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.