SwiftUI で Web アプリという可能性

はじめに

こんにちは.R&D チームの久保です.

現在 JavaScript や JavaScript にトランスパイルする言語 (TypeScript, 新し目の ECMAScript など) が主流であるフロントエンドの Web アプリケーションフレームワーク界隈ですが,数年前に WebAssembly の実行が主要ブラウザ全てでサポートされて以来,ひそかに他の言語によるフレームワークが増えつつあるのをご存知でしょうか?しかもそれらは,JavaScript でデファクトスタンダードとなりつつある ReactVue.js などと同様,仮想 DOM を実装していたりします.

例としては C# (.NET) の Blazor, Rust の Yew, Go の Vugu, C++ の asm-dom などがあります.ただ,現時点ではその多くが実験的なプロジェクトであり,今すぐに JavaScript を置き換えられるかと言われるとまだまだ課題が多いのが現状です.

それらのフレームワークの基本的な仕組みとしては,まずブラウザでページを開いたら,ごく薄い JavaScript 部分経由でメインのコードがコンパイルされた WebAssembly がロードされます.WebAssembly 内ではそれぞれのフレームワークの流儀に従って仮想 DOM 経由でもしくは直接的に実 DOM をいじることがあるわけですが,そういった部分では JavaScript とやりとりするためのラッパーライブラリを用いて JavaScript を呼び出すことでブラウザの Web API にアクセスすることになります.その際のオーバーヘッドなどまだまだ細かい部分が気になる人は気になるでしょうが,今回は話の都合で割愛します.

今回はそんな中でも Swift の Tokamak というフレームワークを中心にお話ししようと思います.これは SwiftUI 互換の API でフロントエンドの Web アプリケーションが組めるフレームワークということで,弊社の「世界初,iPad Pro を使って誰でも簡単に高精度3次元測量ができるアプリ」である OPTiM Land Scan でも SwiftUI が使われていることも相まって非常に興味をそそられるところです.

説明はいいから実際のコードが見てみたいという方は「Tokamak で遊ぼう」まで飛ばしてください.

目次

SwiftUI

SwiftUI は 2019 年に登場した Swift 向けの UI ライブラリであり,iOS, iPadOS, macOS, tvOS, watchOS を対象としています.それらのプラットフォームで従来使われている UIKit や AppKit が UI を手続き的に記述するのに対し,SwiftUI は宣言的に UI を記述するのが特徴です.Flutter や (WPF や UWP アプリ開発における) XAML,あるいは HTML などを想像するとわかりやすいかもしれません.

実際のコードが気になる方はこちらのチュートリアルをご覧ください.

もともと Swift 5.1 以降の言語機能である

をふんだんに使っているため iOS/iPadOS 13 以降, macOS 10.15 以降, tvOS 13 以降, watchOS 6 以降でなければ使えませんが,iOS 14 世代以降は大幅に API が改良・拡充され,Xcode 12 からは新規プロジェクト作成時のデフォルトの選択になったこともあって今後広く普及するであろうことが予想されます.

SwiftWasm

そもそも Swift コンパイラの WebAssembly 対応は現在 SwiftWasm というプロジェクトで本家をフォークしたコンパイラとして進んでいて,将来的に本家 Swift コンパイラへのマージを目指しているようです.

SwiftWasm 自体のこれまでの経緯や意義などについては以下の記事が非常に参考になるのでそちらにお任せします.一つ目の記事を書いている kateinoigakukun さんは SwiftWasm の中心メンバーの一人でもあり,実際の実装に関する話がかなり興味深いですね.

SwiftWasm によって Swift から WebAssembly へのコンパイルするための実装が徐々に進んでくるにつれて,サイドプロジェクトとして (当時は React のようなフレームワークを目指していた) Tokamak や,SwiftWasm のコードから JavaScript を呼び出すためのライブラリである JavaScriptKit や,SwiftWasm で書かれたアプリケーションのためにツールチェーン管理や依存解決やビルド,開発サーバ立ち上げ,テストなどを行うための専用 CLI ツールである carton などが作られました.

SwiftWebUI

時は 2020 年 6 月,Reddit にある投稿が流れました.SwiftUI に似せた API を使って Web アプリが記述できて,SwiftWasm を使って WebAssembly にコンパイルすることでステートフルな Web アプリを作れるようなものが完成したよ!というものです.

そもそもこのフォーク元のオリジナル SwiftWebUI はもっと前からあるプロジェクトで,SwiftUI に似せた API で Web アプリケーションが作れるというのは同じなのですが,サーバ側とクライアント側で通信することではじめて動的な Web アプリケーションを実現するというものでした.その点,フォークされた SwiftWebUI はクライアント側で完結していることになります.

フォーク元にしてもフォーク先にしてもあくまで PoC 的なプロジェクトだったので現在はメンテナンスされていませんが,それの影響を受けてか受けずか Tokamak でも進んでいた SwiftUI 互換に寄せる実装が一気に進んで 7 月に Tokamak 0.2.0 がリリースされました.

Tokamak で遊ぼう

ここからは Tokamak を使って簡単な TODO リストを作ってみます.

まずは環境構築ですが,今回は SwiftWasm のツールチェーンのインストールから,パッケージの依存解決,開発用 Web サーバの立ち上げ,テストの実行などまで行ってくれる carton というコマンドラインツールを使用していきます.carton の前提条件は README にあるように

  • macOS の場合は 10.15 以降で,Xcode 11.4 以降がインストール済みであること
  • Linux の場合は Swift 5.2 以降がインストール済みであること

となっているため,この説明では macOS を用いますが Linux でも開発可能です.Swift 本体は Windows もサポートし始めたので,Windows 勢は今後に期待ですね.

環境構築が面倒という方は,kateinoigakukun さんがブラウザ上で Tokamak を試せる SwiftWasm Pad というものを公開されているので,そちらで試してみてください.

それでは carton をインストールしていきましょう.macOS では Homebrew をインストール済みであればターミナルで以下を実行すれば,依存する wasmer (テスト等に使用) や wabt (ビルド時にバイナリの strip などに使用) なども含めてインストールされます.ちなみに今回インストールした carton のバージョンは 0.5.0 でした.

brew install swiftwasm/tap/carton

インストールが完了したら以下のようにプロジェクトを作成していきましょう.

mkdir TODOList
cd TODOList
carton init --template tokamak

すると以下のようにカレントディレクトリにテンプレートのファイルが生成されるはずです.

$ tree
.
├── Package.swift
├── README.md
├── Sources
│   └── TODOList
│       └── main.swift
└── Tests
    ├── LinuxMain.swift
    └── TODOListTests
        ├── TODOListTests.swift
        └── XCTestManifests.swift

Sources/TODOList/main.swift はこのようになっています.

import TokamakShim

struct TokamakApp: App {
    var body: some Scene {
        WindowGroup("Tokamak App") {
            ContentView()
        }
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
    }
}

// @main attribute is not supported in SwiftPM apps.
// See https://bugs.swift.org/browse/SR-12683 for more details.
TokamakApp.main()

View protocol だけでなく,SwiftUI では iOS/iPadOS 14 から使用できるようになった App protocol にも対応していることがわかりますね.また,Package.swift をみるとこのテンプレートでは Tokamak の 0.3.0 を使用しているようです.

TODO リストを作成する前に一度ブラウザで実行してみましょう.

この状態で以下のコマンドを実行するとビルドが走り開発用 Web サーバが立ち上がるのですが,初回実行時は SwiftWasm のツールチェーンのダウンロードも行われるため少し時間がかかります.

carton dev

全て正常に完了すると以下のように Web サーバが Listen しているアドレスとポート番号が URL で表示されるかと思います.

Watching these directories for changes:
/Users/kebo/Documents/TODOList/Sources/TODOList

[ NOTICE ] Server starting on http://127.0.0.1:8080

[ NOTICE ] Server starting on ... の行に表示された URL にブラウザでアクセスしてみてください.以下のように表示されるはずです.本家 SwiftUI のダークモード対応と同様,デフォルトのスタイルでは CSS の prefers-color-scheme に応じて背景色が変わるようで,私の環境ではダークモードになっていました.

f:id:optim-tech:20200923163517p:plain

それでは main.swift を書き換えて TODO リストを作っていきましょう.といっても,ステップバイステップでは長くなり過ぎてしまうのでいきなり完成形を貼ります.main.swiftContentView のを以下のように置き換えます.

struct TODOItem {
    var checked = false
    var name: String
}

struct ContentView: View {
    @State var items: [TODOItem] = [
        .init(name: "アイテムを追加する"),
    ]
    @State var input = ""

    var body: some View {
        List {
            Section(
                header: TextField(
                    "新しいアイテム...",
                    text: self.$input,
                    onCommit: {
                        self.items.append(TODOItem(name: self.input))
                        self.input.removeAll()
                    }
                )
            ) {
                ForEach(0..<self.items.count, id: \.self) { i in
                    HStack {
                        Button(self.items[i].checked ? "取消" : "完了") {
                            self.items[i].checked.toggle()
                        }
                        if self.items[i].checked {
                            Text(self.items[i].name)
                                .strikethrough()
                        } else {
                            Text(self.items[i].name)
                        }
                    }
                }
            }
        }
    }
}

また,ソースコードの編集には,私が以前「Rust IDE に化ける VSCode」という記事の「Language Server とは」で説明していたように Swift にも SourceKit-LSP という Language Server があるので,これを VS Code や Vim などと連携させることでそれらのエディタで入力補完などが効いた状態で編集が可能です.VS Code 向けのSourceKit-LSP のインストール方法はこちらにありますが,今回は説明を省略します.

carton dev が実行されたまま main.swift を書き換えて保存すると,自動的に再ビルドが走ってブラウザも自動リロードされたことが確認できると思います.

f:id:optim-tech:20200923163521p:plain

Wasm な Web アプリケーションフレームワークの課題

このように JavaScript 以外の言語で手軽に Web アプリケーションが構築できるとはいえ,冒頭にも話していたようにいずれもまだまだ成熟しきっていません.

Tokamak をはじめ,こういったフレームワークの課題としては現段階では以下のようなものがあると思っています.

  1. デバッグ周り (ブレークポイントで止めたり,ステップ実行したり,…)
  2. JavaScript を介さないとロードもできず,DOM やブラウザの Web API へのアクセスもできない
  3. Wasm バイナリのダウンロードにかかる時間

まずデバッグ周りに関しては Blazor がかなり進んでおり,エディタや IDE 上のデバッガでデバッグできるだけでなく,Chrome Dev Tools Protocol に対応したデバッグプロキシを提供することで,ブラウザ上でのデバッグも行えるようです.ブラウザ上でのデバッグまで行えるものはおそらく他にはほとんどないと思います.

一方 SwiftWasm (Tokamak) ではそのあたりはまだあまり進んでいないようですが,kateinoigakukun さんがデバッガを自作していたりします.

次に,JavaScript を介さないとできないことがあるという課題です.DOM を直接操作したい場合や外部サービスの REST API を叩きたい場合なども JavaScript を呼び出す必要があります.JavaScript を仲介することでオーバーヘッドが発生するため,なくせるのであれば無くしたいところですが,例えば Web API にしても C API である程度の言語を賄える可能性があるとはいえ,ブラウザ側が主要言語全てに API を提供するのはメンテナンスコストも考えるとあまり現実的ではありません.この辺りは WASI のような WebAssembly 向けのインターフェースがもうちょっと定まってくると変わるのかなという気がします.

3 つ目に Wasm バイナリのダウンロード時間です.Wasm を実行するためには当然 Web サーバからダウンロードする必要があるので,バイナリサイズが大きいとダウンロードにも時間がかかるのでページが表示されるまでに待たされるなんてことが起こりかねません.バイナリサイズの面ではもともと小さい上に標準ライブラリを外したりできるような Rust や C++ などが非常に有利だといえます.Swift も言語仕様的には同レベルまで小さくできてもおかしくないですが,現状かなり大きくなっています.その原因や改善の取り組みは kateinoigakukun さんが「SwiftのWebAssembly対応の進捗」や「SwiftのWebAssembly対応の軌跡」で説明されているので,そちらにおまかせします.

一方,ブラウザサイドからもこれを改善する取り組みは進められており,Firefox ではここで解説されているように,「Wasm のダウンロードとそれを実行するためのコンパイルを同時に進める」というのと「実行開始を優先で最初は高速にコンパイルし,後で最適化したコンパイル結果で差し替える」ということを行なっていたりします.

さいごに

このようにまだ課題も多い領域ですが,開発は活発に行われており,実際に Blazor や Yew などを使った Web アプリケーションもぽつぽつ見かけるようになってきました.業務で使うのはまだまだ先だと思いますが,Web 開発者は定期的に触れておいて損はない技術だと思います.

OPTiM ではモバイルアプリケーションエンジニアも含む,新卒・中途の様々なエンジニアを募集しています.よろしければそちらもどうぞ.