Help us understand the problem. What is going on with this article?

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

※注意
- 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上でのグリッドデザインを、下記のように簡単に作成できるようになりました。

grid example

(Quote: WWDC2020 - What's new in SwiftUI / 10:11~

1. Documentation

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

スクリーンショット 2020-06-24 17.15.39 2.png

(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で同じ画面を再現したものです

スクリーンショット 2020-06-24 15.18.10.png

  • 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の数だけ、カラムが生成されます。

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

3. GridItem

Documentation

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

スクリーンショット 2020-06-24 17.56.19.png

(Quote: Documentation - GridItem)

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

Variable

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

スクリーンショット 2020-06-24 19.49.24.png

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

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

になります。

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

① fixed

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

スクリーンショット 2020-06-24 20.43.17.png

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でタグが左寄せに並んでいるようなレイアウトを実現する

スクリーンショット 2020-06-24 20.43.17.png

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で同じ画面を再現したコードのログを取得する

スクリーンショット 2020-06-24 18.08.50.png

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

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

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

スクリーンショット 2020-06-24 18.33.23.png

(画像はイメージ図。Xcode11.5より)

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

5. PinnedScrollableViews

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

スクリーンショット 2020-06-24 17.50.13.png

(Quote: Documentation - PinnedScrollableViews)

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

スクリーンショット 2020-06-24 19.17.37 2.png

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

その他

リポジトリ
- SwiftUI_LazyGrid

ドキュメント
- Documentation - LazyHGrid
- Documentation - LazyVGrid
- Documentation - GridItem
- Documentation - GridItem.Size

外国の方がGridのレイアウトを解説している動画
- Building Grids in SwiftUI 2.0 for iOS 14

H_Crane
iOS Engineer。 Web → Android → iOSって感じで仕事してきました。 SQLを書いたり、rails、node、phpを触りサーバー周りをかじったり。 フリーランスでもベンチャー企業のアプリ開発にもコミットしてます。 共著「これからの「分析」の話をしよう」。 Swift / Android / firebase / JavaScript
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした