SwiftUI学習メモ

[『1人でアプリを作る人を支えるSwiftUI開発レシピ』](https://www.amazon.co.jp/dp/B08NCXF81P/)などを読み、SwiftUIを軽く触ったときのメモ。(※理解度がまだ浅いので、間違っている記載がある可能性があります)

<img width="805" alt="Screen Shot 2020-12-01 at 4 17 23 PM" src="https://user-images.githubusercontent.com/1296652/100710040-756b4a80-33f2-11eb-9c00-3911bc1537a7.png">

出典 : [Xcode \- SwiftUI \- Apple Developer](https://developer.apple.com/jp/xcode/swiftui/)

## 概要

- WWDC2019で発表
- iOS13以上で利用可能
  - iOS14 SDKからさらにAPI追加されてる。実質使える状態なのはiOS14からかな...
- iOS14から、フルSwiftUIでアプリ作れるようになった
- iOS14から登場したWidget機能を実装するには、SwiftUIがマストで必要

※[チュートリアル](https://developer.apple.com/tutorials/swiftui/)が充実してる

## 特徴

- 宣言的UI
- データバインディング (ステートドリブンなシステム)
- リアルタイムのViewレイアウトプレビュー

ReactやFlutterやってる人には馴染みやすいはず

### 今までのビュー作成との違い

従来は、命令型 (宣言型の逆) プログラミングでビューを作っていた

```swift
let imageView = UIImageView(image: UIImage(named: "unko"))
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 10
imageView.clipsToBounds = true
view.addSubview(imageView)
```

↑こんな感じのコードが、↓以下のように。
宣言的に書いて、View修飾子 (ex. `.cornerRadius`) をチェーンさせて新しいViewを返していく。

```swift
Image("unko")
    .cornerRadius(10)
```

プレビューもXcodeで出せる！
※ただし、すぐ止まったりする（謎

<img width="884" alt="Screen Shot 2020-12-01 at 2 11 17 PM" src="https://user-images.githubusercontent.com/1296652/100710054-78663b00-33f2-11eb-9a12-488e1c61342e.png">

また、ビューの重ねていき方のパラダイムも異なる。
SwiftUIでは、親Viewが表示可能領域・子の配置位置を決め、子Viewは自身のView領域を決める。つまり、親は子のサイズ決定には基本的に関与しない。子が自身のサイズを決める

### コンポーネント

例えば [\[Swift\] SwiftUIのチートシート \- Qiita](https://qiita.com/hcrane/items/eb847ca7fb7a0b9e8073) にUIKitとの対応がけっこう書いてある。

こういうの見て、あとは実際に書いて理解してくしかないかな...

## アプリの作成

プロジェクトを新しく作成すると、以下のようなファイルが自動でできる。

```swift
import SwiftUI

@main
struct SwiftUISampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

`@main`がエントリポイントを表す。

- App -> Scene (WindowGroup) -> View という構成で作る
- 今のところiPhoneアプリの場合はSceneが1つ
  - iPadアプリではマルチウィンドウ機能があるのでSceneが2つとかになる
- アプリの状態検知は、`scenePhase`つかう (↓のようなイメージ)

```swift
@main
struct SwiftUISampleApp: App {
    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onChange(of: scenePhase) { newScenePhase in
                    switch newScenePhase {
                    case .background:
                        ...
                    }
                }
        }
    }
}
```

## データ管理

'Property Wrapper'というやつでデータをViewにバインディングする機能が標準で備わってる

例えばシンプルな例。

```swift
struct ToggleView: View {
    @State private var isOn = false

    var body: some View {
        VStack {
            Toggle("スイッチON", isOn: $isOn)
            Text("\(isOn ? "ON" : "OFF")")
        }
        .frame(width: 160, height: 64)
    }
}
```

![SwiftUI_ToggleView](https://user-images.githubusercontent.com/1296652/100710066-7bf9c200-33f2-11eb-8690-a3bb5d835215.gif)

`@State`というPropertyWrapperをつけたプロパティに`$`をつけることで、データがバインディング対象になる

### Property Wrapper8種類

データは何か、データはどのように処理するか、データはどこからくるか、で用途を分ける

- `@State`
  - 扱うデータが値型 and View自身でデータを保持して更新する場合に使う
- `@Binding`
  - 扱うデータが値型 and 親Viewなど外部からデータを渡され更新する場合に使う
  - `@State`のデータの先頭に `$` マークをつけることで、 `@Binding`のデータに変換可能
- `@Environment`
  - 環境値を読み取る場合に使う。KeyPathを指定して読み込み
- `@StateObject`
  - ※iOS14以降
  - 扱うデータが参照型 and View自身でデータを保持して更新する場合に使う
  - データ更新→再レンダリングの際も、Viewインスタンスは破棄されない（=最初の一度しか作成されない）
- `@ObservedObject`
  - 扱うデータが参照型 and 親Viewからデータを渡され更新する場合に使う
  - こっちはViewインスタンスが再レンダリング時に破棄される
- `@EnvironmentObject`
  - 階層を飛び越えてViewにデータオブジェクトを渡せる
  - `@ObservedObject`のバケツリレーを避けられる
- ※ `ObservableObject`プロトコル
  - 参照型のデータを監視する場合、対象のオブジェクトはObservableObjectプロトコルに準拠しないといけない
- `@AppStorage`
  - 値型のデータを格納できる
  - 格納先はUserDefaults。ライフサイクルがアプリが消されるまで
- `@SceneStorage`
  - マルチウインドウをサポートするアプリで、シーンごとに値型のデータを格納できる
  - 使い方は、`@AppStorage`とほぼ同じ

## Combine

こちらもWWDC19で発表されたフレームワーク。

- データ処理を宣言的に扱えるようになり、非同期イベントのハンドリングがしやすくなる
- RxSwiftやReactiveSwiftなど、Reactive系のフレームワークに馴染みのある人には扱いやすい？
  - ※まだ慣れてないので調べきれてないが、異なる部分や足りない部分も当然あるはず
- SwiftUIの備えるデータバインディング機構と相性がいいので、SwiftUIにAPI通信処理etc.を組み込む際はCombineの利用が前提になるはず

登場人物はざっくり3つ

- Publisher
- Subscriber
- Operator

役割分担はこんな感じで、RxSwiftとかの経験あるなら概念は理解しやすいと思う

- Publisherが発行するデータストリームをSubscriberが受け取る。
  - = 雑にいうとイベント駆動で処理を書ける
- Operatorは、両者の間でデータストリームの値の変換を行う。

例えば以下のようにCombineを使う。（他にもやり方はある）

```swift
import Combine

class Unko {
    var touched = PassthroughSubject<String, Never>()
}

let unko = Unko()
let subscriber = unko.touched.sink { str in
    print("Unko touch: \(str)")
}

unko.touched.send("1回目")
unko.touched.send("2回目")
subscriber.cancel()
unko.touched.send("3回目はない")
```

出力されるのは、

```
Unko touch: 1回目
Unko touch: 2回目
```

※Unkoクラスのtouchプロパティに、`@Published`をつけるやり方もある。
これをつけとくと、変数の値が更新されたタイミングで、監視してるSubscriberに更新が伝わる。

RxSwift, RxCocoaとのAPI対応表みたいのがあるので、Rxに慣れてる人はそれ見ながら覚えてくのが良さそう
[CombineCommunity/rxswift\-to\-combine\-cheatsheet: RxSwift to Apple’s Combine Cheat Sheet](https://github.com/CombineCommunity/rxswift-to-combine-cheatsheet)

## Widget

- iOS14の新機能。ホーム画面におけるようになったアレ
- Widgetに配置できるのはLabelやImageなど表示系のみ。スクロールとかスイッチは無理
- WidgetのUIイベントはタップのみ
  - アプリを起動できる
  - DeepLinkが設定できるので、特定の画面に遷移させられる

### 実装

アプリ本体のエクステンション (追加ターゲット) として、作成する

- エントリポイントで、`Widget`プロトコルに準拠
  - `WidgetConfiguration`プロトコルのインスタンスをbodyとして返す
    - `StaticConfiguration` : ユーザが設定を編集できない
    - `IntentConfiguration` : ユーザが設定を編集できる
- 表示 (更新) のロジックを`TimelineProvider`プロトコルが担う
  - WidgetはタイムラインにそってViewを更新
  - 3つのメソッドの実装が必要
    - `placeholder(in:)`
      - 初期表示
    - `getSnapshot(in:completion:)`
      - ホーム画面に追加されたときや、Widget Gallleryで表示されたとき
    - `getTimeline(in:completion:)`
      - タイムラインに沿った更新時など？
- タイムラインの更新ポリシーにも種類がある : `TimelineReloadPolicy`
  - `.atEnd`, `.after(_:)`, `.never`
- ※ホストアプリからWidgetKitを通して、更新処理をキックすることももちろんできる
  - `WidgetCenter`を使う

タップ時の処理は、DeepLinkの仕組みでゴニョゴニョする。`.widgetURL`修飾子を使う

1. Widget側に`.widgetURL`を追加して、特定のURLセット
2. ユーザがタップすると、ホストアプリで`.onOpenURL`が呼ばれURLを受け取る
3. ホスト側でURLをハンドリング

## 雑にまとめ

- 動きの少ないビューであれば、UIKitで作るよりもサクッと作れそう
  - 応用が効くかどうか。HIGにのっとって作るなら大丈夫かな...?
- TableViewやCollectionViewのときにメモリを効率的につかってくれるのかがちょい不安
  - iOS14でAPI増えて改善したようなので、たぶん大丈夫...
- Combineとの組み合わせで、必然的にデータバインディングを利用したリアクティブプログラミングで作ることになる
  - ちゃんと作れば、状態の複雑さに起因するバグが確実に減る
  - （たぶん）デファクトにある程度なってるRxSwift系の流れを汲みつつ、RxSwift依存から離れられる
- アプリがiOS14以上サポートになるまで（あと1-2年くらい？）は、既存アプリに組み込むのは面倒が増えるだけかも
  - 練習的に、OSバージョンで条件分岐させて簡単な画面作ってみるのはあり。シンプルな設定画面とか
- 新規アプリで採用するどうかはもう少し使ってみて判断したい。...が、UIKitとのハイブリッド構成もいけるので、困ったらそっちに逃げれる？
- 既存アプリに突っ込むなら、Widget機能つくるタイミングが一番な気がする
  - そもそもSwiftUI使わないとリリースできないので

とりあえずもっと使ってみたい！ (あと、RxSwiftをCombineでリプレイスしたい)