LazyVStackを使用すると描画処理が何度も走ってしまう
概要
iOS14.0で利用できるようになったLazyVStackにより、SwiftUIでの開発がやりやすくなったように感じます。
しかし私が携わっているプロジェクトにて少し複雑なレイアウトをLazyVStack
を用いて組むと、スクロールがカクついてしまうような事象が発生しました。その一因として、LazyVStack
の中にどのようなコンポーネントを配置するかによって毎回レンダリング処理が走ってしまうことが挙げられそうでした。
今回は自分がハマったコンポーネントの配置方法を紹介したいと思います。
Stack+ForEach
LazyVStack
の中にVStack
/HStack
/ZStack
を配置して、さらにその中でForEach
を使うケースは多々あるのかなと思います。
例えば文字列を表示するシンプルなレイアウトを組んでみます。
再描画される書き方
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack {
VStack {
ForEach(0..<10, id: \.self) { i in
Text("Section1-\(i)") // ここでブレークポイントを貼ると毎回停止する。
.frame(height: 50)
}
.frame(maxWidth: .infinity)
.background(.blue.opacity(0.2))
}
VStack {
ForEach(10..<20, id: \.self) { i in
Text("Section2-\(i)")
.frame(height: 50)
}
.frame(maxWidth: .infinity)
.background(.red.opacity(0.2))
}
VStack {
ForEach(20..<30, id: \.self) { i in
Text("Section3-\(i)")
.frame(height: 50)
}
.frame(maxWidth: .infinity)
.background(.green.opacity(0.2))
}
VStack {
ForEach(30..<39, id: \.self) { i in
Text("Section4-\(i)")
.frame(height: 50)
}
.frame(maxWidth: .infinity)
.background(.yellow.opacity(0.2))
}
}
}
}
}
この書き方で問題なのは以下のようにLazyVStack
の配下にVStack
を配置し、そのVStack
スコープ内にForEach
を配置しているところです。
この書き方をするとForEach
スコープに配置したコンポーネントは毎回描画されてしまいます。
Text("Section1-\(i)")
部分にブレークポイントを貼ると、画面に表示されるたびに停止します。
そこで以下のようにVStackを外すと初回のみ描画が行われ、その後何回スクロールしてもブレークポイントで停止することはありません。
再描画されない書き方
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(0..<10, id: \.self) { i in
Text("Section1-\(i)")
.frame(height: 50)
}
.frame(maxWidth: .infinity)
.background(.blue.opacity(0.2))
ForEach(10..<20, id: \.self) { i in
Text("Section2-\(i)")
.frame(height: 50)
}
.frame(maxWidth: .infinity)
.background(.red.opacity(0.2))
ForEach(20..<30, id: \.self) { i in
Text("Section3-\(i)")
.frame(height: 50)
}
.frame(maxWidth: .infinity)
.background(.green.opacity(0.2))
ForEach(30..<39, id: \.self) { i in
Text("Section4-\(i)")
.frame(height: 50)
}
.frame(maxWidth: .infinity)
.background(.yellow.opacity(0.2))
}
}
}
}
Stack+SubView
次はLazyVStack
の中にStack
を配置して、更にその中にSubViewを配置するパターンです。
このパターンも先程と同様にText("SubView")
の部分でブレークポイントを貼ると画面に表示されるたびに停止します。
再描画される書き方
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack {
Color.red
.frame(maxWidth: .infinity)
.frame(height: 1500)
ZStack {
SubView(content: "1-1")
}
}
}
}
}
struct SubView: View {
var content: String
var body: some View {
Text("SubView") // ここでブレークポイントを貼ると毎回停止する。
}
}
SubViewの中で複雑なレイアウトを組んでいる場合やNukeUI[AsyncImage]や(https://developer.apple.com/documentation/swiftui/asyncimage)を使っている場合、表示されるたびに毎回描画されるのでカクつきの原因になってしまう可能性がありそうです。
ただSubView
をZStack
などのStack
で囲わずにLazyVStack
直下に配置すればSubViewは毎回描画されなくなります。
再描画されない書き方
struct ContentView2: View {
var body: some View {
ScrollView {
LazyVStack {
Color.red
.frame(maxWidth: .infinity)
.frame(height: 1500)
// LazyVStack直下に配置するだけ
SubView(content: "1-1")
}
}
}
}
TabView+ForEach
最後はTabViewとForach
を組み合わせるパターンです。
LazyVStack
の中にpageスタイルのTabView
を入れその要素をForEach
を用いて描画しようとすると、ForEach
スコープ内のコンポーネントが表示されるたびに描画されてしまいます。
再描画される書き方
struct ContentView: View {
private let images = [
"https://via.placeholder.com/300x300",
"https://via.placeholder.com/300x300",
"https://via.placeholder.com/300x300",
"https://via.placeholder.com/300x300",
]
@State private var currentImageIndex = 0
var body: some View {
ScrollView {
LazyVStack {
TabView(selection: $currentImageIndex) {
ForEach(images.indices, id: \.self) { index in
// ここでブレークポイントを貼ると毎回停止する。
AsyncImage(url: URL(string: images[index])!) { image in
image
} placeholder: {
ProgressView()
}
}
}
.frame(width: UIScreen.main.bounds.width, height: 300, alignment: .center)
.tabViewStyle(.page(indexDisplayMode: .never))
Color.red
.frame(maxWidth: .infinity)
.frame(height: 500)
Color.blue
.frame(maxWidth: .infinity)
.frame(height: 500)
Color.green
.frame(maxWidth: .infinity)
.frame(height: 500)
Color.yellow
.frame(maxWidth: .infinity)
.frame(height: 500)
}
}
}
}
ForEach
を使用しなければ、AsyncImage
は1度しか呼ばれなくなります。
再描画されない書き方
TabView(selection: $currentImageIndex) {
AsyncImage(url: URL(string: images[index])!) { image in
image
} placeholder: {
ProgressView()
}
AsyncImage(url: URL(string: images[index])!) { image in
image
} placeholder: {
ProgressView()
}
// 省略...
}
まとめ
LazyVStack
は便利そうに見えますが、コンポーネントの配置の仕方でパフォーマンスが悪くなってしまう場合があるように見えます。
同じようなレイアウトが繰り返されるデザインやリストのアイテムがシンプルなデザインの場合はLazyVStack
で対応できるかもしれませんが、複雑なレイアウトが組み合わされたデザインやアイテムが複雑なデザインの場合は、LazyVStack
を使っているとパフォーマンスが悪くなりカクつきの一因となってしまうかもしれません。
LazyVStack
を使っていて「カクつくなー」と感じた場合は、今回紹介したものを疑ってみるのも良いかもしれません。
ただ私の認識やLazyVStack
の使い方が間違っている可能性もあります。
このあたりについて見識があるかたはコメントいただけると幸いです。
なお私は複雑なデザインを組む場合はUIViewRepresentableでUICollectionView
をラップして、UICollectionViewCell
のViewはSwiftUI
で組むようにしています。
Discussion