I have a small List with around 20 elements in it. Each row in the list is a simple custom view with a few labels inside embedded in a NavigationLink.
struct RunList: View {
var model: RunModel
var body: some View {
List {
ForEach(model.runs) { run in
// NavigationLink(destination: RunOverview(run: run)) {. ** seems to have biggest impact on performance.
RunCell(run: run).frame(height: 100)
// }
}
}
.listStyle(CarouselListStyle())
.navigationBarTitle(Text("Demo App"))
}
}
Running this simple list on an Apple Watch using significant amounts of CPU when scrolling causing it to drop frames.
The performance seems to be significantly worse when each list item has a NavigationLink as the root view.Removing the navigation link reduces CPU usage by upto 50% and vastly improves performance on an Apple Watch Series 2 but we need the list rows to be clickable.
The app I am building is very similar in layout to PopQuiz demo app produced by Apple https://developer.apple.com/documentation/watchkit/creating_a_watchos_app_with_swiftui
Running the above sample code also exhibits the same issues.
I have profiled it in instruments and the bulk of the time seems to be in layout related code.
I appreciate the Apple Watch 2 is fairly old now but surely a basic list such as the above should be able to run performant. Other system apps on the device run well although it is unlikely they will be using swiftUI.
Are tips or gotchas I should be aware of?
Some ideas,
RunCell
. Make it conform to Equatable
if it's not already.struct RunCell: View, Equatable {
static func == (lhs: RunCell, rhs: RunCell) -> Bool {
lhs.run == rhs.run // or whatever is equal
}
...
RunCell(run: run).fixedSize(vertical: true).frame(height: 100)
compositingGroup
. There's probably something that can be done with the NavigationLink also, but not sure what. There's a SwiftUI component for Insturments, but I'm not sure it will give more insight than TimeProfiler here.
What about using one NavigationLink that gets activated and its information from RunCell TapGesture?
struct RunList: View {
var model: RunModel
@State var activeRun: Run?
@State var runIsActive = false
var body: some View {
List {
if activeRun != nil {
NavigationLink(destination: RunOverView(run: activeRun!, isActive: $runIsActive, label: {EmptyView()})
}
ForEach(model.runs) { run in
RunCell(run: run)
.frame(height: 100)
.onTapGesture {
self.activeRun = run
self.runIsActive = true
}
}
}
.listStyle(CarouselListStyle())
.navigationBarTitle(Text("Demo App"))
}
}
It might depend on how heavy is RunOverview.init
because all those navigation link views are constructed during this ForEach iteration (even thought not yet activated
You can try DeferView
from this solution to defer real destination construction to the moment when corresponding link activated
ForEach(model.runs) { run in
NavigationLink(destination: DeferView { RunOverview(run: run) }) {
RunCell(run: run).frame(height: 100)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With