过渡期间视图未更新

4

我需要您的帮助来理解一个问题。我无法让视图在转换动画之前重新绘制其主体。看一下这个简单的例子:

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()
    }
}

我对这个示例的期望是:当我点击按钮时,视图会重新创建其主体,文本“Hello”变为红色。现在,视图再次创建其主体并进行过渡。相反,似乎SwiftUI以某种方式合并了两个状态更改,只考虑第二个状态更改。结果是过渡发生了,但文本“Hello”不会改变颜色。

enter image description here

我该如何在SwiftUI中处理这种情况?有没有办法告诉框架分别更新这两个状态变化?谢谢。
@Asperi的编辑
我尝试了你的代码,但它不起作用。结果仍然是相同的。这是带有你的代码的完整示例:
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()
    }
}

这是在 iPhone 12 Mini iOS 14.1 上的结果:

enter image description here

5个回答

2

只用修饰符是无法实现您想要的效果的。

因为当更改视图状态时,带有Text("Hello")的代码块不会被调用,这就是它开始消失并出现您指定的过渡效果的原因(因为此视图在视图层次结构中丢失)。

因此,最好的方法是实现自定义过渡。首先,您需要创建一个具有所需行为的ViewModifier:

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)
    }
}

然后使用这个修饰符创建一个转换 - 在这里,您需要为两个状态指定修饰符:
extension AnyTransition {
    static func foregroundColor(active: Color, identity: Color) -> AnyTransition {
        AnyTransition.modifier(
            active: ForegroundColorModifier(foregroundColor: active),
            identity: ForegroundColorModifier(foregroundColor: identity)
        )
    }
}

将颜色过渡与滑动效果结合:

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

最后,如果您只需要在消失时更改颜色,请使用不对称过渡:
.transition(
    .asymmetric(
        insertion: .slide,
        removal: AnyTransition.slide.combined(
            with: .foregroundColor(
                active: .red,
                identity: .black
            )
        )
    )
)

完整代码:

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()
                }
            }
        }
    }
}

我不确定为什么在这种情况下指定.transition后面的.animation不起作用,但在withAnimation块内更改状态会按预期工作。 最终结果

1
谢谢你的回答,非常有趣。实际上我不想让“Hello”在黑色和红色之间动画变化,我想让“Hello”立即变成红色,然后动画应该开始(这在问题中已经写了)。无论如何,只需要将你的 ForegroundColorModifier 中的内容更改为 content.foregroundColor(foregroundColor) 就可以了。 - superpuccio

1
主要问题在于使用Group - 它不是一个容器,而是应该使用一些真正的容器并直接对视图应用过渡效果,例如:

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()
        }
    }
}

嗯...我刚试了一下你的解决方案,我不知道为什么它对我不起作用。如果我逐字逐句地复制/粘贴你的代码,结果与我在问题中展示的gif相同。你的环境是什么?我使用的是Xcode 12.2, iPhone 12 Pro Max 和iOS 14.2模拟器。 - superpuccio
我会在iOS 14.1上尝试,然后告诉你。 - superpuccio
真的很奇怪。正如您从我的编辑中所看到的,即使在iOS 14.1上使用您的代码,我仍然得到了相同的“错误”结果。 - superpuccio
嗯...看着你的GIF,有些奇怪。它应该以黑色的“Hello”开头(第一次点击时应该变成红色),但根据我的例子,它却以黑色的“world”开头。请考虑从我的EDIT中复制/粘贴整个示例。谢谢。 - superpuccio
嗨Asperi,你有机会研究这个问题吗?非常感谢。 - superpuccio

1
在编写代码时,最简单、最合乎逻辑的方式是在动画控制之前更新视图。

enter image description here

版本 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() }
                
            }
            
        }
    }
}

版本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()
                
            }
            
        }
    }
}


1
说实话,我不喜欢使用延迟的解决方案。可以说这不是我会在生产环境中采用的东西。相反,你的第二个解决方案非常有趣。谢谢。 - superpuccio

1
这是一个可能的解决方案:

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()
                }
            }
        }
    }
}

这里的想法是,通过在withAnimation内部更改condition(相对于没有动画的前景色更改有延迟),来传播由condition更改引起的转换。

在按钮的操作中使用额外的动画来制造延迟!为什么不在截止日期后使用DispatchQueue?在您的代码中似乎没有必要使用withAnimation!因为我们已经在VStack上有了动画! - ios coder
是的,没错。但是已经有一个使用 DispatchQueue 的答案了,所以你可以把它看作另一种可能的解决方案。 - Cuneyt
嗯... 实际上这个解决方案并不错。在按钮关闭时,您基本上是在说:“我想要fgColor在没有任何动画的情况下改变,并且我希望动画所有基于condition值的内容”,这正是我想要的。此外,请注意,您可以删除.animation(.easeOut(duration:3))行,一切仍然按预期工作,因此您甚至没有额外的“动画指令”。 - superpuccio
谢谢@matteopuc,是的,实际上删除.animation(.easeOut(duration: 3))这一行会使它更清晰。 - Cuneyt

-2

试试这个,


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()
    }
}

你的解决方案实际上并没有回答问题。在问题中,我们有一个if/else语句来显示“Hello”或“World”单词。在你的解决方案中,有“Wello”这个词和一个if语句来显示“World”这个词。这是不同的情况,不能作为问题的解决方案(如果你在代码中放置一个if/else,你会看到我在问题中遇到的完全相同的错误)。 - superpuccio

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接