SwiftUI Animations inside NavigationView

I experienced rendering problems when using animations inside a NavigationView. The same animations work fine when no NavigationView is present.

See my example code below. There are two previews, one working, the other one not. Unfortunately, this behavior is not limited to previews but also happens when the app runs in simulator or on a real device.

Am I doing something wrong? Any help would be appreciated. Thanks!

import SwiftUI

struct MyView: View {

    @State private var isAnimating = false

    var body: some View {
        let animation = Animation
            .linear
            .repeatForever(autoreverses: false)

        return Image(systemName: "iphone")
            .rotationEffect(.degrees(isAnimating ? 360 : 0))
            .animation(animation)
            .onAppear { isAnimating = true }
    }
}



struct MyView_Previews: PreviewProvider {

    static var previews: some View {
        Group {
            MyView()
                .previewDisplayName("Working")

            NavigationView {
                MyView()
            }
            .previewDisplayName("Not Working")
        }
    }
}

(Xcode12.5, iOS14.5)

Answered by Monterey in 687852022

After 3 months I finally found a solution which I would like to share with you. There are two steps you need to take to get it to work:

  1. Make the animation explicit. That means: use .withAnimation{} instead of .animation().
  2. Dispatch to main queue (DispatchQueue.main.async{}) before calling .withAnimation. I have no idea why this is needed.

There is an article by javier on swift-lab called "Advanced SwiftUI animations" that helped me a lot. (Unfortunately the forum doesn't allow me to add a link.)

Here is the working result:

import SwiftUI

struct ContentView: View {

    @State private var isAnimating = false

    var body: some View {
        let animation = Animation
            .linear
            .repeatForever(autoreverses: false)

        return Image(systemName: "iphone")
            .rotationEffect(.degrees(isAnimating ? 360 : 0))
            .onAppear {
                DispatchQueue.main.async {
                    withAnimation(animation) {
                        isAnimating = true
                    }
                }
            }
    }
}

struct ContentView_Previews: PreviewProvider {

    static var previews: some View {
        Group {
            ContentView()
                .previewDisplayName("Working")

            NavigationView {
                ContentView()
            }
            .previewDisplayName("Now also working")
        }
    }
}
Accepted Answer

After 3 months I finally found a solution which I would like to share with you. There are two steps you need to take to get it to work:

  1. Make the animation explicit. That means: use .withAnimation{} instead of .animation().
  2. Dispatch to main queue (DispatchQueue.main.async{}) before calling .withAnimation. I have no idea why this is needed.

There is an article by javier on swift-lab called "Advanced SwiftUI animations" that helped me a lot. (Unfortunately the forum doesn't allow me to add a link.)

Here is the working result:

import SwiftUI

struct ContentView: View {

    @State private var isAnimating = false

    var body: some View {
        let animation = Animation
            .linear
            .repeatForever(autoreverses: false)

        return Image(systemName: "iphone")
            .rotationEffect(.degrees(isAnimating ? 360 : 0))
            .onAppear {
                DispatchQueue.main.async {
                    withAnimation(animation) {
                        isAnimating = true
                    }
                }
            }
    }
}

struct ContentView_Previews: PreviewProvider {

    static var previews: some View {
        Group {
            ContentView()
                .previewDisplayName("Working")

            NavigationView {
                ContentView()
            }
            .previewDisplayName("Now also working")
        }
    }
}

I have a similar issue with lists in navigationViews. seems any animation causes rendering issues. I experienced it on WatchOS where the app becomes unresponsive when scrolling back to top, right when the navigation bar size is changed.

struct TestView: View {
    private let items: [String] = Array(0...20).map { String($0) }

    var body: some View {
        NavigationView {
            List {
                Section(header: Text("header")) {
                    ForEach(items, id: \.self) { item in
                        Text(item)
                    }
                }
            }
            .navigationTitle("Title")
        }
//        .animation(.easeInOut) // <- this causes rendering problems
//        .transition(.slide)
    }
}
SwiftUI Animations inside NavigationView
 
 
Q