SwiftUI で開発した iPhone/iPad アプリをリリースしてみた

先日 SwiftUI ベースの iOS/iPadOS アプリをリリースしたので、そこで気づいたことを共有します。

作ったのは [icotile というアプリ](https://apps.apple.com/app/icotile-twitter-list-manager/id1489092646)で、Twitter のリストや友達を管理するものです。Vue.js ベースの [Web アプリケーションとしてすでにリリース](https://icotile.ogaoga.org/)していて、API も揃っているので、移植してみました。

👇 無料なので、動きなどは実際に試してもらえればと思います。
[‎icotile (Twitter List Manager) on the App Store](https://apps.apple.com/app/icotile-twitter-list-manager/id1489092646)

![icotile-iphone-ipadのコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4617/d4d87701-1194-4a97-74d6-8b2628e6e924.png)

 （関連）[8年前に作った HTML5 アプリを最近の技術で作り直した話（５年ぶり２回目） - Qiita](https://qiita.com/ogaoga/items/e7e2c0563dea8737ae5a)

## SwiftUI って業務で使えるの？

いきなりですが結論からすると、業務で SwiftUI を使うのは時期尚早だと思います。

- 実装されていない機能や UI があり、必要ならば UIKit で補う必要がある。
- まだ細かいバグもあり、フレームワークとして完成度が高くない。
- ドキュメントも不十分で、試行錯誤に時間が取られる。

もちろん、簡単なカタログアプリ的なものであれば、トライアルで作ってみるのもありかもしれないですが、まだベータバージョンと言っても過言ではないです。SwiftUI のクセを理解して、うまくプロダクトに落とし込むのに試行錯誤が必要です。

また、次のメジャーバージョンで大幅に改善が入ると、これまでのノウハウが使えなく可能性もあります。まあ、それはよくあることですが、業務で行うとなると、そこもリスクとなるでしょう。

ちなみに、業務レベルで採用されたメジャーな例は、探した感じこのアプリぐらいでした。他にもあるのかな？

[ICカードリーダーのiOSアプリをSwiftUIで開発しました | Money Forward Engineers' Blog](https://moneyforward.com/engineers_blog/2019/09/20/ic-cardreader-swiftui/)

## SwiftUI って初心者向け？

では、どういうケースで利用できるかというと、最初の iOS アプリ開発の入り口として SwiftUI はアリだと思います。UIKit ではじめると、最初、delegate の概念がわからないとか、Storyboard でどう Auto Layout を指定するのかなど、いろいろつまづくポイントはあると思いますが、その点、SwiftUI は素直な記述で実現が可能です。プレビューもできるし、楽しく学べる気がします。

ある程度スキルがついてきて、より複雑な機能を実装したくなったら、そこから UIKit に移行しても良いと思います。SwiftUI から UIKit を呼び出すことも可能なので、シームレスに移行することも（頑張れば）可能です。

## どこに UIKit を使ったか？

ビューのほとんどは SwiftUI で記述しましたが、一部、UIKit も使っています。

UIKit を呼び出すのはさほど難しくなく、ラッパーを書くぐらいの感じです。SwiftUI Tutorial でもわかりやすく説明があります。

[Interfacing with UIKit — SwiftUI Tutorials | Apple Developer Documentation](https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit)

今回は、下記の UI を `UIViewRepresentable` でラップして利用しています。

- `WKWebView`: Twitter のログインを WebView で行うため。
- `UIActivityIndicatorView`: プログレス表示。これは標準で欲しかった...
- `UITextView`: SwiftUI の `TextField` では複数行の編集ができなかったため。
- AdMob: 3rd party の UIView も使えます。

サンプルとして、ログインで使った`WkWebView` を呼び出すコードは下記のような感じです。

<details><summary>クリックして表示</summary><div>

```Swift
import SwiftUI
import WebKit

struct WebView : UIViewRepresentable {
  var url: URL
  var frame: CGRect?
  var configuration: WKWebViewConfiguration?
  var navigationDelegate: WKNavigationDelegate? = nil
  
  func makeUIView(context: Context) -> WKWebView  {
    var webView: WKWebView? = nil
    if let frame = frame {
      if let configuration = configuration {
        webView = WKWebView(frame: frame, configuration: configuration)
      } else {
        webView = WKWebView(frame: frame)
      }
    } else {
      webView = WKWebView()
    }
    if navigationDelegate != nil {
      webView!.navigationDelegate = navigationDelegate!
    }
    webView?.load(URLRequest(url: url))
    return webView!
  }
  
  func updateUIView(_ uiView: WKWebView, context: Context) {
  }
}

// 呼び出す時（シートを利用）
// 引数に渡している `navigationDelegate` は `WKNavigationDelegate` の実装で、
// この中で認証情報を処理している。
SomeView()
  .sheet(isPresented: $viewModel.isLoginWebPresented) {
    LoginWebView(isPresented: self.$viewModel.isLoginWebPresented)
  }
```
</div></details>

## 実は Combine が肝？

SwiftUI はあくまでビューのフレームワークです。データの処理はこれまでと同じ手法が使えますが、同時に発表された Combine と組み合わせることが想定された作りになっています。

Combine は Apple 純正のフレームワークで、非同期処理やイベント処理を宣言的に記述できます。Rx を使ったことがある人にはとっつきやすいと思いますが、これまで避けてきたので理解するのに苦労しました。SwiftUI で UI は簡単に作れたけど、API を呼んでデータを UI に反映させるところにかなり時間がかかりました。

ただ、SwiftUI と Combine は親和性が高いので、おそらく UIKit + RxSwift よりもシンプルで、理解がしやすかったんじゃないかなと思います。今後は UIKit でも Combine を使ってみたいと思いました。

当初は仕様がコロコロ変わって学ぶのと同時に追いつくのが大変でしたが、最近はだいぶ落ち着いたんじゃないかな。これらのページがとても参考になりました。

- [【iOS】Combineフレームワークまとめ - Qiita](https://qiita.com/shiz/items/5efac86479db77a52ccc)
- [Using Combine](https://heckj.github.io/swiftui-notes/)

## 3rd Party ライブラリ

SwiftUI であっても、普通に Swift 系のライブラリは使用可能です。もちろん、CocoaPods や Carthage、Swift Package Manager も利用可能です。

SwiftUI 向けのライブラリはあまりないと思っていましたが、たまたま見つけた [QGrid](https://github.com/Q-Mobile/QGrid) というライブラリをアイコンを並べるビューに使っています。

他にも、いろんなライブラリがあるみたいです（この記事を書いていて、下記の記事を見つけました）。

[SwiftUIおすすめライブラリ!! - Qiita](https://qiita.com/shira-shun/items/492dcdd79ed9dde09bf6#partialsheet-%EF%B8%8F202)

## 工夫したことや困ったこと

### iPhone と iPad への対応

SwiftUI では、Storyboard は使わず、コードでレイアウトを構築します。はじめから様々なデバイスに対応しやすいように設計されていて、iPad 対応も Storyboard より簡単に感じました。

例えば、Master-Detail レイアウトは、下記のように記述すると、iPhone では Master ビューだけが表示され、iPad の横画面では、Master ビューと Detail ビューを同時に表示してくれます。

```Swift
struct ContentView: View {
    var body: some View {
        NavigationView {
            MasterView()
            DetailView()
        }.navigationViewStyle(DoubleColumnNavigationViewStyle())
    }
}
```

なお、XCode の New Project から、Master-Detail App を選択して開発を始めた方が、トラブルは少ないと思います。

### `isPresented` は使わない

Sheet や ActionSheet などは、View にオーバーレイする形で表示します。ここで定義されていますが、メソッドが２つ提供されています。

[View Presentation | Apple Developer Documentation](https://developer.apple.com/documentation/swiftui/view/view_presentation)

`isPresented:` はフラグで制御し、`item:` はその中に表示させたい View を与えるかどうかで制御します。

例えば、同一ビューにて複数の Sheet を表示したい場合には、`isPresented` だと上手くいかず、他にも不都合なことがありました（忘れちゃったけど）。

なので、使用する場合は `item:` に統一するようにしました。

```Swift
  @State private var sheetView: SheetView?
  ...
  var body: some View {
    ...
    AnyView()
      .sheet(item: self.$sheetView) { view in view }
```

例えば、上記で `self.$sheetView` に Sheet で表示させたいビューを代入すると、Sheet が表示されます。

### Sheet を下スワイプで閉じるのを禁止できない。

iOS13 で追加された Sheet は、`.sheet()` で呼び出すことができますが、SwiftUI では下記を制御することができません。

- フルスクリーンで表示できない（UIKit での `modalPresentationStyle = .fullScreen` ができない）。
- 下方向にスワイプすることを禁止できない（UIKit での `isModalInPresentation = true` ができない）。

特に後者は、入力フォームなどで入力内容を明示的に保存させたいようなケースで困ります。今回は、スワイプで閉じたときはキャンセルとみなすようにしましたが、あまりいい体験ではないので、閉じさせないオプションをつけてもらいたいところです。

### View エレメントの数に比例してパフォーマンスが悪くなる。

このアプリでは、１つの画面内に多くの画像を並べることがあり、その数が多くなると表示までのタイムラグが大きくなります。かつ、その間、読み込み中の表示をしたりすることが難しいです。

UITableView/UICollectionView のように、コンテンツの数によらずパフォーマンスが一定になるような工夫を入れるのは、ビューをロジックで制御することになり、SwiftUI のメリットが薄れてしまいます。

### アイコンボタンの反応が悪い

iOS13 から [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/) という公式のアイコンセットが提供されて、このアプリでも利用しています。

このアイコンでボタンを作ったのですが、ただ `Image()`を使っただけではタップした時の反応が悪かったので、自分で大きさを調整しました。この辺りも、OS 側でよろしくやって欲しいところ。

```Swift
.navigationBarItems(
  leading: Button(action: {
    ...
  }, label: {
    Image(systemName: "plus")
      .font(.system(size: 22.0))
      .frame(minWidth: 44.0, maxWidth: .infinity,
             minHeight: 44.0, alignment: .center)
  })
)
```

### iPad での ActionSheet

iPhone で ActionSheet を表示するコードを書いて、それを iPad で実行すると、Popover で表示されるのですが、その位置を指定することができず、想定外の場所に表示されました。

下記は、Master ビューの右上のアイコンをタップした時の表示。

| iPhone           | iPad              |
|:----------------:|:-----------------:|
| ![Simulator Screen Shot - iPhone 11 - 2020-04-05 at 15.41.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4617/44fe2c66-36da-a1e1-0107-b5238da533d3.png) |              ![Simulator Screen Shot - iPad Pro (9.7-inch) - 2020-04-05 at 15.41.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4617/82a6c4b5-7064-2a57-8710-d0344f643b08.png) |

同じコードでここまで対応してくれているのでいいのですが、できれば Popover の場所を指定できるようにして欲しいところ。別のコードを書いて Popover を表示させることもできるのですが、そのとき、Popover の中に ActionSheet を表示することができないので、現在は、このままにしています（一応 Apple の審査は通過しています）。

## 今後の期待

と、困った点を中心に書いてみましたが、もちろんいい点もあります。

- UI をコードでシンプルに記述でき、ロジックと明確に分離できる。
- Combine との組み合わせで、リアクティブなコードを書ける。
- View のプレビューができる。
- Dark Mode やアクセシビリティなどへの対応が容易。
- iPhone/iPad だけでなく、他の Apple プロダクトともコードを共有できる。

まだリリースされて１年も経たないフレームワークなので、いろいろな面で不十分なのは仕方ないと思っていますが、おそらく、７月の WWDC で大きなアップデートがあると思うので、そこでの進化を期待しています。

## リンク

### ドキュメント

- [SwiftUI | Apple Developer Documentation](https://developer.apple.com/documentation/swiftui)
- [SwiftUI Tutorials — Tutorials | Apple Developer Documentation](https://developer.apple.com/tutorials/swiftui/tutorials)
- [SF Symbols - SF Symbols - Human Interface Guidelines - Apple Developer](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/)

### 開発した系の記事

- [ICカードリーダーのiOSアプリをSwiftUIで開発しました | Money Forward Engineers' Blog](https://moneyforward.com/engineers_blog/2019/09/20/ic-cardreader-swiftui/)
- [新卒エンジニアがSwiftUIで個人アプリをリリースしました📚 - Qiita](https://qiita.com/tsuzuki817/items/59c3e5bc620035db4caf)
- [SwiftUI で iOS アプリを個人開発＆ストアに公開した - Daisuke TONOSAKI Weblog](https://daisuke-t-jp.hatenablog.com/entry/2020/01/07/app-income-tax)
- [SwiftUIアプリ開発実践ポイント - Qiita](https://qiita.com/tattn/items/920f17868ee3f97ea17d)

### その他参考にした記事

- [【iOS】Combineフレームワークまとめ - Qiita](https://qiita.com/shiz/items/5efac86479db77a52ccc)
- [Using Combine](https://heckj.github.io/swiftui-notes/)
- [SwiftUIおすすめライブラリ!! - Qiita](https://qiita.com/shira-shun/items/492dcdd79ed9dde09bf6#partialsheet-%EF%B8%8F202)