7

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?

6
  • Do you observe this only on device or on simulator as well? Commented May 20, 2020 at 3:50
  • Yeah there is a clear increase in CPU usage in the sim when having these navigationLinks around each cell. Commented May 20, 2020 at 9:45
  • Which Xcode / watchOS versions do you use, because I actually do not observe what you described with 11.4 / 6.2 Commented May 20, 2020 at 15:49
  • 11.4.1 and 6.2.5. The sample app PopQuiz - developer.apple.com/videos/play/wwdc2019/219 exhibits similar performance issues. Commented May 20, 2020 at 20:54
  • I think we need to know the content of RunOverview and RunCell as anything you do in those two views can affect the performance (using shadows etc.). When you use NavigationLink it increases the CPU usage as using NavigationLink renders the destination right away. I strongly suggest you to share what you do in those two views (at least roughly). Commented May 26, 2020 at 10:52

3 Answers 3

5

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the suggestions. I will definitely look into these. They sound like they could help somewhat.
5
+25

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"))
    }
}

Comments

4

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)
    }
}

1 Comment

Thanks for the suggestion. I tried something very similar to this called LazyView from Objc.io objc.io/blog/2019/07/02/lazy-loading Sadly neither seem to help. The RunOver has nothing but a run property and a body.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.