# `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.