Cross-Platform Swift: WebAssembly

Episode #291 • Aug 19, 2024 • Free Episode

We are going to take a Swift feature into the browser. We will set up a WebAssembly application from scratch, show how to run and debug it, and even set up some basic UI. And then we will integrate our existing model into it, all powered by the magic of Swift’s Observation framework.

Previous episode
Cross-Platform Swift: WebAssembly
Next episode
FreeThis episode is free for everyone.

Subscribe to Point-Free

Access all past and future episodes when you become a subscriber.

See plans and pricing

Already a subscriber? Log in

Important

In order to run the code in this episode you must be on beta 4 of Xcode 16. The version of Swift shipped in newer versions of Xcode unfortunately have a bug that needs to be fixed.

Introduction

Stephen

So this is all pretty cool. We have now shown a 3rd kind of view paradigm for which Swift’s amazing observation tools and our powerful navigation tools can be used to greatly simplify development.

We hope this helps our viewers see that the concepts that we have been talking about for the past many weeks was not solely about UIKit. No, the ideas we have been describing have a far deeper meaning with regards to how one builds apps and models their domains. With a little bit of upfront work we are able to build the core logic of our features in isolation, and then decide how we want to interface with the model in the view. Whether that be SwiftUI, UIKit, a 3rd party framework like Epoxy, or even some combination of view paradigms.

Brandon

But now it’s time to really drive home the point we are trying to make. We are now going to embark on making a cross platform version of our humble counter feature. We are going to make a version that runs in the browser, and still powered by the exact same model that we have been using in SwiftUI, UIKit and Epoxy.

We are going to do this by using something called WebAssembly, or Wasm for short. It is a binary format that can be embedded directly in a web browser, and other platforms, that allows one to run any language in the browser, not just JavaScript. Many languages support Wasm, including Python, Ruby, Rust, and C++, and even Swift has experimental support for Wasm.

This means we should be able to run our previously built model right in the browser, ideally without any changes whatsoever, and then the real work will be connecting our model to the browser’s DOM. This will be reminiscent of how we built our UIKit views, where we needed to explicitly create labels, text fields and buttons, hook those things up to the model, and observe changes in the model to update the UI. This is all possible even in a web app.

Sounds too good to be true, so let’s get started.

Cross-platform development

First there’s a little bit of prep work to perform to get a project ready for building a Wasm app. We are going to use Xcode to do this just because it’s what all of our viewers already have available to them, but in reality it tends to be a little easier to use something like VS Code for this kind of development.

And we are going to even embed the Wasm app right in our existing Xcode project that has our SwiftUI, UIKit and Epoxy version of the app. To do this we are going to create a new Swift package called Counter, and embed it directly in our existing Xcode project.

We are also going to add an executable target called “WasmApp”.

.executableTarget(name: "WasmApp"),

And we need at least one file in the source directory, which will act as the entry point into the application:

@main
struct App {
  static func main() {
  }
}

There are a few dependencies that we will need to add to build a Wasm app. First we will add a package called “carton”, which helps us build and run Wasm apps in Swift:

dependencies: [
  .package(url: "https://github.com/swiftwasm/carton", from: "1.0.0"),
],

This package comes with Swift plugins that allow you to bundle, test and develop Wasm apps in Swift, which we can even see by going to the repo for carton:

.plugin(name: "CartonBundlePlugin", targets: ["CartonBundlePlugin"]),
.plugin(name: "CartonTestPlugin", targets: ["CartonTestPlugin"]),
.plugin(name: "CartonDevPlugin", targets: ["CartonDevPlugin"]),

In order to run SwiftWasm at all we need a particular snapshot of the Swift compiler. We can’t just use the one bundled with Xcode. Luckily the carton plugin can handle all of this for us.

We just need to create a new file at the root of the project called “.swift-version”, and put the identifier of the Swift snapshot we want to use. The most current one at the time of recording this episode is:

wasm-DEVELOPMENT-SNAPSHOT-2024-07-16-a

…but you may want to try newer ones in case some of the issues we will encounter along the way have been fixed.

With that done we can simply run:

swift run carton dev

…on the command line, and we will see it do a bunch of work for us:

$ swift run carton dev
Fetching https://github.com/swiftwasm/carton from cache
Fetched https://github.com/swiftwasm/carton from cache (0.54s)
Computing version for https://github.com/swiftwasm/carton
Computed https://github.com/swiftwasm/carton at 1.1.2 (0.49s)
Fetching https://github.com/swiftwasm/WasmTransformer from cache
Fetching https://github.com/apple/swift-argument-parser.git from cache
Fetching https://github.com/apple/swift-log.git from cache
Fetching https://github.com/apple/swift-nio.git from cache
Fetched https://github.com/swiftwasm/WasmTransformer from cache (1.52s)
Fetched https://github.com/apple/swift-argument-parser.git from cache (1.57s)
Fetched https://github.com/apple/swift-log.git from cache (1.64s)
Fetched https://github.com/apple/swift-nio.git from cache (1.67s)
Computed https://github.com/swiftwasm/carton at 1.1.2 (0.00s)
Computing version for https://github.com/swiftwasm/WasmTransformer
Computed https://github.com/swiftwasm/WasmTransformer at 0.5.0 (0.66s)
Computing version for https://github.com/apple/swift-argument-parser.git
Computed https://github.com/apple/swift-argument-parser.git at 1.3.1 (0.48s)
Computing version for https://github.com/apple/swift-log.git
Computed https://github.com/apple/swift-log.git at 1.6.1 (0.46s)
Computing version for https://github.com/apple/swift-nio.git
Computed https://github.com/apple/swift-nio.git at 2.68.0 (0.54s)
Fetching https://github.com/apple/swift-system.git from cache
Fetching https://github.com/apple/swift-collections.git from cache
Fetching https://github.com/apple/swift-atomics.git from cache
Fetched https://github.com/apple/swift-atomics.git from cache (0.45s)
Fetched https://github.com/apple/swift-system.git from cache (0.52s)
Fetched https://github.com/apple/swift-collections.git from cache (0.65s)
Computing version for https://github.com/apple/swift-atomics.git
Computed https://github.com/apple/swift-atomics.git at 1.2.0 (0.48s)
Computing version for https://github.com/apple/swift-system.git
Computed https://github.com/apple/swift-system.git at 1.3.1 (0.50s)
Computing version for https://github.com/apple/swift-collections.git
Computed https://github.com/apple/swift-collections.git at 1.1.2 (0.70s)
Creating working copy for https://github.com/apple/swift-system.git
Working copy of https://github.com/apple/swift-system.git resolved at 1.3.1
warning: WasmDemo/.build/repositories/swift-argument-parser-54a11a8d is not valid git repository for 'https://github.com/apple/swift-argument-parser.git', will fetch again.
Fetching https://github.com/apple/swift-argument-parser.git from cache
Fetched https://github.com/apple/swift-argument-parser.git from cache (0.24s)
warning: WasmDemo/.build/repositories/swift-argument-parser-54a11a8d is not valid git repository for 'https://github.com/apple/swift-argument-parser.git', will fetch again.
Fetching https://github.com/apple/swift-argument-parser.git from cache
Fetched https://github.com/apple/swift-argument-parser.git from cache (0.23s)
Creating working copy for https://github.com/apple/swift-argument-parser.git
Working copy of https://github.com/apple/swift-argument-parser.git resolved at 1.3.1
Creating working copy for https://github.com/apple/swift-atomics.git
Working copy of https://github.com/apple/swift-atomics.git resolved at 1.2.0
warning: WasmDemo/.build/repositories/swift-collections-9a58d5cf is not valid git repository for 'https://github.com/apple/swift-collections.git', will fetch again.
Fetching https://github.com/apple/swift-collections.git from cache
Fetched https://github.com/apple/swift-collections.git from cache (0.27s)
warning: WasmDemo/.build/repositories/swift-collections-9a58d5cf is not valid git repository for 'https://github.com/apple/swift-collections.git', will fetch again.
Fetching https://github.com/apple/swift-collections.git from cache
Fetched https://github.com/apple/swift-collections.git from cache (0.24s)
Creating working copy for https://github.com/apple/swift-collections.git
Working copy of https://github.com/apple/swift-collections.git resolved at 1.1.2
Creating working copy for https://github.com/swiftwasm/carton
Working copy of https://github.com/swiftwasm/carton resolved at 1.1.2
warning: WasmDemo/.build/repositories/swift-log-ba8887eb is not valid git repository for 'https://github.com/apple/swift-log.git', will fetch again.
Fetching https://github.com/apple/swift-log.git from cache
Fetched https://github.com/apple/swift-log.git from cache (0.10s)
warning: WasmDemo/.build/repositories/swift-log-ba8887eb is not valid git repository for 'https://github.com/apple/swift-log.git', will fetch again.
Fetching https://github.com/apple/swift-log.git from cache
Fetched https://github.com/apple/swift-log.git from cache (0.10s)
Creating working copy for https://github.com/apple/swift-log.git
Working copy of https://github.com/apple/swift-log.git resolved at 1.6.1
Creating working copy for https://github.com/swiftwasm/WasmTransformer
Working copy of https://github.com/swiftwasm/WasmTransformer resolved at 0.5.0
Creating working copy for https://github.com/apple/swift-nio.git
Working copy of https://github.com/apple/swift-nio.git resolved at 2.68.0
Building for debugging...
[58/58] Applying carton
Build of product 'carton' complete! (7.26s)
- checking Swift compiler path: ~/.carton/sdk/wasm-6.0-SNAPSHOT-2024-06-19-a/usr/bin/swift
- checking Swift compiler path: ~/.swiftenv/versions/wasm-6.0-SNAPSHOT-2024-06-19-a/usr/bin/swift
- checking Swift compiler path: ~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift
- checking Swift compiler path: /Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift
Fetching release assets from https://api.github.com/repos/swiftwasm/swift/releases/tags/swift-wasm-6.0-SNAPSHOT-2024-06-19-a
Response contained body, parsing it now...
Response succesfully parsed, choosing from this number of assets: 7
Local installation of Swift version wasm-6.0-SNAPSHOT-2024-06-19-a not found
Swift toolchain/SDK download URL: https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0-SNAPSHOT-2024-06-19-a/swift-wasm-6.0-SNAPSHOT-2024-06-19-a-macos_arm64.pkg
Archive size is 1468 MB
                            Downloading the archive
100% [===========================================================]
saving to ~/.carton/sdk/wasm-6.0-SNAPSHOT-2024-06-19-a.pkg
Download completed successfully
Unpacking the archive: installer -target CurrentUserHomeDirectory -pkg ~/.carton/sdk/wasm-6.0-SNAPSHOT-2024-06-19-a.pkg
Running...
installer -target CurrentUserHomeDirectory -pkg ~/.carton/sdk/wasm-6.0-SNAPSHOT-2024-06-19-a.pkg
installer: Package name is wasm-6.0-SNAPSHOT-2024-06-19-a
installer: Installing at base path ~
installer: The install was successful.
`installer` process finished successfully
To avoid issues with the snapshot, the toolchain will be re-signed.
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swiftc-legacy-driver"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swiftc-legacy-driver: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-api-extract"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-api-extract: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-build-tool"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-build-tool: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-symbolgraph-extract"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-symbolgraph-extract: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/llvm-cov"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/llvm-cov: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-demangle"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-demangle: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/lld"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/lld: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-format"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-format: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clangd"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clangd: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sourcekit-lsp"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sourcekit-lsp: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/lldb"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/lldb: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-package-collection"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-package-collection: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-legacy-driver"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-legacy-driver: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-api-dump.py"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-frontend"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-frontend: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/ld.lld"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/ld.lld: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-help"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-help: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-plugin-server"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-plugin-server: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-cache-tool"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-cache-tool: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/llvm-ar"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/llvm-ar: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang-cl"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang-cl: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/aarch64-swift-linux-musl-clang++.cfg"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/dsymutil"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/dsymutil: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang-cache"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang-cache: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-clang-modules-macosx.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-swift-modules-appletvos.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-clang-modules-appletvos.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/create-module-lists.sh"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-swift-modules-common.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-clang-modules-watchos.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-swift-modules-iosmac.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/infer-imports.py"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-swift-modules-iphoneos.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-clang-modules-iosmac.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-swift-modules-watchos.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-clang-modules-iphoneos.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-clang-modules-common.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/sdk-module-lists/fixed-swift-modules-macosx.txt"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/wasm-ld"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/wasm-ld: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-sdk"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-sdk: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-stdlib-tool"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-stdlib-tool: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-api-checker.py"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/lldb-dap"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/lldb-dap: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/aarch64-swift-linux-musl-clang.cfg"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/aarch64-swift-linux-musl-clang.cfg: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-test"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-test: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang++"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang++: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swiftc"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swiftc: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang-cpp"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang-cpp: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/llvm-profdata"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/llvm-profdata: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-autolink-extract"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-autolink-extract: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-experimental-sdk"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-experimental-sdk: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang-17"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/clang-17: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-api-digester"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-api-digester: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-package-registry"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-package-registry: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-driver"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-driver: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-build-sdk-interfaces"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-build-sdk-interfaces: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/x86_64-swift-linux-musl-clang++.cfg"
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-run"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-run: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-package"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-package: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/docc"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/docc: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-build"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift-build: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/x86_64-swift-linux-musl-clang.cfg"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/x86_64-swift-linux-musl-clang.cfg: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/lld-link"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/lld-link: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/ld64.lld"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/ld64.lld: replacing existing signature
Running "/usr/bin/codesign" "--force" "--preserve-metadata=identifier,entitlements" "--sign" "-" "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/llvm-ranlib"
~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/llvm-ranlib: replacing existing signature
- checking Swift compiler path: ~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift
Inferring basic settings...
- swift executable: ~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift
Apple Swift version 6.0-dev (LLVM de395d39a90ed7a, Swift 490cf64aee23f13)
Target: arm64-apple-macosx14.0
Running "~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/bin/swift" "package" "--triple" "wasm32-unknown-wasi" "--scratch-path" "WasmDemo/.build/carton" "--disable-sandbox" "plugin" "carton-dev" "--pid" "27359"
Fetching https://github.com/apple/swift-system.git from cache
Fetching https://github.com/apple/swift-log.git from cache
Fetching https://github.com/apple/swift-argument-parser.git from cache
Fetching https://github.com/swiftwasm/WasmTransformer from cache
Fetching https://github.com/swiftwasm/carton from cache
Fetching https://github.com/apple/swift-collections.git from cache
Fetching https://github.com/apple/swift-atomics.git from cache
Fetched https://github.com/swiftwasm/WasmTransformer from cache (0.55s)
Fetching https://github.com/apple/swift-nio.git from cache
Fetched https://github.com/apple/swift-atomics.git from cache (0.56s)
Fetched https://github.com/swiftwasm/carton from cache (0.57s)
Computing version for https://github.com/swiftwasm/carton
Fetched https://github.com/apple/swift-system.git from cache (0.68s)
Fetched https://github.com/apple/swift-log.git from cache (0.71s)
Fetched https://github.com/apple/swift-argument-parser.git from cache (0.82s)
Fetched https://github.com/apple/swift-collections.git from cache (0.84s)
Fetched https://github.com/apple/swift-nio.git from cache (0.65s)
Computed https://github.com/swiftwasm/carton at 1.1.2 (2.58s)
Computing version for https://github.com/swiftwasm/WasmTransformer
Computed https://github.com/swiftwasm/WasmTransformer at 0.5.0 (13.20s)
Computing version for https://github.com/apple/swift-nio.git
Computed https://github.com/apple/swift-nio.git at 2.68.0 (0.75s)
Computing version for https://github.com/apple/swift-argument-parser.git
Computed https://github.com/apple/swift-argument-parser.git at 1.3.1 (0.65s)
Computing version for https://github.com/apple/swift-log.git
Computed https://github.com/apple/swift-log.git at 1.6.1 (0.58s)
Computing version for https://github.com/apple/swift-system.git
Computed https://github.com/apple/swift-system.git at 1.3.1 (0.63s)
Computing version for https://github.com/apple/swift-collections.git
Computed https://github.com/apple/swift-collections.git at 1.1.2 (0.89s)
Computing version for https://github.com/apple/swift-atomics.git
Computed https://github.com/apple/swift-atomics.git at 1.2.0 (0.69s)
warning: WasmDemo/.build/carton/repositories/swift-log-ba8887eb is not valid git repository for 'https://github.com/apple/swift-log.git', will fetch again.
Fetching https://github.com/apple/swift-log.git from cache
Fetched https://github.com/apple/swift-log.git from cache (0.10s)
warning: WasmDemo/.build/carton/repositories/swift-log-ba8887eb is not valid git repository for 'https://github.com/apple/swift-log.git', will fetch again.
Fetching https://github.com/apple/swift-log.git from cache
Fetched https://github.com/apple/swift-log.git from cache (0.09s)
Creating working copy for https://github.com/apple/swift-log.git
Working copy of https://github.com/apple/swift-log.git resolved at 1.6.1
Creating working copy for https://github.com/swiftwasm/WasmTransformer
Working copy of https://github.com/swiftwasm/WasmTransformer resolved at 0.5.0
warning: WasmDemo/.build/carton/repositories/swift-argument-parser-54a11a8d is not valid git repository for 'https://github.com/apple/swift-argument-parser.git', will fetch again.
Fetching https://github.com/apple/swift-argument-parser.git from cache
Fetched https://github.com/apple/swift-argument-parser.git from cache (0.20s)
warning: WasmDemo/.build/carton/repositories/swift-argument-parser-54a11a8d is not valid git repository for 'https://github.com/apple/swift-argument-parser.git', will fetch again.
Fetching https://github.com/apple/swift-argument-parser.git from cache
Fetched https://github.com/apple/swift-argument-parser.git from cache (0.21s)
Creating working copy for https://github.com/apple/swift-argument-parser.git
Working copy of https://github.com/apple/swift-argument-parser.git resolved at 1.3.1
Creating working copy for https://github.com/apple/swift-nio.git
Working copy of https://github.com/apple/swift-nio.git resolved at 2.68.0
Creating working copy for https://github.com/apple/swift-atomics.git
Working copy of https://github.com/apple/swift-atomics.git resolved at 1.2.0
Creating working copy for https://github.com/swiftwasm/carton
Working copy of https://github.com/swiftwasm/carton resolved at 1.1.2
Creating working copy for https://github.com/apple/swift-system.git
Working copy of https://github.com/apple/swift-system.git resolved at 1.3.1
warning: WasmDemo/.build/carton/repositories/swift-collections-9a58d5cf is not valid git repository for 'https://github.com/apple/swift-collections.git', will fetch again.
Fetching https://github.com/apple/swift-collections.git from cache
Fetched https://github.com/apple/swift-collections.git from cache (0.25s)
warning: WasmDemo/.build/carton/repositories/swift-collections-9a58d5cf is not valid git repository for 'https://github.com/apple/swift-collections.git', will fetch again.
Fetching https://github.com/apple/swift-collections.git from cache
Fetched https://github.com/apple/swift-collections.git from cache (0.23s)
Creating working copy for https://github.com/apple/swift-collections.git
Working copy of https://github.com/apple/swift-collections.git resolved at 1.1.2
Building for debugging...
[413/413] Applying carton-frontend-tool
Build of product 'carton-frontend' complete! (27.79s)
Building "WasmDemo"
Building for debugging...
[0/5] Write sources
[1/5] Write swift-version--7D924A966E372B55.txt
warning: Could not read SDKSettings.json for SDK at: ~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/share/wasi-sysroot
[3/7] Emitting module WasmDemo
[4/7] Compiling WasmDemo main.swift
<unknown>:0: warning: libc not found for 'wasm32-unknown-wasi'; C stdlib may be unavailable
<unknown>:0: warning: libc not found for 'wasm32-unknown-wasi'; C stdlib may be unavailable
[5/8] Wrapping AST for WasmDemo for debugging
[6/8] Write Objects.LinkFileList
warning: Could not read SDKSettings.json for SDK at: ~/Library/Developer/Toolchains/swift-wasm-6.0-SNAPSHOT-2024-06-19-a.xctoolchain/usr/share/wasi-sysroot
[7/8] Linking WasmDemo.wasm
Build of product 'WasmDemo' complete! (6.71s)

Watching these directories for changes:
WasmDemo/Sources

2024-07-11T15:50:49-0400 info org.swiftwasm.carton.dev-server : [CartonKit] GET /
2024-07-11T15:50:49-0400 info org.swiftwasm.carton.dev-server : [CartonKit] GET /dev.js
2024-07-11T15:50:49-0400 info org.swiftwasm.carton.dev-server : [CartonKit] GET /main.wasm
2024-07-11T15:50:49-0400 info org.swiftwasm.carton.dev-server : [CartonKit] GET /apple-touch-icon-precomposed.png
2024-07-11T15:50:49-0400 info org.swiftwasm.carton.dev-server : [CartonKit] GET /apple-touch-icon.png
2024-07-11T15:50:49-0400 info org.swiftwasm.carton.dev-server : [CartonKit] GET /JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs

By the time this command has finished it has downloaded a snapshot of Swift for us, compiled our project, and even launched our browser pointed to a local URL that hosts our Wasm application. The window is empty, but soon we will be able to add HTML to this page. And we can verify that it is running actual Swift code in the browser by printing in the entry point:

print("Hello, world!")

Saving automatically reloaded the app, and if we open up the web browser’s JavaScript console we will see “Hello world!” was printed to it.

Another thing carton is doing is watching for changes on the file system to any of the files in our package. When it detects a change it will automatically re-compile our project and refresh the browser. It’s a pretty nice developer experience.

So we are already getting a little taste of things to come, but there is more to do. Next we need to another dependency to our project in order to interact with the browser DOM from Swift. One such library is JavaScriptKit from the swiftwasm organization:

.package(
  url: "https://github.com/swiftwasm/JavaScriptKit", exact: "0.19.2"
),

And note that we really are pinning to exactly version 0.19.2 of this library. The newest version of the library doesn’t compile, at least not with our Swift snapshot. So for now we will pin to the last version that definitely compiles, which is 0.19.2.

Then we need our app target to depend on these libraries:

.executableTarget(
  name: "WasmDemo",
  dependencies: [
    .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
    .product(name: "JavaScriptKit", package: "JavaScriptKit"),
  ]
)

And we are further going to even turn on Swift 6 language mode:

swiftLanguageModes: [.v6]

This will put concurrency warnings at their max so that we will immediately be notified when we are doing something wrong. And let’s go ahead and compile everything to make sure we are starting in a good place.

We now have the basics of our Wasm app set up, let’s get something on screen! In order to interact with the DOM, and typically one does that with JavaScript, but of course we don’t want to write JavaScript. We’d rather write Swift.

However, at the end of the day the DOM is still a JavaScript interface to the browser, and there’s just no way around that. Even WebAssembly does nothing to give other programming languages direct access to the DOM. Everything must go through JavaScript, and so we must be familiar with that API, as well as the basics of HTML, even if ultimately we just want to write Swift.

But one of the cool things about browsers is that they are a living, breathing execution environment for the DOM. This makes it super easy to explore these tools in an interactive manner.

We already have the console open and it shows that “Hello world!” was printed earlier. And right in here we can execute JavaScript and manipulate the DOM.

For example, there is a global called document

document

…that represents the document of the webpage we are looking at right now. The document object is capable of creating whole new elements for the DOM:

document.createElement

Elements are little HTML tags that can represent a variety of things, such as simple text, links, headers, lists and more.

For example, a simple, inline text element can be modeled with what is known as a “span” tag:

document.createElement("span")

Executing this line created an element, but then immediately discarded it because we didn’t bind it to a variable. Let’s do that:

let countLabel = document.createElement("span")

This creates an element, but it does not automatically insert it into the DOM. We have to do that ourselves.

But also, you can’t just insert this element anywhere in the DOM. For example, if we do it in the most naive way:

document.appendChild(countLabel)

…we get an error:

HierarchyRequestError: The operation would yield an incorrect node tree.

It is not valid to insert a span element into the root of a document. These kinds of elements can only go into the body of an HTML document, which is a special, unique node.

So, we actually have to do this:

document.body.appendChild(countLabel)

And now this works, and we can even see that an empty span element was inserted into the DOM.

We can update the content of the span node by setting its innerText:

countLabel.innerText = "Count: 0"

And now something is actually showing on the screen, so this is promising.

The next trick is to accomplish this all in Swift, not JavaScript.

We’ll start by importing JavaScriptKit:

import JavaScriptKit

This immediately gives us access to a document global variable:

JSObject.global.document

You’ll notice that document has a different syntax color than global. That is because global is an actual statically defined symbol, but document is a dynamically defined symbol via @dynamicMemberLook on strings.

This means we can technically access any property on global and it will compile just fine:

JSObject.global.foo

This returns a JSValue, which is an enum that describes all the different kinds of types JavaScript supports.

Because undefined is one of the options it is possible for this to resolve at runtime to something that doesn’t exist in JavaScript, and you run the chance of causing a runtime error if you use this value in some way, such as accessing a property on it:

JSObject.global.foo.bar as JSValue

We will see that the code compiles and runs in the browser, and then immediately produces an error.

Coming from a statically compiled language like Swift this is a little surprising, but as we said before, we are ultimately interfacing with JavaScript. And JavaScript is dynamically typed, and allows you to form expressions like this, and so JavaScriptKit allows it too.

And now we can start writing Swift code with this document object much like we did over in the browser inspector. We can eve create a countLabel element that is a span tag:

let document = JSObject.global.document

var countLabel = document.createElement("span")

And we can mutate its innerText to show some text in the element:

countLabel.innerText = "Count: 0"

And then we can append the element to the body of the document:

document.body.appendChild(countLabel)

This does produce a warning in Xcode:

Result of call to function returning ‘JSValue’ is unused

When @dynamicMemberLookup is used to reference a JavaScript function it means that the function can technically return something. And because JavaScript does not have types, we can’t know whether it actually returns something real, or if it’s just a Void-returning function. And so we must always handle the return value, and for Void functions we can just use an underscore to ignore it:

_ = document.body.appendChild(countLabel)

Line-for-line this looks almost identical to JavaScript. This is pretty cool because it means that when it comes to implementing the feature’s logic we can use our beloved Swift, and even share code between platforms, and then when it comes to interacting with the DOM we can still write Swift code, but in a style that is similar to JavaScript.

Also, while we were writing all of that code carton was re-compiling continuously and refreshing the browser, and we now see “Count: 0” in the browser. This page is now being powered off of our Swift code.

Let’s add a few more of the interface elements. We can add buttons for incrementing and decrementing the count and add them to the body:

var decrementButton = document.createElement("button")
decrementButton.innerText = "–"
_ = document.body.appendChild(decrementButton)

var incrementButton = document.createElement("button")
incrementButton.innerText = "+"
_ = document.body.appendChild(incrementButton)

And just the act of saving this file causes our browser to automatically refresh and we now see the new UI on the screen. It’s amazing to see that we now have UI controls rendering on the screen, all powered from Swift. And the code to build these UI components and add them to the DOM doesn’t really look that much different from how we do things over in UIKit world.

Observation in WebAssembly

OK, so this is really incredible. We now have the basics of a Swift app running inside the browser. We are executing actual Swift in this web app, and we are even interfacing with JavaScript through Swift. This is showing the very basics of how one can build a cross-platform Swift app for iOS and the web.

Stephen

But currently we don’t have any functionality in this app. Clicking on the buttons isn’t doing anything, and we aren’t observing changes to the model so that we can update the DOM in the browser. This is where our powerful and cross-platform navigation library comes into play.

Let’s check it out.

Currently the counter model is trapped inside the Xcode project along with the SwiftUI, UIKit and Epoxy views. We need to put the model somewhere that can be accessed from multiple platforms. The best place to do this is in a Swift package, and we can even reuse the package we have right here.

We already have a product in the package that is a library called “Counter”:

products: [
  .library(name: "Counter", targets: ["Counter"])
],

We can drag-and-drop the CounterModel.swift file into this new library. And we’ll need to add some explicit platform requirements to the package file in order build the model.

We will also need to add dependencies on our Perception and Swift Navigation libraries since the CounterModel uses them both:

.package(
  url: "https://github.com/pointfreeco/swift-navigation",
  from: "2.0.0"
),
.package(
  url: "https://github.com/pointfreeco/swift-perception",
  from: "1.0.0"
),

And we will add them to the target:

.target(
  name: "Counter",
  dependencies: [
    .product(name: "Perception", package: "swift-perception"),
    .product(name: "SwiftNavigation", package: "swift-navigation"),
  ]
)

The package now builds, but because we want to access this type and its properties and methods from other modules we will need to make everything public:

And just like that the Counter module is compiling, and we now have a shared library that can be used from both our Wasm app and our iOS app.

Let’s have the Wasm app depend on CounterFeature:

.executableTarget(
  name: "WasmApp",
  dependencies: [
    "Counter",
    …
  ]
)

…and we can now import Counter into the main entry point of the Wasm app:

import Counter

Now everything is compiling in Xcode just fine, which may make you thing everything is OK. However, if we look over at our terminal window we will find that it did not compile:

[22/25] Compiling Counter CounterModel.swift
Sources/Counter/CounterModel.swift:38:30: error: type 'URLSession' (aka 'AnyObject') has no member 'shared'
36 |       try await Task.sleep(for: .seconds(1))
37 |       let loadedFact = try await String(
38 |         decoding: URLSession.shared
   |                              `- error: type 'URLSession' (aka 'AnyObject') has no member 'shared'
39 |           .data(
40 |             from: URL(string: "http://www.numberapi.com/\(count)")!

It turns out that URLSession is not supported in Wasm contexts at all. There is something called FoundationNetworking:

#if canImport(FoundationNetworking)
  import FoundationNetworking
#endif 

…that does give you access to URLSession on non-Apple platforms such as Linux. However, that does not work for Wasm.

In order to perform network requests in Wasm you actually have to go through JavaScript’s own request infrastructure. But it turns out that is not so bad because JavaScript supports a familiar async/await syntax, and JavaScriptKit makes it easy to invoke the JavaScript functionality.

However, it’s going to take time to get all of that working, and we have simpler behavior in this feature to worry about before getting to things like network requests. So let’s comment out the body of the factButtonTapped for now:

public func factButtonTapped() async {
  self.factIsLoading = true
  defer { self.factIsLoading = false }

  do {
    try await Task.sleep(for: .seconds(1))
    /*
     …
     */
  } catch {
  }
}

With that commented out our app is building in terminal again.

Let’s now focus on the incrementing and decrementing logic of the feature, which will allow us to dip our toes into a small amount of complexity. We first need to be able to invoke methods on the model from our JavaScript buttons, and then we need to observe changes in the model to update the DOM.

Let’s start by creating a model that will power this page’s logic and behavior:

let model = CounterModel()

And now we want to invoke the incrementButtonTapped and decrementButtonTapped methods when the corresponding button is clicked in the page.

The way one executes logic when buttons are tapped in HTML is by setting the onclick attribute on the button tag. We can even give this a spin in the console real quick.

We can use the querySelector method on document to find a button tag:

document.querySelector('button')

This prints the element to the console:

<button>–</button> = $1

And hovering over this shows which element in the DOM was found.

And then we can override its onclick handler to print something to the console when it is clicked:

document.querySelector('button').onclick = () => {
  console.log("Decrement")
}

Now when we click on the “-” button we will see the string “Decrement” printed to the console.

Now let’s recreate this in Swift. It will look very similar, but we will need to adapt. We would love if we could just override the onclick property with a Swift closure that will be invoked when it is clicked:

decrementButton.onclick = {

}

But this isn’t possible. The onclick dynamic member can only be assigned with a JSValue, and there is no way to cast closures to other types in Swift.

Instead we need to assign a JSObject kind of JSValue:

decrementButton.onclick = .object(
)

And then we can package up a JSClosure inside this JSObject:

decrementButton.onclick = .object(
  JSClosure { _ in
  }
)

The value handed to JSClosure is an array of all the arguments handed to the closure from JavaScript. Since JavaScript is not statically checked you are free to pass as many arguments as you want to a function, and so the JavaScriptKit library has no choice but to represent that as an array.

We are getting a deprecation warning:

'init(_:)' is deprecated: This initializer will be removed in the next minor version update. Please use init(_ body: @escaping ([JSValue]) -> JSValue) and add return .undefined to the end of your closure

It seems that the version of this initializer that doesn’t return anything is deprecated, and we now need to return something explicitly. The default return value in JavaScript is undefined, so we can return that:

decrementButton.onclick = .object(
  JSClosure { _ in
    return .undefined
  }
)

And just to test that things are working, let’s print to the console in here:

decrementButton.onclick = .object(
  JSClosure { _ in
    print("Decrement from Swift!")
    return .undefined
  }
)

When the browser refreshes we will see that tapping the “-” button does indeed print “Decrement” to the console. We now have a Swift closure containing pure Swift code that is invoked from a button being clicked in HTML. That’s pretty incredible.

So, let’s invoke a method on the model:

decrementButton.onclick = .object(
  JSClosure { _ in
    model.decrementButtonTapped()
    return .undefined
  }
)

And we will do the same for the increment button:

incrementButton.onclick = .object(
  JSClosure { _ in
    model.incrementButtonTapped()
    return .undefined
  }
)

We are now executing our feature’s logic when these HTML buttons are tapped, but the UI is not yet updating. To do that we need to observe changes in the model and update the DOM.

And this sounds like the perfect job for the observe tool we built in previous episodes. Our SwiftNavigation library has the final, polished version of this tool, and so we will import that:

import SwiftNavigation

Note that this library is called SwiftNavigation. Not SwiftUINavigation. In the latest release we renamed the package to be just a simple SwiftNavigation, and it now houses 3 separate libraries:

  • SwiftUINavigation: a collection of tools that make domain modeling and navigation more ergonomic and powerful in vanilla SwiftUI.

  • UIKitNavigation: a collection of tools that make domain modeling and navigation more ergonomic and powerful in UIKit.

  • And then SwiftNavigation: a collection of tools that make domain modeling and navigation more ergonomic and powerful in any kind of Swift application.

The tools in this last library are a lot more broad and foundational, and typically need to do a little bit of upfront work to adapt their usage to the platform being developed on.

We will see more of that in a moment, but for now the observe tool works just fine without any additional work:

observe {
}

This closure will be invoked any time a field accessed on the inside is mutated. So the mere act of touching the count field on the model:

observe {
  _ = model.count
}

…has now subscribed us to changes for this field, and only this field.

We can even print to the console using the global variable:

observe {
  print("Count changed:", model.count)
}

And then heading over to the browser we see that “Count changed: 0” was already printed to the console, and that’s because the closure is invoked immediately, but tapping “-” or “+” doesn’t do anything.

Now you may have noticed this warning we are seeing from our invocation of the observe function:

Result of call to 'observe(_:isolation:)' is unused

The most general form of observe returns an ObservationToken that one must keep around in order to keep the subscription alive. Once the token is deallocated the subscription is cancelled, and the trailing closure of observe will not be called anymore.

We don’t have to worry about keeping the subscription alive when using this tool in UIKit because there is an overload that works on NSObject and it stores the token in the object using the machinery of “associated objects”. But when on non-Apple platforms we do have to keep this token around.

So, in the app entry point we will hold onto a set of observation tokens:

@main
struct App {
  static var tokens: Set<ObservationToken> = []
  …
}

But now we are getting a compiler error due to a concurrency problem:

Static property ‘tokens’ is not concurrency-safe because it is non-isolated global shared mutable state

It turns out that in Swift 6 mode static vars are not safe unless they are isolated to an actor. Well, we can just make our entire app @MainActor:

@main
@MainActor
struct App {
  …
}

…and that fixes the problem.

With that we can now store the token in the set when observing:

observe {
  …
}
.store(in: &tokens)

And now if we tap the “-” or “+” buttons we will see… well nothing changes unfortunately. This is because we are now executing logic after the initial load of the page, but technically our Wasm app completed its execution by the time our main entry point finished. And so there’s no ability to execute more logic later.

To fix this we need to install an event loop so that we can process events later on, and JavaScriptKit comes with a tool to do this:

import JavaScriptEventLoop

…

JavaScriptEventLoop.installGlobalExecutor()

We can even jump into the implementation of this function, and dedicated viewers of Point-Free will see something familiar:

typealias swift_task_enqueueGlobal_hook_Fn =
  @convention(thin) (
    UnownedJob, swift_task_enqueueGlobal_original
  ) -> Void
let swift_task_enqueueGlobal_hook_impl:
swift_task_enqueueGlobal_hook_Fn = { job, original in
  JavaScriptEventLoop.shared.unsafeEnqueue(job)
}
swift_task_enqueueGlobal_hook = unsafeBitCast(
  swift_task_enqueueGlobal_hook_impl,
  to: UnsafeMutableRawPointer?.self
)

This code is overriding the global task enqueue hook of Swift so that it can control how concurrency works. This is exactly what we did in our series of episodes on “Reliably testing async”, where we explored the problem of testing async code in Swift, and then used this tool to fix it. It’s pretty cool seeing this tool being used elsewhere in the ecosystem.

And since we see what this installGlobalExecutor is doing, we should probably execute it as early as possible, that way any async work executed will be enqueued properly.

Now when we run the app in the browser we will see that the newest count is printing to the console when we tap the “+” and “-” buttons.

This is incredible. We are now seeing actual Swift logic executing in our browser! But instead of printing to the console we want to update the DOM.

To do that we can update the countLabel’s innerText inside the observe closure:

countLabel.innerText = "Count: \(model.count)"

This does not compile:

 Cannot assign value of type ‘String’ to type ’JSValue’

…because we need to assign a JSValue to this dynamic member, and we can do so by wrapping it up in the .string case:

countLabel.innerText = .string("Count: \(model.count)")

And now everything is compiling, and our web app is more functional. We can count up and down and the text magically updates in the browser. And this is all powered by Swift!

Next time: Making network requests

I don’t know about you, but I think this pretty incredible. We have a 100% pure Swift code base that is powering a web app. And even better, we are using the same model that we used over in UIKit and SwiftUI. This is showing a possible vision for cross-platform development in Swift. One model powering features on two vastly different platforms: iOS and the web. And it’s all thanks to the cross-platform observe function that comes with our SwiftNavigation library, as well as Swift’s amazing Observation framework.

Brandon

But we are still missing some of the most important behavior in the feature, which is the network request that loads a fact from the server and presents it to the user. This is going to be a little more difficult than everything we have accomplished so far because it involves new APIs for making network requests and it involves asynchronous work.

Let’s dig in.


References

  • Swift for WebAssembly - How To Use Swift In Your Web App
    Steven Van Impe

    A talk from SwiftCraft 2024: WebAssembly is a rapidly growing technology that provides great opportunities for Swift developers. This talk will introduce Swift developers to WebAssembly, and demonstrate how they can run Swift in the browser, call JavaScript from Swift to access the DOM, add Swift modules to web apps, and so much more. A live demo will show how a single Swift codebase can power not just an iOS app, but also a web app, and a back-end.

  • Batteries Not Included: Beyond Xcode
    Kabir Oberai

    A talk from Swift TO 2023: A confrontation of the notion that Xcode+macOS are the only way to develop apps for Apple platforms. We’ll discover what it takes to build apps in other IDEs like Visual Studio Code, as well as on non-Apple platforms, unveiling the secrets of cross-compilation to build, sign, and deploy iOS apps on Windows and Linux.

  • Swift WebAssembly + GoodNotes, a cross-platform story!
    Pedro Gómez • Nov 19, 2022

    A talk from NSSpain 2022 discussing how Goodnotes uses Swift Wasm in their application: When a company implements 100% of the codebase in a language like Swift, there is a chance that in the future you may need to implement something that is not an iOS app with the same code, something like a web page maybe? This talk relates how we’ve reused most of the already implemented code in GoodNotes iOS app to create a web project we can reuse in different platforms, the technical approach we fo llowed, challenges and solutions applied.

Downloads

Get started with our free plan

Our free plan includes 1 subscriber-only episode of your choice, access to 64 free episodes with transcripts and code samples, and weekly updates from our newsletter.

View plans and pricing