※注意
- Xcode 12 beta の環境で動作確認したものです。正式版では動作が変わる可能性もあります。
- 今 Swift や iOS 8 について書くのは NDA 違反か調べてみたと同じ考えのもと、Appleが公開する画像・動画・コードおよびその拡張コードの添付は、問題ないと考えています。
- 本記事に出てくる画面スクリーンショットは、全てXcode11.5およびiOS13で再現した画面です。(Xcode12 betaおよびiOS14のものは1つもありません)
はじめに
WWDC2020が初まり、興奮さめやらぬ人も多いのではないでしょうか。
Xcode12 Betaの配布も始まり、公式サイトではいろんなAPIリファレンスが一気に展開されました。
前回のWWDC2019で注目を集めたSwiftUIですが、まだ課題も多くアップデートに期待がかかっていましたが。。
無事に、今回の発表で多くのAPIが追加されたようです!
その中でも、LazyVGrid / LazyHGrid についてフォーカスしてみました。
LazyVGrid / LazyHGrid
WWDC2020の映像でも公開されていましたが、SwiftUI上でのグリッドデザインを、下記のように簡単に作成できるようになりました。
(Quote: WWDC2020 - What's new in SwiftUI / 10:11~)
1. Documentation
まずは公式のドキュメントから覗いてみます。
(Quote: Documentation - 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で同じ画面を再現したものです)
- Xcode11.5で同じ画面を再現したコード
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を使用したコード
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
を渡してあげる必要があります。
(Quote: Documentation - GridItem)
プロパティを見ても分かる通り、レイアウトの調整に使用します。
イメージとしては、UICollectionViewのLayoutに近いと思います。
Variable
このGridItem
を使うことで、可変のグリッドも作り出すことが可能です。
GridItem
を生成する際の引数としてGridItem.Size
を指定することで可能になります。
また、サイズの最小値・最大値の設定もこちらで行います。
基本的には上記の3タイプで、ざっくり説明すると
① fixed : グリッドのサイズを固定で設定
② flexible : グリッドのサイズを最小値〜最大値で設定
③ adaptive : グリッドのサイズを最小値〜最大値で設定し、アイテムを詰めて設置
になります。
ぞれぞれ具体例を見ていきましょう。
① fixed
(※スクリーンショットはXcode11.5で同じ画面を再現したものです)
Xcode11.5でほぼ同じ画面を再現したコードは**こちら**
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)
}
}
ScrollView {
LazyVGrid(columns: [GridItem(.fixed(10)), GridItem(.fixed(30)), GridItem(.fixed(20))]) { // GridItemが3つなので3カラム
ForEach((1...100), id: \.self) { index in
Text("\(index)")
}
}
}
各columnごとに固定値を設定できるようになっています。
② flexible
「① fixed」が可変になった形です。
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でタグが左寄せに並んでいるようなレイアウトを実現する
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100, maximum: 200))]) {
ForEach((1...100), id: \.self) { index in
Text("\(index)")
}
}
}
1つ設定するだけで、設定した最小値〜最大値でアイテムを詰めて表示してくれるようになります。
複数の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のコードに追加して、挙動の違いを確認していきたいと思います。
.onAppear {
debugPrint("onAppear: \(/* current index */)")
}
.onDisappear {
debugPrint("onDisappear: \(/* current index */)")
}
onAppear
/onDisappear
を用いて画面の表示・非表示のタイミングでログを出すようにします。
Xcode11.5で同じ画面を再現したコードのログを取得する
今までのグリッド実装の場合、画面生成と共にすべてのindexが取得されました。
つまり、画面生成と共にすべての画面(Text)が呼び出されていると言うことになります。
LazyVGridを使用したコードのログを取得する
既存のListと同じ動きをします。
つまり、アイテムが表示されたタイミングonAppearが発火し、非表示になったタイミングでonDisappearが発火します。
(画像はイメージ図。Xcode11.5より)
実際の開発においては、複雑なViewが膨大に並ぶことになるかと思います。
そのため、古い実装ではパフォーマンスとして良くないため実戦投入には不向きでした。
その部分において、Gridを使用してパフォーマンスの改善をすることができそうです。
5. PinnedScrollableViews
名前からなんとなく想像がつきそうな、つかなそうなと言う感じですが、
セクションヘッダー・セクションフッターを固定するかどうか指定するためのプロパティになります。
(Quote: Documentation - PinnedScrollableViews)
試しにヘッダーを固定したものを用意しました。
(※スクリーンショットはXcode11.5で同じ画面を再現したものです)
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でしか開発できないことを鑑みても、ここ2・3年でSwiftUIへ移行はmustになってきそうです。
間違いがあるかもしれないので、指摘あればお願いしますmm
その他
リポジトリ
ドキュメント
- Documentation - LazyHGrid
- Documentation - LazyVGrid
- Documentation - GridItem
- Documentation - GridItem.Size
外国の方がGridのレイアウトを解説している動画