[Swift] SwiftUIの LazyVGrid / LazyHGrid について考察してみる

※注意
- Xcode 12 beta の環境で動作確認したものです。正式版では動作が変わる可能性もあります。
- [今 Swift や iOS 8 について書くのは NDA 違反か調べてみた](https://qiita.com/uasi/items/4d17cd1b7f83a6814c83)と同じ考えのもと、Appleが公開する画像・動画・コードおよびその拡張コードの添付は、問題ないと考えています。
- 本記事に出てくる画面スクリーンショットは、**全てXcode11.5およびiOS13で再現した画面**です。(Xcode12 betaおよびiOS14のものは１つもありません)

# はじめに

WWDC2020が初まり、興奮さめやらぬ人も多いのではないでしょうか。
Xcode12 Betaの配布も始まり、公式サイトではいろんな**APIリファレンス**が一気に展開されました。

前回のWWDC2019で注目を集めたSwiftUIですが、まだ課題も多くアップデートに期待がかかっていましたが。。
無事に、今回の発表で多くのAPIが追加されたようです！

その中でも、**LazyVGrid** / **LazyHGrid** についてフォーカスしてみました。


# LazyVGrid / LazyHGrid

WWDC2020の映像でも公開されていましたが、SwiftUI上でのグリッドデザインを、下記のように簡単に作成できるようになりました。

<img width="1000" alt="grid example" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/004f12ee-98ec-7ce1-4cc4-9d9912df376d.png">

（Quote: [WWDC2020 - What's new in SwiftUI / 10:11~](https://developer.apple.com/videos/play/wwdc2020/10041)）


### 1. Documentation

まずは公式のドキュメントから覗いてみます。

<img width="1533" alt="スクリーンショット 2020-06-24 17.15.39 2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/bc44da77-197e-5a50-8192-75354e38e450.png">

（Quote: [Documentation - LazyVGrid](https://developer.apple.com/documentation/swiftui/lazyvgrid)）

名前で予想はついたかと思いますが、それぞれ**縦方向・横方向にグリッドを組める仕組み**が用意されています。
また、それぞれに同じ記載のあるこの部分

> The grid is “lazy,” in that the grid view does not create items until they are needed.

**必要になるまで**Viewの生成がなされないようです。
こちらに関しては後で説明していきます。

### 2. Code Example

まずは簡単な例をみてもらうのが、一番わかりやすいかと思います。
今回は``LazyVGrid``で例を作成しました。
（**※スクリーンショットはXcode11.5で同じ画面を再現したものです**）

<img width="300" alt="スクリーンショット 2020-06-24 15.18.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/44b3573a-cb29-8358-b431-e2ce3e097ed7.png">

- Xcode11.5で同じ画面を再現したコード

```.swift
ScrollView {
    ForEach((0...24), id: \.self) { row in
        HStack {
            ForEach((1...4), id: \.self) { column in
                Text("\(row*4+column)")
                    .frame(width: 80, height: 60) // widthは目視で同じになるように任意の値を設定
            }
        }.frame(maxWidth: .infinity)
    }
}
```

- LazyVGridを使用したコード

```.swift
ScrollView {
    LazyVGrid(columns: Array(repeating: GridItem(), count: 4)) { // カラム数の指定
        ForEach((1...100), id: \.self) { index in
            Text("\(index)")
                .frame(width: 60, height: 60)
        }
    }
}
```

columnsにある``GridItem``の数だけ、カラムが生成されます。

以前は``ForEach``や``Stack``を用いて、再現したいグリッド部分を入れ子にすることで画面を構成する必要がありました。
``LazyVGrid``では**指定したcolumns数で区切ってくれる**ようになり、コードもすっきりしてだいぶ**明示的**になったのではないでしょうか？


### 3. GridItem

#### Documentation

先ほどのコードにも記載ありましたが、``LazyVGrid``/``LazyHGrid``で画面を構成するためには、必ず``GridItem``を渡してあげる必要があります。

<img width="800" alt="スクリーンショット 2020-06-24 17.56.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/7f052c7f-d06c-8fd0-0b21-5e22ae19055b.png">

(Quote: [Documentation - GridItem](https://developer.apple.com/documentation/swiftui/griditem))

プロパティを見ても分かる通り、**レイアウトの調整**に使用します。
イメージとしては、UICollectionViewのLayoutに近いと思います。

#### Variable

この``GridItem``を使うことで、**可変のグリッド**も作り出すことが可能です。
``GridItem``を生成する際の引数として``GridItem.Size``を指定することで可能になります。
また、サイズの最小値・最大値の設定もこちらで行います。

<img width="800" alt="スクリーンショット 2020-06-24 19.49.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/2b0f916d-ec2b-a0d9-f296-fd62b925ee6f.png">

- Quote: [Documentation - GridItem.Size](https://developer.apple.com/documentation/swiftui/griditem/size-swift.enum)

基本的には上記の３タイプで、ざっくり説明すると

```
① fixed    : グリッドのサイズを固定で設定
② flexible : グリッドのサイズを最小値〜最大値で設定
③ adaptive : グリッドのサイズを最小値〜最大値で設定し、アイテムを詰めて設置
```

になります。

ぞれぞれ具体例を見ていきましょう。

##### ① fixed

（**※スクリーンショットはXcode11.5で同じ画面を再現したものです**）

<img width="200" alt="スクリーンショット 2020-06-24 20.43.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/c9d399c5-ee5c-218e-53f0-126974e2b4b0.png">

<details><summary>Xcode11.5でほぼ同じ画面を再現したコードは**こちら**</summary><div>

```.swift
ScrollView {
    ForEach((0...24), id: \.self) { row in
        HStack {
            Text("\(row*4)").frame(width: 10, height: 60)
            Text("\(row*4+1)").frame(width: 30, height: 60)
            Text("\(row*4+2)").frame(width: 20, height: 60)
        }.frame(maxWidth: .infinity)
    }
}
```

</div></details>



```.swift
ScrollView {
    LazyVGrid(columns: [GridItem(.fixed(10)), GridItem(.fixed(30)), GridItem(.fixed(20))]) { // GridItemが３つなので３カラム
        ForEach((1...100), id: \.self) { index in
            Text("\(index)")
        }
    }
}
```

各columnごとに**固定値**を設定できるようになっています。

##### ② flexible

「① fixed」が可変になった形です。

```.swift
ScrollView {
    LazyVGrid(columns: [GridItem(.flexible(minimum: 30, maximum: 100)), GridItem(.flexible(minimum: 0, maximum: 10))]) { // GridItemが2つなので2カラム
        ForEach((1...100), id: \.self) { index in
            Text("\(index)")
        }
    }
}
```
各columnごとに**最小値**〜**最大値**を設定できるようになっています。

##### ③ adaptive

※こちらは既存のもので再現できなかったので、できることのイメージ画像を添付します。
Quote: [UICollectionViewでタグが左寄せに並んでいるようなレイアウトを実現する](https://qiita.com/kazuhiro4949/items/03bc3d17d3826aa197c0)

<img width="200" alt="スクリーンショット 2020-06-24 20.43.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/8f9049ee-58d0-cd3c-9dad-59d0009b01e7.png">


```.swift
ScrollView {
    LazyVGrid(columns: [GridItem(.adaptive(minimum: 100, maximum: 200))]) {
        ForEach((1...100), id: \.self) { index in
            Text("\(index)")
        }
    }
}
```

１つ設定するだけで、設定した最小値〜最大値で**アイテムを詰めて表示**してくれるようになります。
複数の``adaptive``を設定した場合は**OS側がよしなに分割する**ようです（何回か試したがそんな感じだった）

### 4. Lazy

「1. Documentation」で説明しなかったこの部分

> The grid is “lazy,” in that the grid view does not create items until they are needed.

このViewは**Lazy**がついていることにすごく意味があります。
それは**画面の呼び出しタイミング**が違うということです。

下記のコードを、それぞれ新・旧のSwiftUIのコードに追加して、挙動の違いを確認していきたいと思います。

```.swift
.onAppear {
    debugPrint("onAppear: \(/* current index */)")
}
.onDisappear {
    debugPrint("onDisappear: \(/* current index */)")
}
```

``onAppear``/``onDisappear``を用いて**画面の表示・非表示のタイミング**でログを出すようにします。

#### Xcode11.5で同じ画面を再現したコードのログを取得する


<img width="600" alt="スクリーンショット 2020-06-24 18.08.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/4016b535-84e5-48d1-da33-61005e5d045c.png">

今までのグリッド実装の場合、画面生成と共に**すべてのindex**が取得されました。
つまり、画面生成と共に**すべての画面（Text）が呼び出されている**と言うことになります。


#### LazyVGridを使用したコードのログを取得する

既存の**Listと同じ**動きをします。
つまり、アイテムが**表示されたタイミングonAppearが発火**し、**非表示になったタイミングでonDisappearが発火**します。

<img width="200" alt="スクリーンショット 2020-06-24 18.33.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/219dd2a2-8e83-8439-f694-5830d142ccdb.png">

（画像はイメージ図。Xcode11.5より）


実際の開発においては、複雑なViewが膨大に並ぶことになるかと思います。
そのため、古い実装ではパフォーマンスとして良くないため実戦投入には不向きでした。
その部分において、Gridを使用して**パフォーマンスの改善**をすることができそうです。

### 5. PinnedScrollableViews

名前からなんとなく想像がつきそうな、つかなそうなと言う感じですが、
セクションヘッダー・セクションフッターを**固定するかどうか**指定するためのプロパティになります。

<img width="400" alt="スクリーンショット 2020-06-24 17.50.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/62b67f86-46e0-be86-980b-b6fb0b8cd7ff.png">

(Quote: [Documentation - PinnedScrollableViews](https://developer.apple.com/documentation/swiftui/pinnedscrollableviews))

試しにヘッダーを固定したものを用意しました。
（**※スクリーンショットはXcode11.5で同じ画面を再現したものです**）

<img width="600" alt="スクリーンショット 2020-06-24 19.17.37 2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/54542ae6-ee99-1bad-3200-d0d02dc4209e.png">


```.swift
ScrollView {
    LazyVGrid(columns: Array(repeating: GridItem(), count: 4), pinnedViews: .sectionHeaders) { // 固定する方を指定
        Section(header: Text("header")) { // セクション
            ForEach((1...100), id: \.self) { index in
                Text("\(index)")
                    .frame(width: 60, height: 60)
            }
        }
    }
}
```

引数に追加するだけで**とても簡単**です。
イメージとしては、``UICollectionElementKindSection Header/Footer``に近いと思います。
この引数を指定しない場合は、セクションはそのままスクロールされてしまいます。

# 終わりに
今までのSwiftUIでは``UICollectionView``に相当するものがなかったため、開発においてハック的なやり方で、だいぶ無理をする必要がありました。
今回フォーカスしたGridで完全に補えているかどうかは怪しいところですが。。。

WWDC2020の発表から、SwiftUIのAPIがたくさん増えたことで、開発の幅が広がりました。
一部の機能はSwiftUIでしか開発できないことを鑑みても、ここ２・３年でSwiftUIへ移行はmustになってきそうです。

間違いがあるかもしれないので、指摘あればお願いしますmm

## その他
リポジトリ
- [SwiftUI_LazyGrid](https://github.com/crane-hiromu/SwiftUI_LazyGrid)

ドキュメント
- [Documentation - LazyHGrid](https://developer.apple.com/documentation/swiftui/lazyhgrid) 
- [Documentation - LazyVGrid](https://developer.apple.com/documentation/swiftui/lazyvgrid) 
- [Documentation - GridItem](https://developer.apple.com/documentation/swiftui/griditem)
- [Documentation - GridItem.Size](https://developer.apple.com/documentation/swiftui/griditem/size-swift.enum)

外国の方がGridのレイアウトを解説している動画
- [Building Grids in SwiftUI 2.0 for iOS 14](https://www.youtube.com/watch?v=qNVApzV9V1Q)