SwiftUI文本逐行滑动动画的MVVM实现合理性咨询
问题描述
我的代码能正常运行,但不确定是否符合MVVM规范。我想要实现文本逐行从左侧滑入的动画效果,每秒显示一行。
ViewModel代码
@MainActor class MyViewModel: ObservableObject { @Published var lines: [String] = [] init() { let myLines = [ "This", "is", "just", "a", "test" ] for (index, line) in myLines.enumerated() { DispatchQueue.main.asyncAfter(deadline: .now() + Double(index)) { withAnimation { self.lines.append(line) } } } } }
ContentView代码
import SwiftUI struct ContentView: View { @StateObject var vm = MyViewModel() var body: some View { VStack { ForEach(vm.lines, id: \.self) { line in Text(line) .transition(.move(edge: .leading)) } } .padding() } } #Preview { ContentView() }
我的困惑
我不认可在ViewModel中延迟填充lines数组的做法,因为这个数组可能还有其他无需逐行显示的使用场景。但如果把动画逻辑移到ContentView中,又需要在.onAppear里写不优雅的延迟填充@State变量的代码,没法直接复用vm.lines进行ForEach循环,希望得到相关建议。
优化方案
核心思路是让ViewModel专注于数据管理,把「逐行显示」这类UI展示逻辑完全放在View层,既复用ViewModel的原始数据,又保证两者职责分离。
1. 重构ViewModel:只提供完整数据源
让ViewModel只负责维护完整的业务数据,去掉和动画相关的延迟逻辑,这样数据可以在任意场景直接复用:
@MainActor class MyViewModel: ObservableObject { // 直接提供完整的原始数据,可用于任意展示场景 @Published var allLines: [String] = ["This", "is", "just", "a", "test"] // 这里可以添加其他业务逻辑,比如从网络加载数据、处理数据转换等 }
2. 在ContentView中控制动画逻辑
在View层维护一个用于展示的数组,通过定时任务从ViewModel的完整数据中逐行取数据,实现动画效果:
import SwiftUI struct ContentView: View { @StateObject var vm = MyViewModel() // 仅用于当前View的展示数组,和ViewModel的原始数据解耦 @State private var displayedLines: [String] = [] var body: some View { VStack { ForEach(displayedLines, id: \.self) { line in Text(line) .transition(.move(edge: .leading)) } } .padding() .onAppear { startLineAnimation() } } private func startLineAnimation() { for (index, line) in vm.allLines.enumerated() { DispatchQueue.main.asyncAfter(deadline: .now() + Double(index)) { withAnimation { displayedLines.append(line) } } } } } #Preview { ContentView() }
3. 进阶:封装动画逻辑实现复用
如果这个逐行动画需要在多个View中使用,可以把逻辑封装成ViewModifier,进一步简化代码:
// 封装动画逻辑的Modifier struct LineByLineAnimationModifier: ViewModifier { let fullLines: [String] @State private var displayedLines: [String] = [] func body(content: Content) -> some View { content .onAppear { for (index, line) in fullLines.enumerated() { DispatchQueue.main.asyncAfter(deadline: .now() + Double(index)) { withAnimation { displayedLines.append(line) } } } } .overlay( VStack { ForEach(displayedLines, id: \.self) { line in Text(line) .transition(.move(edge: .leading)) } } ) } } // 给View添加便捷调用方法 extension View { func lineByLineAnimation(with lines: [String]) -> some View { self.modifier(LineByLineAnimationModifier(fullLines: lines)) } } // 简化后的ContentView struct ContentView: View { @StateObject var vm = MyViewModel() var body: some View { VStack {} .padding() .lineByLineAnimation(with: vm.allLines) } }
方案符合MVVM的原因
- ViewModel只负责数据的获取、存储和转换,不涉及任何UI展示逻辑,保证了数据的通用性;
- View层负责UI渲染和交互逻辑,逐行动画属于UI展示范畴,放在View层更合理,同时通过独立的展示数组和ViewModel数据解耦,避免互相干扰。
内容的提问来源于stack exchange,提问作者irrbloss
相关产品推荐
相关产品推荐

