Tokamak
[Looking for active maintainers] SwiftUI-compatible framework for building browser apps with WebAssembly and native apps for other platforms
Install / Use
/learn @TokamakUI/TokamakREADME
SwiftUI-compatible framework for building browser apps with WebAssembly
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), 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 and
PRs to get more details about the current status, or
create a new issue 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), so pull requests are very welcome!
Don't forget to check the "Contributing"
section first.
If you'd like to participate in the growing SwiftWasm community, you're
also very welcome to join our Discord server, or the #webassembly
channel in the SwiftPM Slack.
Example code
Tokamak API attempts to resemble SwiftUI API as much as possible. The main difference is
that you use import TokamakShim instead of import SwiftUI in your files. The former makes
your views compatible with Apple platforms, as well as platforms supported by Tokamak (currently
only WebAssembly/WASI with more coming in the future):
import TokamakShim
struct Counter: View {
@State var count: Int
let limit: Int
var body: some View {
if count < limit {
VStack {
Button("Increment") { count += 1 }
Text("\(count)")
}
.onAppear { print("Counter.VStack onAppear") }
.onDisappear { print("Counter.VStack onDisappear") }
} else {
VStack { Text("Limit exceeded") }
}
}
}
@main
struct CounterApp: App {
var body: some Scene {
WindowGroup("Counter Demo") {
Counter(count: 5, limit: 15)
}
}
}
Arbitrary HTML
With the HTML view you can also render any HTML you want, including inline SVG:
struct SVGCircle: View {
var body: some View {
HTML("svg", ["width": "100", "height": "100"]) {
HTML("circle", [
"cx": "50", "cy": "50", "r": "40",
"stroke": "green", "stroke-width": "4", "fill": "yellow",
])
}
}
}
HTML doesn't support event listeners, and is declared in the TokamakStaticHTML module, which TokamakDOM re-exports. The benefit of HTML is that you can use it for static rendering in libraries like TokamakVapor and TokamakPublish.
Another option is the DynamicHTML view provided by the TokamakDOM module, which has a listeners property with a corresponding initializer parameter. You can pass closures that can handle onclick, onmouseover and other DOM events for you in the listeners dictionary. Check out MDN docs for the full list.
An example of mouse events handling with DynamicHTML would look like this:
struct MouseEventsView: View {
@State var position: CGPoint = .zero
@State var isMouseButtonDown: Bool = false
var body: some View {
DynamicHTML(
"div",
["style": "width: 200px; height: 200px; background-color: red;"],
listeners: [
"mousemove": { event in
guard
let x = event.offsetX.jsValue.number,
let y = event.offsetY.jsValue.number
else { return }
position = CGPoint(x: x, y: y)
},
"mousedown": { _ in isMouseButtonDown = true },
"mouseup": { _ in isMouseButtonDown = false },
]
) {
Text("position is \(position), is mouse button down? \(isMouseButtonDown)")
}
}
}
Arbitrary styles and scripts
While JavaScriptKit is a great option for occasional interactions with JavaScript,
sometimes you need to inject arbitrary scripts or styles, which can be done through direct
DOM access:
import JavaScriptKit
let document = JSObject.global.document
let script = document.createElement("script")
script.setAttribute("src", "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js")
document.head.appendChild(script)
_ = document.head.insertAdjacentHTML("beforeend", #"""
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
"""#)
This way both Semantic UI styles and moment.js localized date formatting (or any arbitrary style/script/font added that way) are available in your app.
Fiber renderers
A new reconciler modeled after React's Fiber reconciler is optionally available. It can provide faster updates and allow for larger View hierarchies. It also includes layout steps that can match SwiftUI layouts closer than CSS approximations.
You can specify which reconciler to use in your App's configuration:
struct CounterApp: App {
static let _configuration: _AppConfiguration = .init(
// Specify `useDynamicLayout` to enable the layout steps in place of CSS approximations.
reconciler: .fiber(useDynamicLayout: true)
)
var body: some Scene {
WindowGroup("Counter Demo") {
Counter(count: 5, limit: 15)
}
}
}
Note: Not all
Views andViewModifiers are supported by Fiber renderers yet.
Requirements
For app developers
- macOS 11 and Xcode 13.2 or later when using VS Code. macOS 12 and Xcode 13.3 or later are recommended if you'd like to use Xcode for auto-completion, or when developing multi-platform apps that target WebAssembly and macOS at the same time.
- Swift 5.6 or later and Ubuntu 18.04/20.04 if you'd like to use Linux. Other Linux distributions are currently not supported.
carton0.15.x (carton is our build tool, see the "Getting started" section for installation steps)
For users of apps depending on Tokamak
Any recent browser that supports WebAssembly and required JavaScript features should work, which currently includes:
- Edge 84+
- Firefox 79+
- Chrome 84+
- Desktop Safari 14.1+
- Mobile Safari 14.8+
If you need to support older browser versions, you'll have to build with
JAVASCRIPTKIT_WITHOUT_WEAKREFS flag, passing -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS flags
when compiling. This should lower browser requirements to these versions:
- Edge 16+
- Firefox 61+
- Chrome 66+
- (Mobile) Safari 12+
Not all of these versions are tested on regular basis though, compatibility reports are very welcome!
Getting started
Tokamak relies on carton as a primary build tool. As a part of these steps
you'll install carton via Homebrew on macOS (unfortunately you'll have to build
it manually on Linux). Assuming you already have Homebrew installed, you can create a new Tokamak
app by following these steps:
- Install
carton:
brew install swiftwasm/tap/carton
If you had carton installed before this, make sure you have version 0.15.0 or greater:
carton --version
- Create a directory for your project and make it current:
mkdir TokamakApp && cd TokamakApp
- Initialize the project from a template with
carton:
carton init --template tokamak
- Build the project and start the development server,
carton devcan be kept running during development:
carton dev
- Open http://127.0.0.1:8080/ in your browser to see the app
running. You can edit the app source code in your favorite editor and save it,
cartonwill immediately rebuild the app and reload all browser tabs that have the app open.
You can also clone this repository and run carton dev --product TokamakDemo in its root
directory. This will build the demo app that shows almost all of the currently implemented APIs.
If you have any questions, pleaes check out the FAQ document, and/or join the #tokamak channel on the SwiftWasm Discord server.
Security
By default, the DOM renderer will escape HTML control characters in Text views. If you wish
to override this functionality, you can use the _domTextSanitizer modifier:
Text("<font color='red'>Unsanitized Text</font>")
._domTextSanitizer(Sanitizers.HTML.insecure)
You can also use custom sanitizers; the argument to _domTextSanitizer is simply a
String -> String closure. If _domTextSanitizer is applied to a non-Text view,
it will apply to all Text in subviews, unless overridden.
If you use user-generated or otherwise unsafe strings elsewhere, make sure to properly sanitize them yourself.
Troubleshooting
unable to find utility "xctest" error when building
This error can only happen on macOS, so make sure you have Xco
