如何在 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
,并创建并返回一个全新的视图。在这种情况下,Overlay
、EmptyView
等也可以派上用场。
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 == 0
和textValue % 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 中将视图添加到层次结构时如何动画过渡