【问题标题】:View not updating during a transition在过渡期间视图不更新
【发布时间】:2020-11-23 14:36:31
【问题描述】:

我需要您的帮助来了解我遇到的问题。我无法让视图在过渡动画之前重绘其主体。看看这个简单的例子:

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”不会改变它的颜色。

如何在 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 上):

【问题讨论】:

    标签: ios swift swiftui


    【解决方案1】:

    只用修饰符是不可能做你想做的事的。

    因为当更改视图状态时,带有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)
        }
    }
    

    然后使用这个修饰符创建一个Transition——这里你需要为两个状态指定修饰符:

    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 块内的状态按预期工作

    【讨论】:

    • 感谢您的回答,非常有趣。实际上我不希望“Hello”在黑色和红色之间进行动画处理,我希望“Hello”立即变为红色,然后动画应该开始(它写在问题中)。无论如何,只需将您的ForegroundColorModifier 更改为content.foregroundColor(foregroundColor) 即可。
    【解决方案2】:

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

    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 更改引起的过渡(与没有动画的前景颜色更改相比,这是延迟的)。

    【讨论】:

    • 在 Button 的动作中使用额外的动画来制造延迟!为什么在截止日期之后不DispatchQueue?在您的代码中似乎没有必要使用 withAnimation !因为我们已经在 VStack 上有了动画!
    • 是的,没错,但 DispatchQueue 的答案已经存在,因此您可以将其视为另一种可能的解决方案。
    • 嗯...这个解决方案其实一点也不差。在按钮关闭中,您基本上是在说:“我希望fgColor 更改没有任何动画,并且我想为基于condition 值的所有内容设置动画”,这正是我正在寻找。此外,考虑到您可以删除 .animation(.easeOut(duration: 3)) 行,一切仍然按预期工作,因此您甚至没有额外的“动画指令”。
    • 感谢@matteopuc,是的,实际上删除.animation(.easeOut(duration: 3)) 行使这一点更加清晰。
    【解决方案3】:

    主要问题在于使用Group - 它不是一个容器,而是使用一些真正的容器并直接将转换应用于视图,例如

    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 模拟器上。
    • 我会在 iOS 14.1 上试试,我会告诉你的。
    • 真的很奇怪。正如您从我的编辑中看到的那样,即使您在 iOS 14.1 上使用您的代码,我也会得到相同的“错误”结果。
    • 嗯...看着你的 GIF 有一些奇怪的东西。它以黑色的“世界”开头,但根据我的示例,它应该以黑色的“Hello”开头(第一次点击时应该变成红色)。请考虑从我的编辑中复制/粘贴我的整个示例。谢谢。
    • 嗨 Asperi,您有机会探讨这个问题吗?非常感谢。
    【解决方案4】:

    让你的代码工作的最简单和合乎逻辑的方法,在动画控制之前更新视图。


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

    【讨论】:

    • 老实说,我不喜欢延迟的解决方案。假设这不是我放在生产环境中的东西。相反,您的第二个解决方案非常有趣。谢谢。
    【解决方案5】:

    试试这个,

    
    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,您将看到与我在问题中遇到的完全相同的错误)。
    猜你喜欢
    • 2011-07-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-29
    • 1970-01-01
    • 2015-06-15
    • 1970-01-01
    相关资源
    最近更新 更多