The missing âï¸: SwiftWebUI
Beginning of the month Apple announced SwiftUI at the WWDC 2019. A single âcross platformâ, âdeclarativeâ framework used to build tvOS, macOS, watchOS and iOS UIs. SwiftWebUI is bringing that to the Web âï¸
Disclaimer: This is a toy project! Do not use for production. Use it to learn more about SwiftUI and its inner workings.
SwiftWebUI
So what exactly is SwiftWebUI? It allows you to write SwiftUI Views which display in a web browser:
import SwiftWebUI
struct MainPage: View {
@State var counter = 0
func countUp() { counter += 1 }
var body: some View {
VStack {
Text("ð¥ð #\(counter)")
.padding(.all)
.background(.green, cornerRadius: 12)
.foregroundColor(.white)
.tapAction(self.countUp)
}
}
}
Results in:
Unlike some other efforts this doesnât just render SwiftUI Views as HTML. It also sets up a connection between the browser and the code hosted in the Swift server, allowing for interaction - buttons, pickers, steppers, lists, navigation, you get it all!
In other words: SwiftWebUI is an implementation of (many but not all parts of) the SwiftUI API for the browser.
To repeat the Disclaimer: This is a toy project! Do not use for production. Use it to learn more about SwiftUI and its inner workings.
Learn once, use anywhere
The stated goal of SwiftUI is not âWrite once, run anywhereâ but âLearn once, use anywhereâ. Donât expect to be able to take a beautiful SwiftUI application for iOS, drop the code into a SwiftWebUI project and get it to render exactly the same in the browser. That is not the point.
The point is to be able to reuse the knoff-hoff and share it between different platforms. In this case, the Web âï¸
But letâs get down to the nitty gritty and write a simple SwiftWebUI application. In the spirit of âLearn once, use anywhereâ watch those two WWDC sessions first: Introducing SwiftUI and SwiftUI Essentials. We donât go that deep in this blog entry, but this one is recommended too (and the concepts are mostly supported in SwiftWebUI): Data Flow Through SwiftUI.
Requirements
Update 2019-07-08: There are three options to run SwiftWebUI:
macOS Catalina
One can use a macOS Catalina installation to run SwiftWebUI. Make sure that the Catalina version matches your Xcode 11 beta! (âSwift ABIâ ð¤¦ââï¸)
Fortunately it is really easy to install Catalina on a separate APFS volume. And an installation of Xcode 11 is required to get the new Swift 5.1 features SwiftUI makes heavy use of. Got that? Very well!
Why is Catalina required? SwiftUI makes use of new Swift 5.1 runtime features (e.g. opaque result types). Those features are not available in the Swift 5 runtime that ships with Mojave. (another reason is the use of Combine which is only available in Catalina, though that part could be fixed using OpenCombine)
tuxOS
SwiftWebUI now runs on Linux using
OpenCombine (also works without
that, but then some things donât work, e.g. NavigationView
).
A Swift 5.1 snapshot is required. We also provide a Docker image containing a 5.1 snapshot over here: helje5/swift.
Mojave
The Xcode 11beta iOS 13 simulators do run on Mojave. You might be able to run SwiftWebUI within an iOS app.
Getting Started with a First App
Creating a SwiftWebUI Project
Fire up Xcode 11, select âFile > New > Projectâ¦â or just press Cmd-Shift-N:
Select the âmacOS / Command Line Toolâ project template:
Give it some nice name, letâs go with âAvocadoToastâ:
Then we add SwiftWebUI as a Swift Package Manager dependency. The option is hidden in the âFile / Swift Packagesâ menu group:
Enter https://github.com/SwiftWebUI/SwiftWebUI.git
as the package URL:
Use âBranchâ master
option to always get the latest and greatest
(you can also use a revision or the develop
branch):
Finally add the SwiftWebUI
library to your tool target:
Thatâs it. You now have a tool project which can import SwiftWebUI
.
(Xcode might take a moment to fetch and build the dependencies.)
SwiftWebUI Hello World
Letâs get started w/ SwiftWebUI.
Open the main.swift
file and replace itâs content
with:
import SwiftWebUI
SwiftWebUI.serve(Text("Holy Cow!"))
Compile and run the app in Xcode, open Safari and hit
http://localhost:1337/
:
What is going on here: First the SwiftWebUI module is imported (donât accidentially import the macOS SwiftUI ð)
Then we call SwiftWebUI.serve
which either takes a a closure
returning a View, or just a straight View - as shown here: a
Text
View (aka a âUILabelâ which can show plain or formatted text).
Behind the scenes
Internally the
serve
function creates a very simple
SwiftNIO
HTTP server listening on port 1337.
When the browser hits that server, it creates a
session
and passes our (Text) View to that session.
Finally, from the View, SwiftWebUI creates a
âShadow DOMâ on the server,
renders that as HTML and sends the result to the browser.
That âShadow DOMâ (and a state object kept alongside) is
stored in the session.
This is a difference between a SwiftWebUI application and a watchOS or iOS SwiftUI app. A single SwiftWebUI application serves a bunch of users instead of just one.
Adding some Interaction
As a first step, letâs organize the code a little better.
Create a new Swift file in the project and call that
MainPage.swift
.
And add a simple SwiftUI View definition to it:
import SwiftWebUI
struct MainPage: View {
var body: some View {
Text("Holy Cow!")
}
}
Adjust the main.swift to serve our custom View:
SwiftWebUI.serve(MainPage())
We can now leave the main.swift
alone and do all our work
in our custom
View
.
Letâs add some interaction to it:
struct MainPage: View {
@State var counter = 3
func countUp() { counter += 1 }
var body: some View {
Text("Count is: \(counter)")
.tapAction(self.countUp)
}
}
Our
View
got a persistent
State
variable named counter
(not sure what this is? Have another look at
Introducing SwiftUI).
And a small function to bump the counter.
We then use the SwiftUI
tapAction
modifier to attach an event handler to our Text
.
Finally, we show the current value within the label:
Behind the scenes
How does that work? When the browser hit our endpoint,
SwiftWebUI created the session and our âShadow DOMâ
within that.
It then sent the HTML describing our View to the browser.
The tapAction
works by adding an onclick
handler to
the HTML.
SwiftWebUI also ships (tiny amounts, no big framework!)
of JavaScript to the browser which handles the click
and forwards that to our Swift server.
Then the usual SwiftUI magic kicks in.
SwiftWebUI correlates the click event with the event
handler in our âShadow DOMâ and invokes the countUp
function.
By modifying the counter
State
variable the function invalidates the rendering of our View.
SwiftWebUI kicks in, and diffes the changes in the
âShadow DOMâ.
Those changes are then sent back to the browser.
The âchangesâ are sent as a JSON array which our small JavaScript in the page can process. If a whole subtree changed (e.g. if a user navigated to a whole new View), a change can be a larger HTML snippet which is applied to
innerHTML
orouterHTML
.
But usually the changes are small things likeadd class
,set HTML attribute
and the likes (i.e. browser DOM modifications).
ð¥ð Avocado Toast
Excellent, the basics work. Letâs bring in more interactivity. The following is based on the âAvocado Toast Appâ used to demo SwiftUI in the SwiftUI Essentials talk. Didnât watch it yet? Maybe you should, it is about delicious toasts.
The HTML/CSS styling isnât quite perfect nor beautiful yet. As you can imagine we are not web designers and could use some help here. PRs are welcome!
Want to skip the details, watch a GIF of the app and download it on GitHub: ð¥ð.
The ð¥ð Order Form
The talk starts off with this (~6:00), which we can add to a new OrderForm.swift
file:
struct Order {
var includeSalt = false
var includeRedPepperFlakes = false
var quantity = 0
}
struct OrderForm: View {
@State private var order = Order()
func submitOrder() {}
var body: some View {
VStack {
Text("Avocado Toast").font(.title)
Toggle(isOn: $order.includeSalt) {
Text("Include Salt")
}
Toggle(isOn: $order.includeRedPepperFlakes) {
Text("Include Red Pepper Flakes")
}
Stepper(value: $order.quantity, in: 1...10) {
Text("Quantity: \(order.quantity)")
}
Button(action: submitOrder) {
Text("Order")
}
}
}
}
For testing direct SwiftWebUI.serve()
in main.swift
to the new
OrderForm
View.
This is what it looks like in the browser:
SemanticUI is used for styling some things in SwiftWebUI. It is not strictly required for the operation, it just accomplishes somewhat decent looking widgets.
Note: Only the CSS/fonts are used, not the JavaScript components.
Intermission: Some SwiftUI Layout
Around 16:00 in SwiftUI Essentials they are getting to SwiftUI layout and View modifier ordering:
var body: some View {
HStack {
Text("ð¥ð")
.background(.green, cornerRadius: 12)
.padding(.all)
Text(" => ")
Text("ð¥ð")
.padding(.all)
.background(.green, cornerRadius: 12)
}
}
Results in this, notice how the ordering of the modifiers is relevant:
SwiftWebUI tries to replicate common SwiftUI layouts, but doesnât fully succeed yet. After all it has to deal with the layout system the browser provides. Help wanted, flexbox experts welcome!
The ð¥ð Order History
Back to the app, the talk (~19:50) introduces the List View for showing an Avocado toast order history. This is how it looks on the Web:
The List
view walks over the array of completed orders
and creates a child View for each one (OrderCell
),
passing in the current item in the list.
This is the code we are using:
struct OrderHistory: View {
let previousOrders : [ CompletedOrder ]
var body: some View {
List(previousOrders) { order in
OrderCell(order: order)
}
}
}
struct OrderCell: View {
let order : CompletedOrder
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(order.summary)
Text(order.purchaseDate)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
if order.includeSalt {
SaltIcon()
}
else {}
if order.includeRedPepperFlakes {
RedPepperFlakesIcon()
}
else {}
}
}
}
struct SaltIcon: View {
let body = Text("ð§")
}
struct RedPepperFlakesIcon: View {
let body = Text("ð¶")
}
// Model
struct CompletedOrder: Identifiable {
var id : Int
var summary : String
var purchaseDate : String
var includeSalt = false
var includeRedPepperFlakes = false
}
The SwiftWebUI List View is really inefficient, it always renders the whole set of children. No cell reuse, no nothing ð There are various ways to deal with that in a web app, e.g. by using paging or more client side logic.
So you donât have to type down the sample data from the talk, we did that for you:
let previousOrders : [ CompletedOrder ] = [
.init(id: 1, summary: "Rye with Almond Butter", purchaseDate: "2019-05-30"),
.init(id: 2, summary: "Multi-Grain with Hummus", purchaseDate: "2019-06-02",
includeRedPepperFlakes: true),
.init(id: 3, summary: "Sourdough with Chutney", purchaseDate: "2019-06-08",
includeSalt: true, includeRedPepperFlakes: true),
.init(id: 4, summary: "Rye with Peanut Butter", purchaseDate: "2019-06-09"),
.init(id: 5, summary: "Wheat with Tapenade", purchaseDate: "2019-06-12"),
.init(id: 6, summary: "Sourdough with Vegemite", purchaseDate: "2019-06-14",
includeSalt: true),
.init(id: 7, summary: "Wheat with Féroce", purchaseDate: "2019-06-31"),
.init(id: 8, summary: "Rhy with Honey", purchaseDate: "2019-07-03"),
.init(id: 9, summary: "Multigrain Toast", purchaseDate: "2019-07-04",
includeSalt: true),
.init(id: 10, summary: "Sourdough with Chutney", purchaseDate: "2019-07-06")
]
The ð¥ð Spread Picker
The Picker control and how to use it w/ enumâs is demonstrated ~43:00. First the enums for the various toast options:
enum AvocadoStyle {
case sliced, mashed
}
enum BreadType: CaseIterable, Hashable, Identifiable {
case wheat, white, rhy
var name: String { return "\(self)".capitalized }
}
enum Spread: CaseIterable, Hashable, Identifiable {
case none, almondButter, peanutButter, honey
case almou, tapenade, hummus, mayonnaise
case kyopolou, adjvar, pindjur
case vegemite, chutney, cannedCheese, feroce
case kartoffelkase, tartarSauce
var name: String {
return "\(self)".map { $0.isUppercase ? " \($0)" : "\($0)" }
.joined().capitalized
}
}
We can add those to our Order
struct:
struct Order {
var includeSalt = false
var includeRedPepperFlakes = false
var quantity = 0
var avocadoStyle = AvocadoStyle.sliced
var spread = Spread.none
var breadType = BreadType.wheat
}
And then display them using the different Picker types. It is pretty neat how you can just loop over the enum values:
Form {
Section(header: Text("Avocado Toast").font(.title)) {
Picker(selection: $order.breadType, label: Text("Bread")) {
ForEach(BreadType.allCases) { breadType in
Text(breadType.name).tag(breadType)
}
}
.pickerStyle(.radioGroup)
Picker(selection: $order.avocadoStyle, label: Text("Avocado")) {
Text("Sliced").tag(AvocadoStyle.sliced)
Text("Mashed").tag(AvocadoStyle.mashed)
}
.pickerStyle(.radioGroup)
Picker(selection: $order.spread, label: Text("Spread")) {
ForEach(Spread.allCases) { spread in
Text(spread.name).tag(spread) // there is no .name?!
}
}
}
}
The result:
Again, this needs some CSS love to make it look better â¦
The ð¥ð âFinishedâ App
No we diverge a little from the original, and do not really finish it either. It doesnât look that great yet, but it is a demo after all ð
The finished app is available on GitHub: AvocadoToast.
HTML and SemanticUI
The
UIViewRepresentable
peer in SwiftWebUI is emitting raw HTML.
Two variants are provided, the HTML
outputs a String as-is,
or by HTML escaping the contents:
struct MyHTMLView: View {
var body: some View {
VStack {
HTML("<blink>Blinken Lights</blink>")
HTML("42 > 1337", escape: true)
}
}
}
Using this primitive you can essentially build any HTML you want.
A little higher level and even used internally is
HTMLContainer
.
For example this is the implementation of our Stepper
control:
var body: some View {
HStack {
HTMLContainer(classes: [ "ui", "icon", "buttons", "small" ]) {
Button(self.decrement) {
HTMLContainer("i", classes: [ "minus", "icon" ], body: {EmptyView()})
}
Button(self.increment) {
HTMLContainer("i", classes: [ "plus", "icon" ], body: {EmptyView()})
}
}
label
}
}
The HTMLContainer
is âreactiveâ, i.e. it will emit regular DOM
changes if classes, styles or attributes change (instead of
re-rendering the whole thing)
SemanticUI
SwiftWebUI also comes w/ a few SemanticUI controls pre-setup:
VStack {
SUILabel(Image(systemName: "mail")) { Text("42") }
HStack {
SUILabel(Image(...)) { Text("Joe") } ...
}
HStack {
SUILabel(Image(...)) { Text("Joe") } ...
}
HStack {
SUILabel(Image(...), Color("blue"),
detail: Text("Friend"))
{
Text("Veronika")
} ...
}
}
⦠renders such:
Note that SwiftWebUI also supports some SFSymbols image names (via
Image(systemName:)
). Those are backed by SemanticUIâs support for Font Awesome.
There is also SUISegment
, SUIFlag
andSUICard
:
SUICards {
SUICard(Image.unsplash(size: UXSize(width: 200, height: 200),
"Zebra", "Animal"),
Text("Some Zebra"),
meta: Text("Roaming the world since 1976"))
{
Text("A striped animal.")
}
SUICard(Image.unsplash(size: UXSize(width: 200, height: 200),
"Cow", "Animal"),
Text("Some Cow"),
meta: Text("Milk it"))
{
Text("Holy cow!.")
}
}
⦠renders those:
It is very easy and a lot of fun to add such Views.
One can quickly compose pretty complex and good looking layouts
using WOComponent's SwiftUI Views.
Image.unsplash
constructs image queries against the Unsplash API running athttp://source.unsplash.com
. Just give it some query terms, the size you want and optional scopes.
Note: That specific Unsplash service seems to be a little slow and unreliable sometimes.
Summary
Thatâs it for our demo. We hope you like it! But to again repeat the Disclaimer: This is a toy project! Do not use for production. Use it to learn more about SwiftUI and its inner workings.
We think it is a nice toy and likely a valuable tool to learn more about the inner workings of SwiftUI.
Abitrary Technology Notes
Just a set of notes on various aspects of the technology. Can be skipped, not that interesting ð
Issues
There are a whole lot of issues, some are filed on GitHub: Issues. Feel free to file more.
Quite a few HTML layout things (e.g. ScrollView
doesnât always
scroll),
but also some open ends like Shapes (which might be easy to do
via SVG &| CSS).
Oh, and the single case If-ViewBuilder doesnât work. No idea why:
var body: some View {
VStack {
if a > b {
SomeView()
}
// currently need an empty else: `else {}` to make it compile.
}
}
Halp wanted! PRs welcome!
Vs the original SwiftUI
This implementation is pretty simple and inefficient. The real thing has to deal with a much higher rate of state modifying events, does all the animation things at 60Hz frame rates, etc etc.
Ourâs focuses on getting the basic operations right, e.g. how States and Bindings work, how and when Views get updated, and so on. Quite possible that the implementation does some things incorrectly, Apple forgot to send us the originalâs sources as part of Xcode 11.
WebSockets
We currently use AJAX to connect the browser to the server. Using WebSockets would have multiple advantages:
- guaranteed event ordering (AJAX requests can arrive out of sync)
- non-user initiated, server side DOM updates (timers, push)
- session timeout indicator
It would make a chat client demo trivial.
Adding WebSockets is actually really easy because events are already sent as JSON. We just need the client and server side shims. All this is already tried out in swift-nio-irc-webclient and just needs to be ported over.
SPA
The current incarnation of SwiftWebUI is an SPA (single page application) attached to a stateful backend server.
There are other ways to do this, e.g. by persisting the tree states while the user traverses through an application via regular links. Aka WebObjects ;-)
In general it would be nice to have better control on DOM ID generation,
link generation, routing and more.
Similar to what SwiftObjects provides.
But in the end a user would have to give up a lot of the
âLearn once, use anywhereâ
since SwiftUI action handlers often are built around the fact that
those capture arbitrary state.
Weâll see what Swift based server side frameworks come up with ð½
WASM
The whole thing would become more useful once we get proper Swift WASM. Go WASM!
WebIDs
Some SwiftUI Views like ForEach
require Identifiable
objects, where
the id
can be any Hashable
.
This doesnât play too well w/ the DOM, because we need string based IDs
to identify the nodes.
This is worked around by mapping IDs to strings in a global map. Which
is technically unbounded (a particular issue w/ class references).
Summary: For web code it is better to identify items using strings or ints.
Form
The Form could use a lot more love: Issue.
SemanticUI has some good form layouts, we should probably rewrite the child trees to those. TBD.
WebObjects 6 for Swift
Took a while to make it click, but:
SwiftUI summarised for 40s+ people. pic.twitter.com/6cflN0OFon
— Helge Heà (@helje5) June 7, 2019
With SwiftUI Apple indeed gave us a âSwift-styleâ WebObjects 6!
Next: Direct To Web and some Swiftâified EOF (aka CoreData aka ZeeQL).
Links
- SwiftWebUI on GitHub
- SwiftUI
- Introducing SwiftUI (204)
- SwiftUI Essentials (216)
- Data Flow Through SwiftUI (226)
- SwiftUI Framework API
- SwiftObjects
- SemanticUI
- SwiftNIO
Contact
Hey, we hope you liked the article and we love feedback!
Twitter, any of those:
@helje5,
@ar_institute.
Email: [email protected].
Slack: Find us on SwiftDE, swift-server, noze, ios-developers.
Want to support my work? Buy an app! You donât have to use it! ð