simibac
simibac

Reputation: 8590

Broken Animation SwiftUI

I initially had this question here. The solution proposed by @arsenius was working for this toy example. However, my application is more complex and it took me forever to find out where the animation breaks. In the example I used two animated HStack. But if I replace these HStack with two different (!) custom views, the animation is broken again.

Here is the code:

class State:ObservableObject{
    @Published var showSolution = false
}

struct ContentView: View {
    @EnvironmentObject var state:State

    var body:some View {
        VStack {
            if state.showSolution{
                CustomToggleOne()
                    .background(Color.red)
                    .id("one")
                    .animation(Animation.default)
                    .transition(.slide)
            } else {
                CustomToggleTwo()
                    .background(Color.yellow)
                    .id("two")
                    .animation(Animation.default.delay(2))
                    .transition(.slide)
            }
        }
    }
}

struct CustomToggleOne: View{
    @EnvironmentObject var state:State

    var body:some View{
        HStack{
            Spacer()
            Button(action:{
                withAnimation {
                    self.state.showSolution.toggle()
                }
            }){
                Text("Next")
            }.padding()
            Spacer()
        }
    }
}

struct CustomToggleTwo: View{
    @EnvironmentObject var state:State

    var body:some View{
        HStack{
            Spacer()
            Button(action:{
                withAnimation {
                    self.state.showSolution.toggle()
                }
            }){
                Text("Next")
            }.padding()
            Spacer()
        }
    }
}

I added an instance of State to the ContentView in SceneDelegate.swift as an EnvironmentObject as follows:

let contentView = ContentView().environment(\.managedObjectContext, context).environmentObject(State())

The expected animation can be seen when we use CustomToggleOne() twice in the ContentView instead of CustomToggleTwo().

Upvotes: 3

Views: 2170

Answers (1)

Asperi
Asperi

Reputation: 258107

Here is the approach that is correct, some explanations below and in comments. Tested with Xcode 11.2 / iOS 13.2

demo

Reminder: don't test transitions in Preview - only on Simulator or Device

// !! Don't name your types as API,
// State is SwiftUI type, might got unpredictable weird issues
class SolutionState:ObservableObject{
    @Published var showSolution = false
}

struct TestBrokenAnimation: View {
    @EnvironmentObject var state:SolutionState

    var body:some View {
        VStack {
            if state.showSolution{
                CustomToggleOne()
                    .background(Color.red)
                    .id("one")
                    .transition(.slide) // implicit animations confuse transition, don't use it
            } else {
                CustomToggleTwo()
                    .background(Color.yellow)
                    .id("two")
                    .transition(.slide)
            }
        }
    }
}
public func withAnimation<Result>(_ animation: Animation? = .default, 
              _ body: () throws -> Result) rethrows -> Result

as it seen withAnimation is not state, which just activate animations defined via modifier, it explicitly applies specified animation, own, so having more in modifiers will definitely result in some conflicts.

So using only explicit animations with transitions.

struct CustomToggleOne: View{
    @EnvironmentObject var state:SolutionState

    var body:some View{
        HStack{
            Spacer()
            Button(action:{
                withAnimation(Animation.default.delay(2)) {
                    self.state.showSolution.toggle()
                }
            }){
                Text("Next")
            }.padding()
            Spacer()
        }
    }
}

struct CustomToggleTwo: View{
    @EnvironmentObject var state:SolutionState

    var body:some View{
        HStack{
            Spacer()
            Button(action:{
                withAnimation() {
                    self.state.showSolution.toggle()
                }
            }){
                Text("Next")
            }.padding()
            Spacer()
        }
    }
}

Upvotes: 4

Related Questions