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
This commit is contained in:
parent
e11effdd8c
commit
2a49b7808b
85
README.md
85
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.
|
||||
[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/).
|
||||
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":
|
||||
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.
|
||||
|
||||
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.
|
||||
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":
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### 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.
|
||||
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),
|
||||
|
|
|
@ -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`.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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<V: View>(_ 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"))\
|
||||
</\(html.tag)>
|
||||
"""
|
||||
}
|
||||
}
|
||||
```
|
|
@ -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<StaticHTMLRenderer>?
|
||||
var rootTarget: HTMLTarget
|
||||
|
||||
public var html: String {
|
||||
"""
|
||||
<html>
|
||||
\(rootTarget.outerHTML)
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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<V: View>(_ 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
|
||||
<html>
|
||||
<body>
|
||||
<span style="...">Hello, world!</span>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Congratulations 🎉 You successfully wrote a `Renderer`. We can’t wait to see what platforms you’ll
|
||||
bring Tokamak to.
|
|
@ -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.
|
|
@ -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<V: View>(_ 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"))\
|
||||
</\(html.tag)>
|
||||
"""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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<StaticHTMLRenderer>?
|
||||
var rootTarget: HTMLTarget
|
||||
|
||||
public var html: String {
|
||||
"""
|
||||
<html>
|
||||
\(rootTarget.outerHTML)
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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<V: View>(_ 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
|
||||
<html>
|
||||
<body>
|
||||
<span style="...">Hello, world!</span>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
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.
|
Loading…
Reference in New Issue