SwiftWebUIでWebアプリをつくろう！

# はじめに

WWDC2019で発表され、大いに注目を集めるSwiftUIですが、そのSwiftUIでWebアプリを作れることをご存知でしょうか？

[SwiftWebUI](https://github.com/SwiftWebUI/SwiftWebUI)を使えばできます！
SwiftWebUIは、そのままですが、SwiftUIをWebでも使えるようにしようというプロジェクトです。

<img width="813" alt="Screen Shot 2019-12-09 at 6.45.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69911/068a1b52-ff37-4795-fd8d-35d9d834f0f6.png">

今回は簡単にTODOアプリを作っていきます。

![Task App Demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69911/01b88927-19dd-6a3e-c9d7-28e3c2adf3e6.gif)

# 参考リンク

- [GitHub - SwiftWebUI/SwiftWebUI: A demo implementation of SwiftUI for the Web](https://github.com/SwiftWebUI/SwiftWebUI)
- [SwiftWasm - compile Swift to WebAssembly](https://swiftwasm.org/)
- [iOSアプリ開発にSwift Package Managerを使おう - Qiita](https://qiita.com/hironytic/items/09a4c16857b409c17d2c)
- [GitHub - mtfum/SwiftWebUISample](https://github.com/mtfum/SwiftWebUISample)

# Hello World

Xcodeを開き、New Project, macOS, Command Line Toolを選択します。
<img width="713" alt="Screen Shot 2019-12-09 at 5.46.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69911/30a7e5cb-62e4-9cbf-6f3f-e17cd9fe868a.png">

適当に名前を入力し、作成できたらSwift Package Managerで[SwiftWebUI](https://github.com/SwiftWebUI/SwiftWebUI)を入れていきます。

[[iOSアプリ開発にSwift Package Managerを使おう - Qiita](https://qiita.com/hironytic/items/09a4c16857b409c17d2c)]のリンクがわかりやすいです。

きちんと導入できれば、`main.swift` で下記をコピペしてBuild&Runして、[http://localhost:1337](http://localhost:1337)にアクセスすれば、テキストの内容(`Hello World`)が表示されているでしょう

```swift:main.swift
import SwiftWebUI

SwiftWebUI.serve(Text("Hello World"))
```

続いて簡単なTODOアプリを作っていきましょう

# TODOアプリ

モデルを定義します。


```swift:Task.swift

struct Task: Identifiable {
  var text: String
  var createdAt: Date
  var isFinished: Bool

  var id: String { text }
  var createdAtString: String {
    let f = DateFormatter()
    f.timeStyle = .short
    f.dateStyle = .short
    f.locale = Locale(identifier: "ja_JP")
    return f.string(from: createdAt)
  }
}
```

タスク一つ一つのUIを作成します。
Viewが一つのtaskを持っていて、Doneボタンを押せば`isFinished`が`true`になってタスクが完了し、文字色やViewのEnabledが制御されています。

```swift:TaskRow.swift

struct TaskRow: View {

  @State var task: Task

  var body: some View {
    HStack {
      Button("Done") {
        self.task.isFinished = true
      }

      VStack(alignment: .leading) {
        Text(task.name)
          .font(.headline)
          .color(task.isFinished ? .secondary : .primary)
        Text(task.createdAtString)
          .color(.secondary)
          .font(.subheadline)
      }
    }
    .padding()
    .disabled(task.isFinished)
  }
}
```

上記のUIは↓のようになります。

<img width="241" alt="Screen Shot 2019-12-09 at 6.10.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69911/105f84f7-3e0d-ea3b-f992-f7d73a44d27e.png">

モデルとUIができればモックが作れるので、`main.swift`を書き換えていきましょう。

```swift

import Foundation
import SwiftWebUI

struct MainView: View {

  @State private var tasks: [Task] = [
    Task(name: "Sample Task", createdAt: Date(), isFinished: false),
    Task(name: "Sample Task", createdAt: Date(), isFinished: false),
    Task(name: "Sample Task", createdAt: Date(), isFinished: false)
  ]

  var body: some View {
    Form {
      Section(header: Text("Task App").font(.title)) {
        List(tasks.identified(by: \.id)) { TaskRow(task: $0) }
      }
    }
  }
}

SwiftWebUI.serve(MainView())
```

この状態でBuild&Runすればタスクが画面の真ん中に表示されているかと思います。

<img width="256" alt="Screen Shot 2019-12-09 at 6.13.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69911/4171338a-bd72-54fb-3d12-6d0b0b6da198.png">

あとはタスクを追加できるようにすれば完了です。

`TaskInputView`を作成し、textを`TextField`で変更していき、追加ボタンを押したらactionを呼ぶようにします。

```swift:TaskInputView.swift
import SwiftWebUI

struct TaskInputView: View {

  @State private var text: String = ""

  let action: (String) -> Void

  var body: some View {
    HStack {
      TextField($text)
      Spacer()
      Button("追加", action: {
        self.action(self.text)
        self.text = ""
      })
        .disabled(text.isEmpty)
    }
  }
}
```

一方で、`main.swift`では`TaskInputView`のactionでString型受け取るので、tasksの配列に追加していくようにしておきます。

```swift:main.swift
  var body: some View {
    Form {
      Section(header: Text("Task App").font(.title)) {
        TaskInputView(action: { text in
          let task = Task(text: text, createdAt: Date(), isFinished: false)
          self.tasks.append(task)
        })
        Spacer(minLength: 12)
        Text("Task List")
          .font(.title)
          .bold()
        List(tasks.identified(by: \.id)) { TaskRow(task: $0) }
      }
    }
  }
```

下記のようなアプリが出来上がります。

![Task App Demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69911/01b88927-19dd-6a3e-c9d7-28e3c2adf3e6.gif)

既知のバグとして

- TextField内のtextの変更を受け取れていない
- Task Listを空配列で定義するとタスクを追加できない

が残っておりいろいろ雑ですが、現状のSwiftWebUIの仕様など、詳しくみれていないので時間があれば調査したいと思います。

完成形のプロジェクトをGitHubに上げておきます。
[GitHub - mtfum/SwiftWebUISample](https://github.com/mtfum/SwiftWebUISample)

# 懸念点

- `NavigationView`が利用できない
- Buttonの色が変更されない
- タスクの空配列に追加してもListに反映されない
    - `TypeError: Cannot read property 'parentNode' of Null`のエラーが表示される

など、実際に触ってみるとバグも多く、利用していくにはまだまだ先になりそうです。
また、ブラウザ上でSwiftを動かすためには[Swift Wasm](https://swiftwasm.org/)の対応も待たなければなりません。

# 最後に

SwiftWebUIはプロダクション用ではなくSwiftUIを学ぶのに適していると注意書きされています。
> Disclaimer: This is a toy project! Do not use for production. Use it to learn more about SwiftUI and its inner workings.

しかし、SwiftでWebアプリがつくれる日が近づいている証でもあり、まだまだ先は長そうですが今からとても楽しみです。

SwiftUIを触ってみるには良い環境かもしれないので、興味のある人は触ってみてください。
ということでSwiftWebUIの紹介を示させてもらえればと思います。
最後までお読みいただきありがとうございました。