In SwiftUI, when using ForEach blocks, AnyView should not directly be used as the first type of the ForEach block.
In order for SwiftUI to have best performances, it needs to know at compile-time the "tree" of Views it is going to display.
Using the type-erased AnyView prevents SwiftUI from knowing what's inside the AnyView, like a black-box, and this can lead to performance issues, especially when displaying lists of elements, even when lazily loaded inside a List.
To prevent from having this issue, AnyView should be avoided as returned elements in ForEach blocks, and @ViewBuilder along with some View should be preferred instead.
This is a mistake often made when starting to use SwiftUI and trying to create an intermediary func or var to provide a SwiftUI View, like in this situation:
struct ContentView: View {
var body: some View {
List {
ForEach(0...10_000, id: \.self) { _ in
cellView()
}
}
}
// this does not compile, the compiler suggests using `any View`, which won't work anyway because when `cellView()` is called
// the compiler needs to know which underlying type is sent by cellView
func cellView() -> View {
return RandomColorView()
}
}
A quick fix might be:
struct ContentView: View {
var body: some View {
List {
ForEach(0...10_000, id: \.self) { _ in
cellView()
}
}
}
func cellView() -> AnyView {
return AnyView(RandomColorView())
}
}
This will compile, but now SwiftUI only has a type-erased AnyView inside the ForEach block and won't be able to smartly detect it is always a RandomColorView that is used under the hood and won't make any optimisation when scrolling through the List.
As we can see in this video (performed on a real device iPhone 12 Pro):
It takes more than one minute and a half to scroll through the whole list.
This video was analyzed in parallel with Instruments to check CPU performances, here are the results below:
We can see that often the CPU reaches 100% usage, and hangs several times.
If instead we use the following code which relies on @ViewBuilder and some View, which tells the IDE that the underlying type will be known at compile-time:
struct ContentView: View {
var body: some View {
List {
ForEach(0...10_000, id: \.self) { _ in
cellView()
}
}
}
@ViewBuilder
func cellView() -> some View {
RandomColorView()
}
}
We can see in the video below (same iPhone 12 Pro):
Rule title
Avoid usage of
AnyView
insideForEach
.Language and platform
Swift 5.0+, iOS 13+
Rule description
In SwiftUI, when using
ForEach
blocks,AnyView
should not directly be used as the first type of the ForEach block.In order for SwiftUI to have best performances, it needs to know at compile-time the "tree" of
View
s it is going to display.Using the type-erased
AnyView
prevents SwiftUI from knowing what's inside theAnyView
, like a black-box, and this can lead to performance issues, especially when displaying lists of elements, even when lazily loaded inside aList
.To prevent from having this issue,
AnyView
should be avoided as returned elements inForEach
blocks, and@ViewBuilder
along withsome View
should be preferred instead.Bad code:
Good code:
Rule short description
AnyView
should be avoided as returned elements inForEach
blocks, and@ViewBuilder
along withsome View
should be preferred instead.Rule justification
Assuming the following existing code:
This is a mistake often made when starting to use SwiftUI and trying to create an intermediary
func
orvar
to provide a SwiftUIView
, like in this situation:A quick fix might be:
This will compile, but now SwiftUI only has a type-erased AnyView inside the
ForEach
block and won't be able to smartly detect it is always aRandomColorView
that is used under the hood and won't make any optimisation when scrolling through theList
.As we can see in this video (performed on a real device iPhone 12 Pro):
https://github.com/green-code-initiative/ecoCode-challenge/assets/5621515/8281d4fc-595d-4bf6-a55d-e83eac8be7f6
It takes more than one minute and a half to scroll through the whole list.
This video was analyzed in parallel with Instruments to check CPU performances, here are the results below:
We can see that often the CPU reaches 100% usage, and hangs several times.
If instead we use the following code which relies on
@ViewBuilder
andsome View
, which tells the IDE that the underlying type will be known at compile-time:We can see in the video below (same iPhone 12 Pro):
https://github.com/green-code-initiative/ecoCode-challenge/assets/5621515/0639906c-6f55-4b21-a08e-9020014e5ddb
that performances are dramatically better, as it takes only 10 seconds to reach the bottom of the 10k elements list.
This is confirmed by Instruments as well:
as there are no hangs, and CPU sometimes reaches 100% but for very short amounts of time.
Similar investigation results can be found in this article: https://martinmitrevski.com/2024/01/02/anyviews-impact-on-swiftui-performance/
Instruments trace file: AnyView_CPUTest_OK.trace.zip
Severity / Remediation Cost
Severity: Major (depends of on the size of the list)
Remediation: Medium
Remediation consists in using
@ViewBuilder
and@some View
in the function / var signature.Implementation principle
In a
ForEach
loop, if a singleView
is returned, ensure the returned type in the block is NOTAnyView
.Valid triggering examples: