superpuccio
superpuccio

Reputation: 12972

View not updating during a transition

I need your help to understand an issue I'm having. I can't manage to make a view redraws its body just before a transition animation. Take a look at this simple example:

import SwiftUI

struct ContentView: View {
    @State private var condition = true
    @State private var fgColor = Color.black

    var body: some View {
        VStack {
            Group {
                if condition {
                    Text("Hello")
                        .foregroundColor(fgColor)
                } else {
                    Text("World")
                }
            }
            .transition(.slide)
            .animation(.easeOut(duration: 3))

            Button("TAP") {
                fgColor = .red
                condition.toggle()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

What I expected from this example was: when I tap on the button the view creates its body again and the text "Hello" becomes red. Now, the view creates its body another time and the transition happens. Instead, it seems that SwiftUI merges the two state changes somehow and only the second one is considered. The result is that the transition happens, but the text "Hello" won't change its color.

enter image description here

How can I manage a situation like this in SwiftUI? Is there a way to tell the framework to update the two state changes separately? Thank you.

EDIT for @Asperi:

I tried your code but it doesn't work. The result is still the same. This is the complete example with your code:

import SwiftUI

struct ContentView: View {
    @State private var condition = true
    @State private var fgColor = Color.black

    var body: some View {
        VStack {
            VStack {
                if condition {
                    Text("Hello")
                        .foregroundColor(fgColor)
                                .transition(.slide)
                } else {
                    Text("World")
                        .transition(.slide)
                }
            }
            .animation(.easeOut(duration: 3))

            Button("TAP") {
                fgColor = .red
                condition.toggle()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

And this is the result (on iPhone 12 Mini iOS 14.1):

enter image description here

Upvotes: 4

Views: 785

Answers (5)

swiftPunk
swiftPunk

Reputation: 1

The most easier and logical way of making your code work, updating View before Animation take the control.


enter image description here

Version 1.0.0:

import SwiftUI

struct ContentView: View {
    
    @State private var condition = true
    @State private var fgColor = Color.black
    
    var body: some View {
        
        VStack(spacing: 40.0) {
            
            Group {
                
                if condition {
                    
                    Text("Hello")
                        .foregroundColor(fgColor)
                    
                }
                else {
                    
                    Text("World")
                }
                
            }
            .transition(.slide)
            .animation(.easeOut(duration: 3))
            
            
            Button("TAP") {
                
                fgColor = .red
                
                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(50)) { condition.toggle() }
                
            }
            
        }
    }
}

Version 2.0.0:

import SwiftUI

struct ContentView: View {
    
    @State private var condition: Bool = true
    @State private var fgColor: Color = Color.green
    @State private var startTransition: Bool = Bool()
    
    var body: some View {
        
        VStack(spacing: 40.0) {
            
            Group {
                
                if condition {
                    
                    Text("Hello")
                        .foregroundColor(fgColor)
                    
                }
                else {
                    
                    Text("World")
                }
                
            }
            .transition(.slide)
            .animation(.easeOut(duration: 3))
            .onChange(of: startTransition) { _ in condition.toggle() }
            
            
            Button("TAP") {
                
                if fgColor == Color.red { fgColor = Color.green } else { fgColor = Color.red }
                
                startTransition.toggle()
                
            }
            
        }
    }
}

Upvotes: 1

Cuneyt
Cuneyt

Reputation: 981

Here is a possible solution:

struct ContentView: View {

    @State private var condition = true
    @State private var fgColor = Color.black

    var body: some View {
        VStack {
            Group {
                if condition {
                    Text("Hello")
                        .foregroundColor(fgColor)
                } else {
                    Text("World")
                }
            }
            .transition(.slide)
            .animation(.easeOut(duration: 3))

            Button("TAP") {
                fgColor = .red

                withAnimation {
                    condition.toggle()
                }
            }
        }
    }
}

The idea here is that you propagate the transition that was caused by condition change by changing it inside withAnimation (which is delayed compared to the foreground color change with no animation).

Upvotes: 1

Phil Dukhov
Phil Dukhov

Reputation: 87834

It's impossible to do what you want with just modifiers.

Because when change the view state, the code block with Text("Hello") doesn't get's called, and that's the reason it starts disappearing with transition you've specifier(because this view is missing in the view hierarchy)

So the best way you can do it is implementing your custom transition. At first you need to create a ViewModifier with your desired behavior:

struct ForegroundColorModifier: ViewModifier {
    var foregroundColor: Color
    
    func body(content: Content) -> some View {
        content
            // for some reason foregroundColor for text is not animatable by itself, but can animate with colorMultiply
            .foregroundColor(.white)
            .colorMultiply(foregroundColor)
    }
}

Then create a Transition using this modifier - here you need to specify modifiers for two states:

extension AnyTransition {
    static func foregroundColor(active: Color, identity: Color) -> AnyTransition {
        AnyTransition.modifier(
            active: ForegroundColorModifier(foregroundColor: active),
            identity: ForegroundColorModifier(foregroundColor: identity)
        )
    }
}

Combine color transition with a slide one:

.transition(
    AnyTransition.slide.combined(
        with: .foregroundColor(
            active: .red,
            identity: .black
        )
    )
)

Finally If you need color change only on disappearing, use asymmetric transition:

.transition(
    .asymmetric(
        insertion: .slide,
        removal: AnyTransition.slide.combined(
            with: .foregroundColor(
                active: .red,
                identity: .black
            )
        )
    )
)

Full code:

struct ContentView: View {
    @State private var condition = true
    
    var body: some View {
        VStack {
            Group {
                if condition {
                    Text("Hello")
                    
                } else {
                    Text("World")
                }
            }
            .transition(
                .asymmetric(
                    insertion: .slide,
                    removal: AnyTransition.slide.combined(
                        with: .foregroundColor(
                            active: .red,
                            identity: .black
                        )
                    )
                )
            )
            
            Button("TAP") {
                withAnimation(.easeOut(duration: 3)) {
                    condition.toggle()
                }
            }
        }
    }
}

I'm not sure why specifying .animation after .transition doesn't work in this case, but changing state inside withAnimation block works as expected

Final result

Upvotes: 2

Shahriar Nasim Nafi
Shahriar Nasim Nafi

Reputation: 1356

Try this,


struct ContentView: View {
    @State private var condition = true
    @State private var fgColor = Color.black
    
    var body: some View {
        VStack {
            HStack {
                Text("Hello")
                    .foregroundColor(fgColor)
                if !condition {
                    Text("World")
                }
            }
            .transition(.slide)
            .animation(.easeOut(duration: 3))
            
            Button("TAP") {
                fgColor = .red
                condition.toggle()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Upvotes: -2

Asperi
Asperi

Reputation: 257709

The main issue is in using Group - it is not a container, instead use some real container and apply transitions to views directly, like

demo

var body: some View {
    VStack {
        VStack {
            if condition {
                Text("Hello")
                    .foregroundColor(fgColor)
                            .transition(.slide)
            } else {
                Text("World")
                    .transition(.slide)
            }
        }
        .animation(.easeOut(duration: 3))

        Button("TAP") {
            fgColor = .red
            condition.toggle()
        }
    }
}

Upvotes: 1

Related Questions