SwiftUI中的过渡动画无法正常工作。

92

我正在尝试创建一个非常简单的过渡动画,通过点击按钮来显示/隐藏屏幕中央的消息:

struct ContentView: View {
    @State private var showMessage = false
    
    var body: some View {
        ZStack {
            Color.yellow
            
            VStack {
                Spacer()
                Button(action: {
                    withAnimation(.easeOut(duration: 3)) {
                        self.showMessage.toggle()
                    }
                }) {
                    Text("SHOW MESSAGE")
                }
            }                
            if showMessage {
                Text("HELLO WORLD!")
                    .transition(.opacity)
            }
        }
    }
}

根据.transition(.opacity)动画的文档:

插入时从透明到不透明过渡,并在移除时从不透明到透明。

showMessage状态属性变为true时,该消息应该淡入,当其变为false时则淡出。但在我的情况下并非如此。该消息出现时有淡入动画,但隐藏时完全没有动画。有什么想法吗?

编辑:查看下面从模拟器中获取的gif结果。

enter image description here

10个回答

188

问题在于当视图在ZStack中出现和消失时,它们的“zIndex”并不保持不变。 当“showMessage”从真变为假时,带有“Hello World”文本的VStack被放置在堆栈底部,黄色颜色立即在其上绘制。 它实际上正在淡出,但是它在黄色颜色后面这样做,所以你看不到它。

要解决此问题,您需要明确指定堆栈中每个视图的“zIndex”,以便它们始终保持不变-像这样:

struct ContentView: View {
    @State private var showMessage = false
    
    var body: some View {
        ZStack {
            Color.yellow.zIndex(0)
            
            VStack {
                Spacer()
                Button(action: {
                    withAnimation(.easeOut(duration: 3)) {
                        self.showMessage.toggle()
                    }
                }) {
                    Text("SHOW MESSAGE")
                }
            }.zIndex(1)
            
            if showMessage {
                Text("HELLO WORLD!")
                    .transition(.opacity)
                    .zIndex(2)
            }
        }
    }
}

你不需要为所有视图添加zIndex,因为在具有许多视图的复杂视图中,这非常困难。我只需向要动画的视图添加一个zIndex,一切都可以正常工作。 - Sajjad Hajavi

104

我的发现是不透明度过渡效果并不总是有效的。(然而,与 .animation 结合使用的幻灯片可以正常工作。)

.transition(.opacity) //does not always work

如果我将其编写为自定义动画,则可以工作:

.transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2))) 
.zIndex(1)

4
我可以确认这个有效。奇怪的是,SwiftUI 上的某些实现不一致。 - Jandro Rojas
5
顺便提一下,截至目前(iOS 14 / XCode 12),不需要 zIdex。 - Jandro Rojas
4
这实际上非常有趣 - 如果您在代码的其他地方使用 withAnimation(),并且对于转换只使用 .opacity,则会得到单向动画 - 仅在移出层次结构时起作用。当您按照此答案所告诉的那样使用它时,它可以双向工作。这不是一种错误吗? - iSpain17
4
哇,有人为此提交了雷达吗?SwiftUI中的转换非常有问题。 - bze12
2
这个有文档吗?看起来像是一个错误。 - Xaxxus
显示剩余5条评论

37
我在SwiftUI的预览中发现了一个动画bug。当你在代码中使用过渡动画,并希望在SwiftUI的预览中看到它时,它将不会显示动画,或者只会在某个视图以动画方式消失时显示。要解决这个问题,你只需要将你的视图添加到一个VStack中进行预览,就像这样:
struct test_UI: View {
    @State var isShowSideBar = false
    var body: some View {
        ZStack {
            Button("ShowMenu") {
                withAnimation {
                    isShowSideBar.toggle()
                }
                
            }
            if isShowSideBar {
                SideBarView()
                    .transition(.slide)
            }
        }
    }
}
struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
           SomeView()
        }
    }
}

在此之后,所有的动画将会发生。

4
这真的非常奇怪,但它有效!谢谢。 - mattsven
2
到了2022年底,这个漏洞仍然存在。谢谢! - Martin
2
令人难以置信。Xcode 15已经发布了,但这个bug仍然存在。 - undefined

14

我认为这是与画布相关的问题。今天早上我在尝试过渡效果时发现,虽然画布上不起作用,但它们似乎在模拟器中有效。你可以试试看。我已经向苹果报告了这个 bug。


3
这正是我的问题!我在想这段代码错在哪儿了。非常感谢你! - Rizwan Ahmed
1
已确认截至2020年12月30日仍存在此问题。 - Legolas Wang
1
两年后仍然存在问题。我正在使用Xcode 14.0.1,必须在预览提供程序中使用VStack来包装视图。 - cleanrun
2023年1月仍然存在问题。 - Zain

11

我更喜欢Scott Gribben的答案(见下文),但由于无法删除此答案(因为被绿色勾选),我将保留原始答案。不过,我认为这确实是一个bug。人们预期zIndex应该由视图在代码中的出现顺序隐式分配。


要解决此问题,您可以将if语句嵌套在VStack中。

struct ContentView: View {
    @State private var showMessage = false

    var body: some View {
        ZStack {
            Color.yellow

            VStack {
                Spacer()
                Button(action: {
                    withAnimation(.easeOut(duration: 3)) {
                        self.showMessage.toggle()
                    }
                }) {
                    Text("SHOW MESSAGE")
                }
            }

            VStack {
                if showMessage {
                    Text("HELLO WORLD!")
                        .transition(.opacity)
                }
            }
        }
    }
}

为什么这段代码能够执行?可以再详细解释一下吗? - whistler
@whistler 不是真的,这只是一种解决程序漏洞的方法。测试不同方案的结果是希望框架代码使用不同的路径来应用转换。 - kontiki
兄弟,真的很奇怪,如果我删除 Color.yellow,它就像预期的那样工作。我会提交反馈。 - Mark Moeykens
这个问题让我发疯了。 - Andoni Da Silva
@kontiki:这非常诚实的你。 - Chris

2
以下代码应该可以正常工作。
import SwiftUI

struct SwiftUITest: View {
    
    @State private var isAnimated:Bool = false
  
    var body: some View {
        ZStack(alignment:.bottom) {
            VStack{
                Spacer()
                Button("Slide View"){
                    withAnimation(.easeInOut) {
                        isAnimated.toggle()
                    }
                    
                }
                Spacer()
                Spacer()
           
            }
            if isAnimated {
                RoundedRectangle(cornerRadius: 16).frame(height: UIScreen.main.bounds.height/2)
                    .transition(.slide)

            }
            
            
        }.ignoresSafeArea()
    }
}

struct SwiftUITest_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            SwiftUITest()
        }
    }
}

2
我将放弃使用`.transition`,因为它不起作用。相反,我会动画化视图的偏移量,这更加可靠:
首先,我会创建一个偏移量的状态变量:
@State private var offset: CGFloat = 200

其次,我将VStack的偏移量设置为它。然后,在其.onAppear()中,我使用动画将偏移量改回0:

        VStack{
            Spacer()
            HStack{
                Spacer()
                Image("MyImage")
            }
        }
        .offset(x: offset)
        .onAppear {
            withAnimation(.easeOut(duration: 2.5)) {
                offset = 0
            }
        }

2

zIndex可能会在中断时导致动画停止。将您想要应用转换的视图包装在一个VStackHStack或任何其他容器中是有意义的。


0

你应该放置

 .id(showMessage) 

在你的VStack主体之后,这应该会对你有所帮助。


0

SwiftUI自定义不透明度转场

您可以为inout不透明度转场创建扩展。

import SwiftUI

extension AnyTransition {
    static var inOpacity: AnyTransition {
        AnyTransition.modifier(
                        active: OpacityModifier(opacity: 0),
                      identity: OpacityModifier(opacity: 1)
        )
    }
    static var outOpacity: AnyTransition {
        AnyTransition.modifier(
                        active: OpacityModifier(opacity: 1),
                      identity: OpacityModifier(opacity: 0)
        )
    }
}
struct OpacityModifier : ViewModifier {
    let opacity: Double
    
    func body(content: Content) -> some View {
        content.opacity(opacity)
    }
}

enter image description here

struct ContentView : View {
    
    @State var isMessageVisible: Bool = false
    @State var text = Text("SwiftUI Transition Animation")
                          .font(.largeTitle)
                          .foregroundColor(.yellow)
    
    var body: some View {
        ZStack {
            Color.indigo.ignoresSafeArea()
            
            VStack {
                Spacer()
                Button("SHOW MY MESSAGE") {
                    withAnimation(.linear(duration: 2)) {
                        isMessageVisible.toggle()
                    }
                }
            }
            if isMessageVisible {
                text.transition(.inOpacity)       // in
            } else {
                text.transition(.outOpacity)      // out
            }
        }
    }
}

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