1

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?

2
  • Clarification: In my original code I have about 40 RectViews (actually they are more complex than a rect). Only one or two of them are moving when the state changes. They are deeply nested in a hierarchy of ZStacks, VStacks, GeometryReaders etc. This is why I need to work with matchedGeometryEffect and offset. Commented Oct 7, 2021 at 12:26
  • Hi, I would really like to hear your final solution as I am having the same issue. I understand the reason that TabViews preserve their states and so the animation will be restarted but also uses its older state. I solved it by adding a flag inside the onAppear so that it will be only called once, but this seems a little bit hacky to me. Did you find a better way? Thx in advance :-) Commented Jan 22, 2024 at 16:05

2 Answers 2

0

I solved the issue via using a Bool flag which I set only once during the first call of onAppear() in subview.

More details here: Broken animation inside SwiftUI TabView

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

Comments

-1

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
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.