如何在 SwiftUI 中动画/过渡文本值更改

Posted

技术标签:

【中文标题】如何在 SwiftUI 中动画/过渡文本值更改【英文标题】:How to animate/transition text value change in SwiftUI 【发布时间】:2019-12-03 12:53:41 【问题描述】:

我正在尝试使用 withAnimation 为文本中的值更改设置动画,但它似乎不起作用。我遇到了类似的问题,但 answer 没有为文本值设置动画。

我正在尝试在纯 SwiftUI 中重新创建此行为 (UIKit Example):

我已经尝试过这段代码,但它没有为文本更改设置动画:

struct TextAnimationView: View 
    @State private var textValue = "0"
    var body: some View 
        VStack (spacing: 50) 
            Text(textValue)
                .font(.largeTitle)
                .frame(width: 200, height: 200)
                .transition(.opacity)
            Button("Next") 
                withAnimation (.easeInOut(duration: 1)) 
                    self.textValue = "\(Int.random(in: 1...100))"
                
            
        
    

我对 SwiftUI 的经验很少,还有其他方法可以实现吗?

提前致谢:)

【问题讨论】:

【参考方案1】:

事实证明这真的很容易

Text(textValue)
  .font(.largeTitle)
  .frame(width: 200, height: 200)
  .transition(.opacity)
  .id("MyTitleComponent" + textValue)

请注意末尾的附加 id。 SwiftUI 在重绘时使用它来决定是否处理相同的视图。如果 id 不同,则假定前一个视图已被删除,而此视图已被添加。因为它正在添加一个新视图,所以它会按预期应用指定的转换。

注意:这个 id 很可能对于整个视图树应该是唯一的,因此您可能需要注意相应地为其命名空间(因此示例中的 MyTitleComponent 前缀)。

【讨论】:

非常好的解决方案和解释! 当您有像 MyView(data: self.model.currentData) 和 model.currentData 是 @Published 这样的自定义视图时,这很有帮助。你改变 this is model 和 MyView 改变,所以你想要一个过渡。添加 .id() 属性告诉 SwiftUI 这个视图实际上是一个不同的视图。谢谢! 对于那些不能让它工作的人(没有动画),尝试将文本包装在一个堆栈元素(VStack、HStack、ZStack)中。在我的情况下,仅包装在一个组中不起作用:(。在 Xcode 11.4 中测试。任何猜测为什么它会这样工作?【参考方案2】:

我找不到通过淡入淡出为文本值设置动画的方法。在设置 Text 的动画属性时,您将在动画时看到三个点 (...)。

至于现在,我想出了一个可以改变不透明度的解决方法:

@State private var textValue: Int = 1
@State private var opacity: Double = 1

var body: some View 
    VStack (spacing: 50) 
        Text("\(textValue)")
            .font(.largeTitle)
            .frame(width: 200, height: 200)
            .opacity(opacity)
        Button("Next") 
            withAnimation(.easeInOut(duration: 0.5), 
                self.opacity = 0
            )
            self.textValue += 1
            withAnimation(.easeInOut(duration: 1), 
                self.opacity = 1
            )
        
    

当您更改它时,它会淡出并淡入文本。

【讨论】:

这真的很有趣,但我一直在寻找“交叉淡入淡出”效果。感谢您的回答【参考方案3】:

下面是AnimatableModifier 的方法。它只会淡入新值。如果您也想淡出旧值,自定义修改器不会那么难。此外,由于您的显示值是数字,您可以将其本身用作控制变量,只需稍作修改。

这种方法不仅可以用于淡入淡出,还可以用于其他类型的动画,以响应视图值的变化。您可以将其他参数传递给修饰符。您也可以完全忽略body 中传递的content,并创建并返回一个全新的视图。在这种情况下,OverlayEmptyView 等也可以派上用场。

import SwiftUI

struct FadeModifier: AnimatableModifier 
    // To trigger the animation as well as to hold its final state
    private let control: Bool

    // SwiftUI gradually varies it from old value to the new value
    var animatableData: Double = 0.0

    // Re-created every time the control argument changes
    init(control: Bool) 
        // Set control to the new value
        self.control = control

        // Set animatableData to the new value. But SwiftUI again directly
        // and gradually varies it from 0 to 1 or 1 to 0, while the body
        // is being called to animate. Following line serves the purpose of
        // associating the extenal control argument with the animatableData.
        self.animatableData = control ? 1.0 : 0.0
    

    // Called after each gradual change in animatableData to allow the
    // modifier to animate
    func body(content: Content) -> some View 
        // content is the view on which .modifier is applied
        content
            // Map each "0 to 1" and "1 to 0" change to a "0 to 1" change
            .opacity(control ? animatableData : 1.0 - animatableData)

            // This modifier is animating the opacity by gradually setting
            // incremental values. We don't want the system also to
            // implicitly animate it each time we set it. It will also cancel
            // out other implicit animations now present on the content.
            .animation(nil)
    


struct ExampleView: View 
    // Dummy control to trigger animation
    @State var control: Bool = false

    // Actual display value
    @State var message: String = "Hi" 
        didSet 
            // Toggle the control to trigger a new fade animation
            control.toggle()
        
    

    var body: some View 
        VStack 
            Spacer()

            Text(message)
                .font(.largeTitle)

                // Toggling the control causes the re-creation of FadeModifier()
                // It is followed by a system managed gradual change in the
                // animatableData from old value of control to new value. With
                // each change in animatableData, the body() of FadeModifier is
                // called, thus giving the effect of animation
                .modifier(FadeModifier(control: control))

                // Duration of the fade animation
                .animation(.easeInOut(duration: 1.0))

            Spacer()

            Button(action: 
                self.message = self.message == "Hi" ? "Hello" : "Hi"
            ) 
                Text("Change Text")
            

            Spacer()
        
    


struct ExampleView_Previews: PreviewProvider 
    static var previews: some View 
        ExampleView()
    

【讨论】:

【参考方案4】:

这是使用标准转换的方法。字体大小、帧、动画持续时间可根据您的需要进行配置。 Demo 只包含重要的方法。

struct TestFadeNumbers: View 
    @State private var textValue: Int = 0

    var body: some View 
        VStack (spacing: 50) 
            if textValue % 2 == 0 
                Text("\(textValue)")
                    .font(.system(size: 200))
                    .transition(.opacity)
            
            if textValue % 2 == 1 
                Text("\(textValue)")
                    .font(.system(size: 200))
                    .transition(.opacity)
            
            Button("Next") 
                withAnimation(.linear(duration: 0.25), 
                    self.textValue += 1
                )
            
            Button("Reset") 
                withAnimation(.easeInOut(duration: 0.25), 
                    self.textValue = 0
                )
            
        
    

【讨论】:

你能详细说明我们为什么要做textValue % 2 == 0textValue % 2 == 1。只是为了动画效果吗? 转换在视图出现/消失时被激活,所以这里我们需要一个视图出现和一个视图消失......奇数/偶数分隔仅针对此数字相关任务。 数字计数器是一个非常特殊的案例。如果您想使用任何其他类型的文本,只需使用布尔状态并在每次更改文本时切换它。此外,为简单起见,您可以用 else 语句替换 second if :) 我有两个问题。 1)为什么新数字没有动画,而旧数字却淡出? 2) 如果您使用else 而不是新的条件,为什么会有不同的行为?【参考方案5】:

如果@Tobias Hesselink 的代码不起作用,考虑使用这个:

@State var MyText = "Hello, world?"

    VStack 
        Text(MyText)
            .transition(AnyTransition.opacity.combined(with: .scale))
            .id("MyTitleComponent" + MyText)
        Button("Button") 
            withAnimation(.easeInOut(duration: 1.0)) 
                MyText = "Vote for my post?"
            

【讨论】:

以上是关于如何在 SwiftUI 中动画/过渡文本值更改的主要内容,如果未能解决你的问题,请参考以下文章

在 SwiftUI 中将视图添加到层​​次结构时如何动画过渡

SwiftUI:动画过渡

SwiftUI - 使用淡入淡出动画更改文本

NavigationView中的SwiftUI自定义动画过渡?

值更改时如何在 UIKit 中更新 SwiftUI 视图?

动画不适用于 SwiftUI 视图状态更改