Blog
by Carson Katri

Deploying SwiftUI on the Web

I recently released a web version of a SwiftUI app. However, instead of building out a separate webapp (in something like React), I brought my SwiftUI code onto the web using SwiftWasm.

The App

The app I ported is a game designed for SharePlay called Shhh!. The concept should be familiar if you’ve played any social deduction games like Mafia or SpyFall before. Essentially, all but one player know a secret location. Their goal is to keep this hidden from that spy, while also deducing who the spy is through a series of questions.

The spy was defeated!The spy was defeated!

The Problem

I wanted to make sure that the game could be played by as many people as possible, even though it is mainly intended to be played over FaceTime. Because Apple introduced the web version of FaceTime, there was the very real possibility of players being on a call with Android/Windows users as well. To accommodate for this, I wanted to launch a web version alongside the app.

The Solution

Developing and maintaining a separate web app would’ve been too time consuming if I wanted to finish the game by the time SharePlay launched. So, I decided to reuse the majority of my codebase on the web using Tokamak, a SwiftUI-compatible framework for building browser apps with WebAssembly.

The spy was defeated (from the web)!The spy was defeated (from the web)!

So, how did it go?

The website is almost entirely written in Swift! 🎉

The landing page uses TokamakStaticHTML to generate its markup, and when you tap “Join a Game” you are taken to the SwiftWasm web app.

To make this work, the app was split into a few different pieces:

  1. ShhhShared — Swift package with shared code such as models, game logic, UI, etc.
  2. Shhh — The Xcode project for the iOS and macOS apps
  3. ShhhWeb — Swift package for the Tokamak web app

The entirety of the game logic and UI is contained in ShhhShared, with Shhh and ShhhWeb providing only pieces of UI and extra bits required for their platforms. For instance, they contain their own code for interfacing with Firebase, Shhh using the Swift package, and ShhhWeb using the JavaScript library (with a slim wrapper for Swift using JavaScriptKit). Shhh also has a Multipeer game backend for playing local games, and, of course, the SharePlay backend when playing with only other iOS 15.1 users. When you generate a web join code in the app from a local game or SharePlay, it is moved to Firebase so the web app can access it.

What Works

The app successfully runs on the web with the majority of code shared between platforms, so anyone with a modern browser can play. I have around 40 #if os(WASI) checks in the project, which isn’t bad considering I have around 20 #if os(macOS)/os(iOS) checks.

You may need to keep a version of Xcode 13.0 around, as I do face some issues with building SwiftWasm projects with 13.1. Just xcode-select --switch /Applications/Xcode-13_0.app when you are working on the web side.

Hot reload with Carton worked great during development, and I was able to use carton bundle to easily package up my app and deploy with Netlify.

The resulting .wasm file is definitely larger than a JavaScript app would typically be. Hopefully this can be reduced in the future. I used a --custom-index-page to show a loading screen while the file is downloading and the app is starting up.

What Doesn’t Work (Yet)

While web and native players have very similar experiences, some trade-offs had to be made.

Performance

Some complex UI elements needed to be simplified so the web app would run fast enough. For instance, the location list on native uses Canvas to create custom animated graphics for each location. The web version does not have any animated graphics here, and is instead a list of toggle-able buttons.

A screenshot of the location list on nativeA screenshot of the location list on the web

The web app still has animated graphics for every location, but many of them are simpler than their native counterpart, and they're only shown on the main game screen.

Web version of the Cruise Ship graphicWeb version of the Cruise Ship graphic

SF Symbols

Tokamak understandably has no support for SF Symbols. I was able to work around this by extending Image to have a system name initializer that redirects to a .png with the same name:

extension Image {
  init(systemName: String) {
    self.init("\(systemName).png")
  }
}

Then I can just supply a custom image for each symbol I use in the app.

Accessibility

Tokamak does not yet have support for any of SwiftUI’s accessibility modifiers. I was able to stub them out with inert modifiers, but having a properly accessible web app would be much better.

Layout Issues

Layout on the web is notoriously hard, and Tokamak does have issues with certain layouts. Thankfully, we can dip into HTML/CSS when needed. I used the HTML view to manually work around some of the layout problems:

#if os(WASI)
HTML("div", ["style": "some custom styling"]) {
    content
}
#else
content
#endif

In other cases, I opted to just simplify the UI:

A screenshot of voting for a player on nativeA screenshot of voting for a player on the web

There's clearly room for improvement, but the core functionality is still present, and the site will improve as Tokamak's layout approaches SwiftUI's.

Missing Views & Modifiers

Here’s a list of everything I built custom replacements for until they are added to Tokamak. Some of these additions are simply inert modifiers added to get it building.

An "inert" modifier looks like this:

#if os(WASI)
public extension View {
  func symbolRenderingMode(_: SymbolRenderingMode) -> some View {
    self
  }
}
#endif
  1. alert - the view it's attached to is replaced with the alert content when presented. An official implementation would probably use the JavaScript alert.
  2. allowsHitTesting - inert
  3. All the accessibility modifiers, as mentioned
  4. contentShape - inert
  5. disabled - just removes the element
  6. fixedSize - inert
  7. ignoresSafeArea - inert
  8. mask - inert
  9. onChange - inert
  10. safeAreaInset - defers to VStack + HStack
  11. symbolRenderingMode - inert
  12. symbolVariant - inert
  13. TabView (tabItem) - working implementation (I will make a PR after some polishing)

Not too bad overall, especially considering how many didn’t absolutely need an implementation.

Recommendations

If you're developing a SwiftUI experience that you want more people to use, give Tokamak a try! See how far you get before you hit a missing piece, then drop in an inert modifier, or submit an issue. It's pretty fun to see your app running in the browser.

Thanks for reading this quick breakdown!

Generated using
Publish
and
Tokamak
RSS Feed