Reputation: 12972
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.
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):
Upvotes: 4
Views: 785
Reputation: 1
The most easier and logical way of making your code work, updating View before Animation take the control.
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() }
}
}
}
}
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
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
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
Upvotes: 2
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
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
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