Broken SwiftUI animation when using TabView

480 Views Asked by At

I have a working animation in a SwiftUI view, which breaks, when I put that view into a TabView, go to the other tab and come back. How can I fix it?

My view is more complex, but I boiled it down to the following:

import SwiftUI

@main
struct AnimationTestApp: App {
    var body: some Scene {
        WindowGroup {
            TabView {
                ContentView()
                    .tabItem {
                        Text("Tab 1")
                    }
                Text("Content 2")
                    .tabItem {
                        Text("Tab 2")
                    }
            }
        }
    }
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    @Namespace var namespace

    var body: some View {
        VStack {
            Button(action: {
                viewModel.move()
            }) {
                Text("Move")
            }

            ZStack() {
                Rectangle()
                    .fill(.gray)
                    .frame(width: 300, height: 300, alignment: .center)
                if viewModel.moved {
                    RectView()
                        .matchedGeometryEffect(id: 0, in: namespace, properties: .frame)
                        .animation(Animation.default.speed(0.5))
                        .offset(x: 0, y: -130)

                } else {
                    RectView()
                        .matchedGeometryEffect(id: 0, in: namespace, properties: .frame)
                        .animation(Animation.default.speed(0.5))
                        .offset(x: 0, y: 130)
                }
            }
        }
    }
}

struct RectView: View {
    var body: some View {
        Rectangle()
            .frame(width: 100, height: 30, alignment: .center)
    }
}


final class ViewModel: ObservableObject {
    @Published var moved = false

    func move() {
        moved = !moved
    }
}

Here is an screen recording of that code. The RectView animates smoothly up and down when tapping Move. After switching to Tab 2 and going back, the first time I tap Move, it jumps without animation. The next taps are animated again.

Animation

In reality the view model is more complex. The state (in this case: moved) is changed some time after hitting the button. Some work is done on a background thread and then the state change is triggered on the main thread. This is why I can't move the animation into the Button action.

Also the view is more complex. The RectView is removed from deep in the view hierarchy and added somewhere entirely else.

The method func animation(_ animation: Animation?) is deprecated in iOS 15. The problem is already existing in iOS 14 forever. I also tried removing the animation modifiers and put the moved = !moved into an withAnimation { } block. Still the same result.

How could I fix that while keeping the TabView?

1

There are 1 best solutions below

0
On

You had some bad coding issue, here the working code, also you do not need matchedGeometryEffect here:

import SwiftUI

@main
struct AnimationTestApp: App {
    var body: some Scene {
        WindowGroup {
            TabView {
                ContentView()
                    .tabItem {
                        Text("Tab 1")
                    }
                Text("Content 2")
                    .tabItem {
                        Text("Tab 2")
                    }
            }
        }
    }
}

struct ContentView: View {
    
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        
        VStack {
            
            Button(action: { viewModel.moved.toggle() }) {
                Text("Move")
            }

            ZStack() {

                Rectangle()
                    .fill(Color.gray)
                    .frame(width: 300, height: 300, alignment: .center)
                
                RectView()
                    .offset(x: 0, y: viewModel.moved ? -130 : 130)
                    .animation(Animation.default.speed(0.5), value: viewModel.moved)
                
            }
        }
    }
}

struct RectView: View {
    var body: some View {
        Rectangle()
            .frame(width: 100, height: 30)
    }
}


final class ViewModel: ObservableObject {
    @Published var moved: Bool = false
}