From e11effdd8c602aac36a70ea95fcfc875ffb96e1f Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 6 Aug 2020 13:57:36 +0100 Subject: [PATCH 1/2] Use the latest 5.3 snapshot in `.swift-version` (#252) * Use the latest 5.3 snapshot in `.swift-version` These SwiftWasm snapshots should be more stable in general and also have a workaround for https://github.com/swiftwasm/JavaScriptKit/issues/6 included. They still use the old metadata layout, so Runtime and OpenCombine dependencies had to be updated in `Package.swift` for `@ObservableObject` to work with these snapshots. * Fix linter warning --- .swift-version | 2 +- Package.resolved | 8 ++++---- Package.swift | 7 +++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.swift-version b/.swift-version index 55167b9ac..01e98ba4f 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -wasm-DEVELOPMENT-SNAPSHOT-2020-06-12-a +wasm-5.3-SNAPSHOT-2020-07-27-a diff --git a/Package.resolved b/Package.resolved index b16a32101..04662b69f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,8 @@ "package": "OpenCombine", "repositoryURL": "https://github.com/MaxDesiatov/OpenCombine.git", "state": { - "branch": "observable-object", - "revision": "3c3a181acad7ab44a64d7c41140eb843222bb2aa", + "branch": "observable-object-5.3", + "revision": "979d83e8725a504567827b89a5656bf4fd36d0c6", "version": null } }, @@ -23,8 +23,8 @@ "package": "Runtime", "repositoryURL": "https://github.com/MaxDesiatov/Runtime.git", "state": { - "branch": "wasi-build", - "revision": "a9309b4822d6dd0e4a8e92351ee9e3d210e19b4e", + "branch": "wasi-build-5.3", + "revision": "a617ead8a125a97e69d6100e4d27922006e82e0a", "version": null } } diff --git a/Package.swift b/Package.swift index a2e9ca67a..dd1e07cae 100644 --- a/Package.swift +++ b/Package.swift @@ -38,8 +38,11 @@ let package = Package( // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), .package(url: "https://github.com/kateinoigakukun/JavaScriptKit.git", .revision("c90e82f")), - .package(url: "https://github.com/MaxDesiatov/Runtime.git", .branch("wasi-build")), - .package(url: "https://github.com/MaxDesiatov/OpenCombine.git", .branch("observable-object")), + .package(url: "https://github.com/MaxDesiatov/Runtime.git", .branch("wasi-build-5.3")), + .package( + url: "https://github.com/MaxDesiatov/OpenCombine.git", + .branch("observable-object-5.3") + ), ], targets: [ // Targets are the basic building blocks of a package. A target can define From 2a49b7808bf3f67388b06b88ee72fef605e447fe Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 7 Aug 2020 16:01:27 +0100 Subject: [PATCH 2/2] Link to the renderers guide from `README.md` (#251) * Link to the renderers guide from `README.md` The guide itself was merged into a single file for easier navigation. * Update RenderersGuide.md --- README.md | 93 +++-- .../1 Renderers in Tokamak.md | 19 - .../2 Understanding Renderers.md | 17 - .../3 TokamakStatic Setup.md | 80 ---- .../4 Building the Target.md | 45 --- .../5 Building the Renderer.md | 134 ------- ... Providing platform-specific primitives.md | 61 --- docs/RenderersGuide.md | 363 ++++++++++++++++++ 8 files changed, 414 insertions(+), 398 deletions(-) delete mode 100644 docs/Building a Renderer/1 Renderers in Tokamak.md delete mode 100644 docs/Building a Renderer/2 Understanding Renderers.md delete mode 100644 docs/Building a Renderer/3 TokamakStatic Setup.md delete mode 100644 docs/Building a Renderer/4 Building the Target.md delete mode 100644 docs/Building a Renderer/5 Building the Renderer.md delete mode 100644 docs/Building a Renderer/6 Providing platform-specific primitives.md create mode 100644 docs/RenderersGuide.md diff --git a/README.md b/README.md index b1edb11c5..4da40b146 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,25 @@ ![CI status](https://github.com/swiftwasm/Tokamak/workflows/CI/badge.svg?branch=main) -At the moment Tokamak implements a very basic subset of SwiftUI. Its DOM renderer supports -a few view types and modifiers (you can check the current list in [the progress document](docs/progress.md)), -and a new `HTML` view for constructing arbitrary HTML. The long-term goal of Tokamak is to implement -as much of SwiftUI API as possible and to provide a few more helpful additions that simplify HTML -and CSS interactions. +At the moment Tokamak implements a very basic subset of SwiftUI. Its DOM renderer supports a few +view types and modifiers (you can check the current list in [the progress +document](docs/progress.md)), and a new `HTML` view for constructing arbitrary HTML. The long-term +goal of Tokamak is to implement as much of SwiftUI API as possible and to provide a few more helpful +additions that simplify HTML and CSS interactions. If there's some SwiftUI API that's missing but you'd like to use it, please review the existing -[issues](https://github.com/swiftwasm/Tokamak/issues) and [PRs](https://github.com/swiftwasm/Tokamak/pulls) -to get more details about the current status, or [create a new issue](https://github.com/swiftwasm/Tokamak/issues/new) -to let us prioritize the development based on the demand. We also try to make the development of -views and modifiers easier (with the help from the `HTML` view, see [the example -below](https://github.com/swiftwasm/Tokamak#arbitrary-html)), so pull requests are very welcome! Don't -forget to check [the "Contributing" section](https://github.com/swiftwasm/Tokamak#contributing) first. - -If you'd like to participate in the growing [SwiftWasm](https://swiftwasm.org) community, you're also very -welcome to join the `#webassembly` channel in [the SwiftPM Slack](https://swift-package-manager.herokuapp.com/). +[issues](https://github.com/swiftwasm/Tokamak/issues) and +[PRs](https://github.com/swiftwasm/Tokamak/pulls) to get more details about the current status, or +[create a new issue](https://github.com/swiftwasm/Tokamak/issues/new) to let us prioritize the +development based on the demand. We also try to make the development of views and modifiers easier +(with the help from the `HTML` view, see [the example +below](https://github.com/swiftwasm/Tokamak#arbitrary-html)), so pull requests are very welcome! +Don't forget to check [the "Contributing" +section](https://github.com/swiftwasm/Tokamak#contributing) first. + +If you'd like to participate in the growing [SwiftWasm](https://swiftwasm.org) community, you're +also very welcome to join the `#webassembly` channel in [the SwiftPM +Slack](https://swift-package-manager.herokuapp.com/). ### Example code @@ -169,34 +172,33 @@ will build the demo app that shows almost all of the currently implemented APIs. ### Modular structure Tokamak is built with modularity in mind, providing a cross-platform `TokamakCore` module and -separate modules for platform-specific renderers. Currently, the only available renderer module -is `TokamakDOM`, but we intend to provide other renderers in the future, such as `TokamakHTML` -for static websites and server-side rendering. Tokamak users only need to import a renderer module -they would like to use, while `TokamakCore` is hidden as an "internal" `Tokamak` package target. -Unfortunately, Swift does not allow us to specify that certain symbols in `TokamakCore` are private -to a package, but they need to stay `public` for renderer modules to get access to them. Thus, the -current workaround is to mark those symbols with underscores in their names to indicate this. It -can be formulated as these "rules": - -1. If a symbol is restricted to a module and has no `public` access control, no need for an underscore. -2. If a symbol is part of a public renderer module API (e.g. `TokamakDOM`), no need for an underscore, - users may use those symbols directly, and it is re-exported from `TokamakCore` by the renderer module - via `public typealias`. -3. If a function or a type have `public` on them only by necessity to make them available in `TokamakDOM`, - but unavailable to users (or not intended for public use), underscore is needed to indicate that. - -The benefit of separate modules is that they allow us to provide separate renderers for different platforms. -Users can pick and choose what they want to use, e.g. purely static websites would use only `TokamakHTML`, -single-page apps would use `TokamakDOM`, maybe in conjuction with `TokamakHTML` for pre-rendering. As we'd -like to try to implement a native renderer for Android at some point, probably in a separate `TokamakAndroid` -module, Android apps would use `TokamakAndroid` with no need to be aware of any of the web modules. - -### Sponsorship - -If this library saved you any amount of time or money, please consider [sponsoring -the work of its maintainer](https://github.com/sponsors/MaxDesiatov). While some of the -sponsorship tiers give you priority support or even consulting time, any amount is -appreciated and helps in maintaining the project. +separate modules for platform-specific renderers. Currently, the only available renderer modules are +`TokamakDOM` and `TokamakStaticHTML`, the latter can be used for static websites and server-side +rendering. If you'd like to implement your own custom renderer, please refer to our [renderers +guide](docs/RenderersGuide.md) for more details. + +Tokamak users only need to import a renderer module they would like to use, while +`TokamakCore` is hidden as an "internal" `Tokamak` package target. Unfortunately, Swift does not +allow us to specify that certain symbols in `TokamakCore` are private to a package, but they need to +stay `public` for renderer modules to get access to them. Thus, the current workaround is to mark +those symbols with underscores in their names to indicate this. It can be formulated as these +"rules": + +1. If a symbol is restricted to a module and has no `public` access control, no need for an + underscore. +2. If a symbol is part of a public renderer module API (e.g. `TokamakDOM`), no need for an + underscore, users may use those symbols directly, and it is re-exported from `TokamakCore` by the + renderer module via `public typealias`. +3. If a function or a type have `public` on them only by necessity to make them available in + `TokamakDOM`, but unavailable to users (or not intended for public use), underscore is needed to + indicate that. + +The benefit of separate modules is that they allow us to provide separate renderers for different +platforms. Users can pick and choose what they want to use, e.g. purely static websites would use +only `TokamakStaticHTML`, single-page apps would use `TokamakDOM`, maybe in conjuction with +`TokamakStaticHTML` for pre-rendering. As we'd like to try to implement a native renderer for +Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use +`TokamakAndroid` with no need to be aware of any of the web modules. ### Coding Style @@ -232,6 +234,13 @@ Conduct](https://github.com/swiftwasm/Tokamak/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to conduct@tokamak.dev. +### Sponsorship + +If this library saved you any amount of time or money, please consider [sponsoring +the work of its maintainer](https://github.com/sponsors/MaxDesiatov). While some of the +sponsorship tiers give you priority support or even consulting time, any amount is +appreciated and helps in maintaining the project. + ## Maintainers [Carson Katri](https://github.com/carson-katri), diff --git a/docs/Building a Renderer/1 Renderers in Tokamak.md b/docs/Building a Renderer/1 Renderers in Tokamak.md deleted file mode 100644 index c2f0af4bf..000000000 --- a/docs/Building a Renderer/1 Renderers in Tokamak.md +++ /dev/null @@ -1,19 +0,0 @@ -# `Renderers` in Tokamak - -Tokamak is a flexible library. `TokamakCore` provides the SwiftUI-API, which your `Renderer` can use -to construct a representation of `Views` that your platform understands. - -To explain the creation of `Renderers`, we’ll be creating a simple one: `TokamakStaticHTML` (which -you can find in the `Tokamak` repository). - -Before we create the `Renderer`, we need to understand the requirements of our platform: - -1. Stateful apps cannot be created This simplifies the scope of our project, as we only have to - render once. However, if you are building a `Renderer` that supports state changes, the process - is largely the same. `TokamakCore`’s `StackReconciler` will let your `Renderer` know when a - `View` has to be redrawn. -2. HTML should be rendered `TokamakDOM` provides HTML representations of many `Views`, so we can - utilize it. However, we will cover how to provide custom `View` bodies your `Renderer` can - understand, and when you are required to do so. - -And that’s it! In the next part we’ll go more in depth on `Renderers`. diff --git a/docs/Building a Renderer/2 Understanding Renderers.md b/docs/Building a Renderer/2 Understanding Renderers.md deleted file mode 100644 index 23af81be6..000000000 --- a/docs/Building a Renderer/2 Understanding Renderers.md +++ /dev/null @@ -1,17 +0,0 @@ -# Understanding `Renderers` - -So, what goes into a `Renderer`? - -1. A `Target` - Targets are the destination for rendered `Views`. For instance, on iOS this is - `UIView`, on macOS an `NSView`, and on the web we render to DOM nodes. -2. A `StackReconciler` - The reconciler does all the heavy lifting to understand the view tree. It - notifies your `Renderer` of what views need to be mounted/unmounted. -3. `func mountTarget`- This function is called when a new target instance should be created and - added to the parent (either as a subview or some other way, e.g. installed if it’s a layout - constraint). -4. `func update` - This function is called when an existing target instance should be updated (e.g. - when `State` changes). -5. `func unmount` - This function is called when an existing target instance should be unmounted: - removed from the parent and most likely destroyed. - -That’s it! Let’s get our project setup. diff --git a/docs/Building a Renderer/3 TokamakStatic Setup.md b/docs/Building a Renderer/3 TokamakStatic Setup.md deleted file mode 100644 index 09aaab331..000000000 --- a/docs/Building a Renderer/3 TokamakStatic Setup.md +++ /dev/null @@ -1,80 +0,0 @@ -# `TokamakStaticHTML` Setup - -Every `Renderer` can choose what `Views`, `ViewModifiers`, property wrappers, etc. are available to -use. A `Core.swift` file is used to reexport these symbols. For `TokamakStaticHTML`, we’ll use the -following `Core.swift` file: - -```swift -import TokamakCore - -// MARK: Environment & State - -public typealias Environment = TokamakCore.Environment - -// MARK: Modifiers & Styles - -public typealias ViewModifier = TokamakCore.ViewModifier -public typealias ModifiedContent = TokamakCore.ModifiedContent - -public typealias DefaultListStyle = TokamakCore.DefaultListStyle -public typealias PlainListStyle = TokamakCore.PlainListStyle -public typealias InsetListStyle = TokamakCore.InsetListStyle -public typealias GroupedListStyle = TokamakCore.GroupedListStyle -public typealias InsetGroupedListStyle = TokamakCore.InsetGroupedListStyle - -// MARK: Shapes - -public typealias Shape = TokamakCore.Shape - -public typealias Capsule = TokamakCore.Capsule -public typealias Circle = TokamakCore.Circle -public typealias Ellipse = TokamakCore.Ellipse -public typealias Path = TokamakCore.Path -public typealias Rectangle = TokamakCore.Rectangle -public typealias RoundedRectangle = TokamakCore.RoundedRectangle - -// MARK: Primitive values - -public typealias Color = TokamakCore.Color -public typealias Font = TokamakCore.Font - -public typealias CGAffineTransform = TokamakCore.CGAffineTransform -public typealias CGPoint = TokamakCore.CGPoint -public typealias CGRect = TokamakCore.CGRect -public typealias CGSize = TokamakCore.CGSize - -// MARK: Views - -public typealias Divider = TokamakCore.Divider -public typealias ForEach = TokamakCore.ForEach -public typealias GridItem = TokamakCore.GridItem -public typealias Group = TokamakCore.Group -public typealias HStack = TokamakCore.HStack -public typealias LazyHGrid = TokamakCore.LazyHGrid -public typealias LazyVGrid = TokamakCore.LazyVGrid -public typealias List = TokamakCore.List -public typealias ScrollView = TokamakCore.ScrollView -public typealias Section = TokamakCore.Section -public typealias Spacer = TokamakCore.Spacer -public typealias Text = TokamakCore.Text -public typealias VStack = TokamakCore.VStack -public typealias ZStack = TokamakCore.ZStack - -// MARK: Special Views - -public typealias View = TokamakCore.View -public typealias AnyView = TokamakCore.AnyView -public typealias EmptyView = TokamakCore.EmptyView - -// MARK: Misc - -// Note: This extension is required to support concatenation of `Text`. -extension Text { - public static func + (lhs: Self, rhs: Self) -> Self { - _concatenating(lhs: lhs, rhs: rhs) - } -} - -``` - -We’ve omitted any stateful `Views`, as well as property wrappers used to modify state. diff --git a/docs/Building a Renderer/4 Building the Target.md b/docs/Building a Renderer/4 Building the Target.md deleted file mode 100644 index c5a0be1f8..000000000 --- a/docs/Building a Renderer/4 Building the Target.md +++ /dev/null @@ -1,45 +0,0 @@ -# Building the `Target` - -If you recall, we defined a `Target` as: - -> the destination for rendered `Views` - -In `TokamakStaticHTML`, this would be a tag in an `HTML` file. A tag has several properties, -although we don’t need to worry about all of them. For now, we can consider a tag to have: - -- The HTML for the tag itself (outer HTML) -- Child tags (inner HTML) - -We can describe our target simply: - -```swift -public final class HTMLTarget: Target { - var html: AnyHTML - var children: [HTMLTarget] = [] - - init(_ view: V, - _ html: AnyHTML) { - self.html = html - super.init(view) - } -} -``` - -`AnyHTML` is from `TokamakDOM`, which you can declare as a dependency. The target stores the `View` -it hosts, the `HTML` that represents it, and its child elements. - -Lastly, we can also provide an HTML string representation of the target: - -```swift -extension HTMLTarget { - var outerHTML: String { - """ - <\(html.tag)\(html.attributes.isEmpty ? "" : " ")\ - \(html.attributes.map { #"\#($0)="\#($1)""# }.joined(separator: " "))>\ - \(html.innerHTML ?? "")\ - \(children.map(\.outerHTML).joined(separator: "\n"))\ - - """ - } -} -``` diff --git a/docs/Building a Renderer/5 Building the Renderer.md b/docs/Building a Renderer/5 Building the Renderer.md deleted file mode 100644 index aeb0e5fab..000000000 --- a/docs/Building a Renderer/5 Building the Renderer.md +++ /dev/null @@ -1,134 +0,0 @@ -# Building the `Renderer` - -Now that we have a `Target`, we can start the `Renderer`: - -```swift -public final class StaticHTMLRenderer: Renderer { - public private(set) var reconciler: StackReconciler? - var rootTarget: HTMLTarget - - public var html: String { - """ - - \(rootTarget.outerHTML) - - """ - } -} -``` - -We start by declaring the `StackReconciler`. It will handle the app, while our `Renderer` can focus -on mounting and un-mounting `Views`. - -```swift -... -public init(_ view: V) { - rootTarget = HTMLTarget(view, HTMLBody()) - reconciler = StackReconciler( - view: view, - target: rootTarget, - renderer: self, - environment: EnvironmentValues() - ) { closure in - fatalError("Stateful apps cannot be created with TokamakStaticHTML") - } -} -``` - -Next we declare an initializer that takes a `View` and builds a reconciler. The reconciler takes the -`View`, our root `Target` (in this case, `HTMLBody`), the renderer (`self`), and any default -`EnvironmentValues` we may need to setup. The closure at the end is the scheduler. It tells the -reconciler when it can update. In this case, we won’t need to update, so we can crash. - -`HTMLBody` is declared like so: - -```swift -struct HTMLBody: AnyHTML { - let tag: String = "body" - let innerHTML: String? = nil - let attributes: [String : String] = [:] - let listeners: [String : Listener] = [:] -} -``` - -## Mounting - -Now that we have a reconciler, we need to be able to mount the `HTMLTargets` it asks for. - -```swift -public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTarget? { - // 1. - guard let html = mapAnyView( - host.view, - transform: { (html: AnyHTML) in html } - ) else { - // 2. - if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil { - return parent - } - - return nil - } - - // 3. - let node = HTMLTarget(host.view, html) - parent.children.append(node) - return node -}} -``` - -1. We use the `mapAnyView` function to convert the `AnyView` passed in to `AnyHTML`, which can be - used with our `HTMLTarget`. -2. `ParentView` is a special type of `View` in Tokamak. It indicates that the view has no - representation itself, and is purely a container for children (e.g. `ForEach` or `Group`). -3. We create a new `HTMLTarget` for the view, assign it as a child of the parent, and return it. - -The other two functions required by the `Renderer` protocol can crash, as `TokamakStaticHTML` -doesn’t support state changes: - -```swift -public func update(target: HTMLTarget, with host: MountedHost) { - fatalError("Stateful apps cannot be created with TokamakStaticHTML") -} - -public func unmount( - target: HTMLTarget, - from parent: HTMLTarget, - with host: MountedHost, - completion: @escaping () -> () -) { - fatalError("Stateful apps cannot be created with TokamakStaticHTML") -} -``` - -If you are creating a `Renderer` that supports state changes, here’s a quick synopsis: - -- `func update` - Mutate the `target` to match the `host`. -- `func unmount` - Remove the `target` from the `parent`, and call `completion` once it has been - removed. - -Now that we can mount, let’s give it a try: - -```swift -struct ContentView : View { - var body: some View { - Text("Hello, world!") - } -} - -let renderer = StaticHTMLRenderer(ContentView()) -print(renderer.html) -``` - -This spits out: - -```html - - - Hello, world! - - -``` - -Congratulations 🎉 You successfully wrote a `Renderer`. We can’t wait to see what platforms you’ll -bring Tokamak to. diff --git a/docs/Building a Renderer/6 Providing platform-specific primitives.md b/docs/Building a Renderer/6 Providing platform-specific primitives.md deleted file mode 100644 index 165e21f26..000000000 --- a/docs/Building a Renderer/6 Providing platform-specific primitives.md +++ /dev/null @@ -1,61 +0,0 @@ -# Providing platform-specific primitives - -Primitive `Views`, such as `Text`, `Button`, `HStack`, etc. have a body type of `Never`. When the -`StackReconciler` goes to render these `Views`, it expects your `Renderer` to provide a body. - -This is done via the `ViewDeferredToRenderer` protocol. There we can provide a `View` that our -`Renderer` understands. For instance, `TokamakDOM` (and `TokamakStaticHTML` by extension) use the -`HTML` view. Let’s look at a simpler version of this view: - -```swift -protocol AnyHTML { - let tag: String - let attributes: [String:String] - let innerHTML: String -} - -struct HTML: View, AnyHTML { - let tag: String - let attributes: [String:String] - let innerHTML: String - var body: Never { - neverBody("HTML") - } -} -``` - -Here we define an `HTML` view to have a body type of `Never`, like other primitive `Views`. It also -conforms to `AnyHTML`, which allows our `Renderer` to access the attributes of the `HTML` without -worrying about the `associatedtypes` involved with `View`. - -## `ViewDeferredToRenderer` - -Now we can use `HTML` to override the body of the primitive `Views` provided by `TokamakCore`: - -```swift -extension Text: ViewDeferredToRenderer { - var deferredBody: AnyView { - AnyView(HTML("span", [:], _TextProxy(self).rawText)) - } -} -``` - -If you recall, our `Renderer` mapped the `AnyView` received from the reconciler to `AnyHTML`: - -```swift -// 1. -guard let html = mapAnyView( - host.view, - transform: { (html: AnyHTML) in html } -) else { ... } -``` - -Then we were able to access the properties of the HTML. - -## Proxies - -Proxies allow access to internal properties of views implemented by `TokamakCore`. For instance, to -access the storage of the `Text` view, we were required to use a `_TextProxy`. - -Proxies contain all of the properties of the primitive necessary to build your platform-specific -implementation. diff --git a/docs/RenderersGuide.md b/docs/RenderersGuide.md new file mode 100644 index 000000000..050e81801 --- /dev/null +++ b/docs/RenderersGuide.md @@ -0,0 +1,363 @@ +# `Renderers` in Tokamak + +**Author: [@carson-katri](https://github.com/carson-katri)** + +Tokamak is a flexible library. `TokamakCore` provides the SwiftUI API, which your `Renderer` can use +to construct a representation of `Views` that your platform understands. + +To explain the creation of `Renderers`, we’ll be creating a simple one: `TokamakStaticHTML` (which +you can find in the `Tokamak` repository). + +Before we create the `Renderer`, we need to understand the requirements of our platform: + +1. Stateful apps cannot be created. This simplifies the scope of our project, as we only have to + render once. However, if you are building a `Renderer` that supports state changes, the process + is largely the same. `TokamakCore`’s `StackReconciler` will let your `Renderer` know when a + `View` has to be redrawn. +2. HTML should be rendered. `TokamakDOM` provides HTML representations of many `Views`, so we can + utilize it. However, we will cover how to provide custom `View` bodies your `Renderer` can + understand, and when you are required to do so. + +And that’s it! In the next part we’ll go more in depth on `Renderers`. + +## Understanding `Renderers` + +So, what goes into a `Renderer`? + +1. A `Target` - Targets are the destination for rendered `Views`. For instance, on iOS this is + `UIView`, on macOS an `NSView`, and on the web we render to DOM nodes. +2. A `StackReconciler` - The reconciler does all the heavy lifting to understand the view tree. It + notifies your `Renderer` of what views need to be mounted/unmounted. +3. `func mountTarget`- This function is called when a new target instance should be created and + added to the parent (either as a subview or some other way, e.g. installed if it’s a layout + constraint). +4. `func update` - This function is called when an existing target instance should be updated (e.g. + when `State` changes). +5. `func unmount` - This function is called when an existing target instance should be unmounted: + removed from the parent and most likely destroyed. + +That’s it! Let’s get our project set up. + +## `TokamakStaticHTML` Setup + +Every `Renderer` can choose what `Views`, `ViewModifiers`, property wrappers, etc. are available to +use. A `Core.swift` file is used to re-export these symbols. For `TokamakStaticHTML`, we’ll use the +following `Core.swift` file: + +```swift +import TokamakCore + +// MARK: Environment & State + +public typealias Environment = TokamakCore.Environment + +// MARK: Modifiers & Styles + +public typealias ViewModifier = TokamakCore.ViewModifier +public typealias ModifiedContent = TokamakCore.ModifiedContent + +public typealias DefaultListStyle = TokamakCore.DefaultListStyle +public typealias PlainListStyle = TokamakCore.PlainListStyle +public typealias InsetListStyle = TokamakCore.InsetListStyle +public typealias GroupedListStyle = TokamakCore.GroupedListStyle +public typealias InsetGroupedListStyle = TokamakCore.InsetGroupedListStyle + +// MARK: Shapes + +public typealias Shape = TokamakCore.Shape + +public typealias Capsule = TokamakCore.Capsule +public typealias Circle = TokamakCore.Circle +public typealias Ellipse = TokamakCore.Ellipse +public typealias Path = TokamakCore.Path +public typealias Rectangle = TokamakCore.Rectangle +public typealias RoundedRectangle = TokamakCore.RoundedRectangle + +// MARK: Primitive values + +public typealias Color = TokamakCore.Color +public typealias Font = TokamakCore.Font + +public typealias CGAffineTransform = TokamakCore.CGAffineTransform +public typealias CGPoint = TokamakCore.CGPoint +public typealias CGRect = TokamakCore.CGRect +public typealias CGSize = TokamakCore.CGSize + +// MARK: Views + +public typealias Divider = TokamakCore.Divider +public typealias ForEach = TokamakCore.ForEach +public typealias GridItem = TokamakCore.GridItem +public typealias Group = TokamakCore.Group +public typealias HStack = TokamakCore.HStack +public typealias LazyHGrid = TokamakCore.LazyHGrid +public typealias LazyVGrid = TokamakCore.LazyVGrid +public typealias List = TokamakCore.List +public typealias ScrollView = TokamakCore.ScrollView +public typealias Section = TokamakCore.Section +public typealias Spacer = TokamakCore.Spacer +public typealias Text = TokamakCore.Text +public typealias VStack = TokamakCore.VStack +public typealias ZStack = TokamakCore.ZStack + +// MARK: Special Views + +public typealias View = TokamakCore.View +public typealias AnyView = TokamakCore.AnyView +public typealias EmptyView = TokamakCore.EmptyView + +// MARK: Misc + +// Note: This extension is required to support concatenation of `Text`. +extension Text { + public static func + (lhs: Self, rhs: Self) -> Self { + _concatenating(lhs: lhs, rhs: rhs) + } +} + +``` + +We’ve omitted any stateful `Views`, as well as property wrappers used to modify state. + +## Building the `Target` + +If you recall, we defined a `Target` as: + +> the destination for rendered `Views` + +In `TokamakStaticHTML`, this would be a tag in an `HTML` file. A tag has several properties, +although we don’t need to worry about all of them. For now, we can consider a tag to have: + +- The HTML for the tag itself (outer HTML) +- Child tags (inner HTML) + +We can describe our target simply: + +```swift +public final class HTMLTarget: Target { + var html: AnyHTML + var children: [HTMLTarget] = [] + + init(_ view: V, + _ html: AnyHTML) { + self.html = html + super.init(view) + } +} +``` + +`AnyHTML` type is coming from `TokamakDOM`, which you can declare as a dependency. The target stores +the `View` it hosts, the `HTML` that represents it, and its child elements. + +Lastly, we can also provide an HTML string representation of the target: + +```swift +extension HTMLTarget { + var outerHTML: String { + """ + <\(html.tag)\(html.attributes.isEmpty ? "" : " ")\ + \(html.attributes.map { #"\#($0)="\#($1)""# }.joined(separator: " "))>\ + \(html.innerHTML ?? "")\ + \(children.map(\.outerHTML).joined(separator: "\n"))\ + + """ + } +} +``` + +## Building the `Renderer` + +Now that we have a `Target`, we can start the `Renderer`: + +```swift +public final class StaticHTMLRenderer: Renderer { + public private(set) var reconciler: StackReconciler? + var rootTarget: HTMLTarget + + public var html: String { + """ + + \(rootTarget.outerHTML) + + """ + } +} +``` + +We start by declaring the `StackReconciler`. It will handle the app, while our `Renderer` can focus +on mounting and un-mounting `Views`. + +```swift +... +public init(_ view: V) { + rootTarget = HTMLTarget(view, HTMLBody()) + reconciler = StackReconciler( + view: view, + target: rootTarget, + renderer: self, + environment: EnvironmentValues() + ) { closure in + fatalError("Stateful apps cannot be created with TokamakStaticHTML") + } +} +``` + +Next we declare an initializer that takes a `View` and builds a reconciler. The reconciler takes the +`View`, our root `Target` (in this case, `HTMLBody`), the renderer (`self`), and any default +`EnvironmentValues` we may need to setup. The closure at the end is the scheduler. It tells the +reconciler when it can update. In this case, we won’t need to update, so we can crash. + +`HTMLBody` is declared like so: + +```swift +struct HTMLBody: AnyHTML { + let tag: String = "body" + let innerHTML: String? = nil + let attributes: [String : String] = [:] + let listeners: [String : Listener] = [:] +} +``` + +### Mounting + +Now that we have a reconciler, we need to be able to mount the `HTMLTargets` it asks for. + +```swift +public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTarget? { + // 1. + guard let html = mapAnyView( + host.view, + transform: { (html: AnyHTML) in html } + ) else { + // 2. + if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil { + return parent + } + + return nil + } + + // 3. + let node = HTMLTarget(host.view, html) + parent.children.append(node) + return node +}} +``` + +1. We use the `mapAnyView` function to convert the `AnyView` passed in to `AnyHTML`, which can be + used with our `HTMLTarget`. +2. `ParentView` is a special type of `View` in Tokamak. It indicates that the view has no + representation itself, and is purely a container for children (e.g. `ForEach` or `Group`). +3. We create a new `HTMLTarget` for the view, assign it as a child of the parent, and return it. + +The other two functions required by the `Renderer` protocol can crash, as `TokamakStaticHTML` +doesn’t support state changes: + +```swift +public func update(target: HTMLTarget, with host: MountedHost) { + fatalError("Stateful apps cannot be created with TokamakStaticHTML") +} + +public func unmount( + target: HTMLTarget, + from parent: HTMLTarget, + with host: MountedHost, + completion: @escaping () -> () +) { + fatalError("Stateful apps cannot be created with TokamakStaticHTML") +} +``` + +If you are creating a `Renderer` that supports state changes, here’s a quick synopsis: + +- `func update` - Mutate the `target` to match the `host`. +- `func unmount` - Remove the `target` from the `parent`, and call `completion` once it has been + removed. + +Now that we can mount, let’s give it a try: + +```swift +struct ContentView : View { + var body: some View { + Text("Hello, world!") + } +} + +let renderer = StaticHTMLRenderer(ContentView()) +print(renderer.html) +``` + +This spits out: + +```html + + + Hello, world! + + +``` + +Congratulations 🎉 You successfully wrote a `Renderer`. We can’t wait to see what platforms you’ll +bring Tokamak to. + +## Providing platform-specific primitives + +Primitive `Views`, such as `Text`, `Button`, `HStack`, etc. have a body type of `Never`. When the +`StackReconciler` goes to render these `Views`, it expects your `Renderer` to provide a body. + +This is done via the `ViewDeferredToRenderer` protocol. There we can provide a `View` that our +`Renderer` understands. For instance, `TokamakDOM` (and `TokamakStaticHTML` by extension) use the +`HTML` view. Let’s look at a simpler version of this view: + +```swift +protocol AnyHTML { + let tag: String + let attributes: [String:String] + let innerHTML: String +} + +struct HTML: View, AnyHTML { + let tag: String + let attributes: [String:String] + let innerHTML: String + var body: Never { + neverBody("HTML") + } +} +``` + +Here we define an `HTML` view to have a body type of `Never`, like other primitive `Views`. It also +conforms to `AnyHTML`, which allows our `Renderer` to access the attributes of the `HTML` without +worrying about the `associatedtypes` involved with `View`. + +### `ViewDeferredToRenderer` + +Now we can use `HTML` to override the body of the primitive `Views` provided by `TokamakCore`: + +```swift +extension Text: ViewDeferredToRenderer { + var deferredBody: AnyView { + AnyView(HTML("span", [:], _TextProxy(self).rawText)) + } +} +``` + +If you recall, our `Renderer` mapped the `AnyView` received from the reconciler to `AnyHTML`: + +```swift +// 1. +guard let html = mapAnyView( + host.view, + transform: { (html: AnyHTML) in html } +) else { ... } +``` + +Then we were able to access the properties of the HTML. + +### Proxies + +Proxies allow access to internal properties of views implemented by `TokamakCore`. For instance, to +access the storage of the `Text` view, we were required to use a `_TextProxy`. + +Proxies contain all of the properties of the primitive necessary to build your platform-specific +implementation.