【问题标题】:How to animate/transition text value change in SwiftUI如何在 SwiftUI 中动画/过渡文本值更改
【发布时间】:2026-01-13 01:00:01
【问题描述】:

我正在尝试使用 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 的经验很少,还有其他方法可以实现吗?

提前致谢:)

【问题讨论】:

    标签: ios swift animation 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?"
                  }}}
      

      【讨论】: