本文我们要在visionOS内实现一个标题输出的动画效果。主要讲ViewModifier
协议,修饰符(modifier)应用于视图或另一个视图修饰符,生成原值的另一个版本。在希望创建一个可应用于不同视图的修饰符时可实现View
协议。
首先定义ViewModel
,本例中的模型比较简单,仅定了三个变量,分别表示当前文本、标题输出是否完成以及最终的标题文本。
1 2 3 4 5 6 7 8 |
import SwiftUI @Observable class ViewModel { var titleText: String = "" var isTitleFinished: Bool = false var finalTitle: String = "第三回 托内兄如海荐西宾 接外孙贾母惜孤女" } |
因模型中有默认值且需要在程序运行的过程中进行修改,所以在入口文件中需要将模型注入到环境中:
1 2 3 4 5 6 7 8 9 10 11 12 |
import SwiftUI @main struct visionOSDemoApp: App { @State private var model = ViewModel() var body: some Scene { WindowGroup() { ContentView() .environment(model) } } } |
接下来就是本文的重点了,我们需要自定义一个文本修饰符。虽然可以直接将修饰符应用于视图,但更常见和地道的做法是使用修饰符来定义一个View
来包装这个视图修饰符。我们在代码里就是这么做的,在视图中我们传入了5个变量,text
和isFinished
是需要进行修改的,所以使用了Binding
,cursor
定义了光标,默认使用了常见的|
,isAnimated
表示是否显示动画。
在TypeTextModifier
中可以看到,如果isAnimated
为false
,就直接显示最终文本。而在任务中有两个for
循环,分别设置初始的光标闪烁效果以及后续逐个文字和光标交替输出的效果,最后等待片刻,标记输出结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
import SwiftUI extension View { func typeText( text: Binding<String>, finalText: String, isFinished: Binding<Bool>, curor: String = "|", isAnimated: Bool = true ) -> some View { self.modifier( TypeTextModifier( text: text, finalText: finalText, isFinished: isFinished, cursor: curor, isAnimated: isAnimated ) ) } } private struct TypeTextModifier: ViewModifier { @Binding var text: String var finalText: String @Binding var isFinished: Bool var cursor: String var isAnimated: Bool func body(content: Content) -> some View { content .onAppear { if isAnimated == false { text = finalText isFinished = true } } .task { guard isAnimated else { return } // Blink the cursor a few times for _ in 1...2 { text = cursor try? await Task.sleep(for: .milliseconds(500)) text = "" try? await Task.sleep(for: .milliseconds(200)) } // Type out the title for index in finalText.indices { text = String(finalText.prefix(through: index)) + cursor let milliseconds = (1 + UInt64.random(in: 0...1)) * 100 try? await Task.sleep(for: .milliseconds(milliseconds)) } // Wrap up the title sequence try? await Task.sleep(for: .milliseconds(400)) text = finalText isFinished = true } } } |
ContentView
内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
struct ContentView: View { @Environment(ViewModel.self) private var model var body: some View { @Bindable var model = model NavigationStack { VStack { Spacer() VStack { Text(model.finalTitle) .monospaced() .font(.system(size: 50, weight: .bold)) .padding(.horizontal, 40) .hidden() .overlay(alignment: .leading) { Text(model.titleText) .monospaced() .font(.system(size: 50, weight: .bold)) .padding(.leading, 40) } Text("林黛玉进贾府") .font(.title) .padding(.top, 10) .opacity(model.isTitleFinished ? 1 : 0) } Spacer() } .typeText(text: $model.titleText, finalText: model.finalTitle, isFinished: $model.isTitleFinished, isAnimated: !model.isTitleFinished) } } } |
这里在屏幕中央输出两段文本,第一段会以修饰符的动画效果进行输出直至结束,第二段在第一段文本输出完成后显示。
示例代码:GitHub仓库
其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记