Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI List slow performance with NavigationLinks on watchOS

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.

enter image description here

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?

like image 264
bencallis Avatar asked May 17 '20 15:05

bencallis


3 Answers

Some ideas,

  1. Avoid unnecessary redrawing of 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
    }
...
  1. Maybe fix the offered size of the List elements

RunCell(run: run).fixedSize(vertical: true).frame(height: 100)

  1. If all RunCell views look roughly the same in terms of properties, put the List in a 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.

like image 106
Cenk Bilgen Avatar answered Nov 11 '22 15:11

Cenk Bilgen


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"))
    }
}
like image 27
TheLegend27 Avatar answered Nov 11 '22 16:11

TheLegend27


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)
    }
}
like image 39
Asperi Avatar answered Nov 11 '22 14:11

Asperi