You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 14:43:14