SwiftUI+MVI:构建可维护的响应式应用

144 阅读10分钟

本文基于实际项目经验,详细介绍如何在iOS应用中使用MVI(Model-View-Intent)架构模式,通过单向数据流和明确的职责分离,构建可测试、可维护的现代化应用。

什么是MVI架构

MVI(Model-View-Intent)是一种响应式架构模式,最初在Android开发中流行,但同样适用于iOS开发。MVI通过单向数据流来管理应用状态,确保数据流向的可预测性和可追踪性。

MVI的核心思想

  • 单向数据流:数据只能沿着一个方向流动,从View → Intent → ViewModel → State → View
  • 不可变状态:State是不可变的,所有状态更新都通过创建新状态实现
  • 纯函数Reducer:状态转换逻辑是纯函数,易于测试和调试
  • 副作用隔离:异步操作和副作用通过Middleware统一处理

为什么选择MVI

在iOS开发中,我们通常使用MVVM或MVC架构。MVI相比这些传统架构有以下优势:

✅ 优势

  1. 可预测性:单向数据流使状态变化更容易追踪和调试
  2. 可测试性:纯函数Reducer和Middleware易于单元测试
  3. 可维护性:明确的职责分离,代码结构清晰
  4. 状态管理:集中式状态管理,避免状态分散
  5. 时间旅行调试:可以轻松实现撤销/重做功能

⚠️ 适用场景

  • 复杂的状态管理需求
  • 需要频繁状态同步的应用
  • 需要撤销/重做功能的应用
  • 团队协作开发,需要清晰的代码结构

核心组件详解

1. State(状态)

State是应用的唯一数据源,所有UI都基于State渲染。

public struct CState: Equatable {
    // 基础状态
    public var count: Int
    public var toastMessage: String?
    
    // 加载状态
    public var isLoading: Bool
    public var loadingMessage: String?
    
    // 错误状态
    public var errorMessage: String?
    
    // 列表数据
    public var items: [ListItem]
    
    // 搜索相关
    public var searchText: String
    public var searchResults: [ListItem]
    
    // 派生状态(computed properties)
    public var displayItems: [ListItem] {
        if !searchText.isEmpty {
            return searchResults
        }
        return items
    }
}

设计原则

  • ✅ 使用不可变结构体
  • ✅ 使用派生状态减少冗余
  • ✅ 避免在State中存储UI特定的数据
  • ⚠️ 注意历史记录的内存占用(已限制为50条)

2. Intent(意图)

Intent表示用户想要执行的操作,是用户交互的抽象。

public enum CIntent {
    // 基础操作
    case incrementCount
    case showTime
    
    // 数据加载
    case loadItems
    case loadMoreItems
    
    // 搜索功能
    case searchTextChanged(String)
    case performSearch(String)
    
    // 表单验证
    case inputTextChanged(String)
    case submitForm
    
    // 错误处理
    case clearError
    case retryLastOperation
    
    // 撤销/重做
    case undo
    case redo
    case saveToHistory
}

设计原则

  • ✅ 每个用户操作对应一个Intent
  • ✅ Intent应该是描述性的,而不是命令式的
  • ✅ 区分同步Intent和异步Intent

3. Effect(副作用)

Effect表示副作用的结果或需要处理的事件,通常由Middleware产生。

public enum CEffect {
    // 网络操作
    case networkDataFetched
    
    // 数据加载
    case itemsLoaded([CState.ListItem])
    case moreItemsLoaded([CState.ListItem])
    
    // 错误处理
    case networkError(String)
    case validationError(String)
    
    // 加载状态
    case loadingStarted(String?)
    case loadingFinished
    
    // Intent触发(Effect到Intent的转换)
    case triggerIntent(CIntent)
    
    // 链式Effect
    case chainEffect1
    case chainEffect2
    case chainEffect3
}

设计原则

  • ✅ Effect表示副作用的结果
  • ✅ Effect可以触发新的Intent(通过triggerIntent)
  • ✅ Effect可以触发链式操作

4. Reducer(状态转换器)

Reducer是纯函数,根据Intent或Effect更新State。

public struct CReducer {
    /// 根据Intent更新State
    public static func reduce(state: CState, intent: CIntent) -> CState {
        var newState = state
        
        switch intent {
        case .incrementCount:
            newState.count += 1
            
        case .showTime:
            let formatter = DateFormatter()
            formatter.locale = Locale(identifier: "zh_CN")
            formatter.dateFormat = "H点m分s秒"
            newState.toastMessage = formatter.string(from: Date())
            
        case .searchTextChanged(let text):
            newState.searchText = text
            newState.searchResults = []
            
        default:
            return newState
        }
        
        return newState
    }
    
    /// 根据Effect更新State
    public static func reduce(state: CState, effect: CEffect) -> CState {
        var newState = state
        
        switch effect {
        case .itemsLoaded(let items):
            newState.items = items
            newState.isLoading = false
            
        case .networkError(let message):
            newState.errorMessage = message
            newState.isLoading = false
            
        default:
            break
        }
        
        return newState
    }
}

设计原则

  • ✅ 保持Reducer为纯函数
  • ✅ 不执行副作用
  • ✅ 总是返回新状态,不修改原状态

5. Middleware(中间件)

Middleware处理异步操作和副作用,返回Publisher来处理异步操作。

public struct CMiddleware {
    /// 处理Intent,返回对应的Effect
    public static func middleware(intent: CIntent, state: CState) -> AnyPublisher<CEffect?, Never> {
        switch intent {
        case .loadItems:
            // 先发送加载开始状态,然后异步加载数据
            return Just(CEffect.loadingStarted("正在加载数据..."))
                .append(
                    Future<CEffect?, Never> { promise in
                        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
                            let items = (1...10).map { index in
                                CState.ListItem(
                                    id: "item_\(index)",
                                    title: "项目 \(index)",
                                    subtitle: "这是第 \(index) 个项目"
                                )
                            }
                            promise(.success(.itemsLoaded(items)))
                        }
                    }
                )
                .eraseToAnyPublisher()
                
        case .submitForm:
            // 表单验证
            if state.inputText.isEmpty {
                return Just(CEffect.validationError("输入不能为空")).eraseToAnyPublisher()
            }
            // 验证通过后的处理...
            
        default:
            return Just(nil).eraseToAnyPublisher()
        }
    }
}

设计原则

  • ✅ 处理所有异步操作
  • ✅ 可以访问当前State进行条件判断
  • ✅ 返回Publisher来处理异步操作

6. ViewModel(视图模型)

ViewModel连接View和业务逻辑,管理状态流。

@MainActor
public class CViewModel: ObservableObject {
    @Published public private(set) var state: CState
    
    private let intentSubject = PassthroughSubject<CIntent, Never>()
    private var cancellables = Set<AnyCancellable>()
    
    public init(initialState: CState = CState()) {
        self.state = initialState
        
        // 订阅Intent流,处理用户意图
        intentSubject
            .sink { [weak self] intent in
                guard let self = self else { return }
                self.handleIntent(intent)
            }
            .store(in: &cancellables)
    }
    
    /// 处理Intent,区分同步和异步操作
    private func handleIntent(_ intent: CIntent) {
        // 判断是否为同步Intent
        let isSynchronous: Bool = {
            switch intent {
            case .incrementCount, .showTime, .inputTextChanged:
                return true
            default:
                return false
            }
        }()
        
        if isSynchronous {
            // 同步操作直接通过Reducer更新State
            self.state = CReducer.reduce(state: self.state, intent: intent)
        } else {
            // 异步操作通过Middleware处理
            CMiddleware.middleware(intent: intent, state: self.state)
                .sink { [weak self] effect in
                    guard let self = self, let effect = effect else { return }
                    self.handleEffect(effect)
                }
                .store(in: &cancellables)
        }
    }
    
    /// 处理Effect,更新状态并处理可能的后续操作
    private func handleEffect(_ effect: CEffect) {
        self.state = CReducer.reduce(state: self.state, effect: effect)
        
        // 处理Effect可能触发的后续操作
        switch effect {
        case .triggerIntent(let intent):
            self.dispatch(intent)
        default:
            break
        }
    }
    
    public func dispatch(_ intent: CIntent) {
        intentSubject.send(intent)
    }
}

7. View(视图)

View是UI层,观察State并发送Intent。

public struct CView: View {
    @StateObject private var viewModel = CViewModel()
    
    public var body: some View {
        VStack {
            Text("计数: \(viewModel.state.count)")
                .font(.system(size: 48, weight: .bold))
            
            Button("点击加1") {
                viewModel.dispatch(.incrementCount)
            }
        }
    }
}

数据流设计

MVI架构的核心是单向数据流,数据流向如下:

View → Intent → ViewModel → Middleware → Effect → Reducer → State → View

完整数据流示例

  1. 用户操作触发Intent

    Button("加载数据") {
        viewModel.dispatch(.loadItems)
    }
    
  2. ViewModel接收Intent

    intentSubject.send(.loadItems)
    
  3. 判断同步/异步

    • 同步Intent:直接通过Reducer更新State
    • 异步Intent:通过Middleware处理
  4. Middleware处理异步操作

    case .loadItems:
        return Just(CEffect.loadingStarted("正在加载数据..."))
            .append(Future { ... })
    
  5. Effect通过Reducer更新State

    case .itemsLoaded(let items):
        newState.items = items
        newState.isLoading = false
    
  6. State变化触发UI更新

    @Published var state: CState  // 自动触发UI更新
    

实战场景

场景1:同步操作(简单状态更新)

最简单的场景,用户点击按钮,直接更新计数。

// Intent
case incrementCount

// Reducer
case .incrementCount:
    newState.count += 1

// View
Button("加1") {
    viewModel.dispatch(.incrementCount)
}

场景2:异步操作(网络请求)

处理网络请求,包括加载状态和错误处理。

// Intent
case loadItems

// Middleware
case .loadItems:
    return Just(CEffect.loadingStarted("正在加载数据..."))
        .append(
            Future<CEffect?, Never> { promise in
                // 模拟网络请求
                DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                    promise(.success(.itemsLoaded(items)))
                }
            }
        )

// Effect
case itemsLoaded([CState.ListItem])

// Reducer
case .itemsLoaded(let items):
    newState.items = items
    newState.isLoading = false

场景3:搜索功能(防抖处理)

实现搜索防抖,避免频繁触发搜索请求。

// Intent
case searchTextChanged(String)
case performSearch(String)

// ViewModel中的防抖处理
if case .searchTextChanged(let text) = intent {
    self.searchDebounceWorkItem?.cancel()
    let workItem = DispatchWorkItem { [weak self] in
        guard let self = self, !text.isEmpty else { return }
        self.dispatch(.performSearch(text))
    }
    self.searchDebounceWorkItem = workItem
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: workItem)
}

// View
TextField("搜索...", text: Binding(
    get: { viewModel.state.searchText },
    set: { viewModel.dispatch(.searchTextChanged($0)) }
))

场景4:表单验证

实现表单输入验证,包括实时验证和提交验证。

// Intent
case inputTextChanged(String)
case submitForm

// Middleware(包含验证逻辑)
case .submitForm:
    if state.inputText.isEmpty {
        return Just(CEffect.validationError("输入不能为空")).eraseToAnyPublisher()
    }
    if state.inputText.count < 3 {
        return Just(CEffect.validationError("输入至少需要3个字符")).eraseToAnyPublisher()
    }
    // 验证通过后的处理...

// Effect
case validationError(String)

// Reducer
case .validationError(let message):
    newState.inputError = message

场景5:撤销/重做功能

实现状态历史记录,支持撤销和重做操作。

// State中的历史记录
public var history: [CState]
public var historyIndex: Int

// Intent
case undo
case redo
case saveToHistory

// Reducer
case .undo:
    if newState.canUndo {
        newState.historyIndex -= 1
        if newState.historyIndex >= 0 {
            return newState.history[newState.historyIndex]
        }
    }

case .saveToHistory:
    var updatedHistory = Array(newState.history.prefix(newState.historyIndex + 1))
    updatedHistory.append(newState)
    
    // 限制历史记录数量,避免内存问题
    if updatedHistory.count > 50 {
        updatedHistory.removeFirst()
    }
    newState.history = updatedHistory

场景6:条件性操作

根据当前状态决定是否执行操作。

// Intent
case conditionalIncrement

// Middleware
case .conditionalIncrement:
    // 条件性操作:只有当count小于10时才执行
    if state.count < 10 {
        return Future<CEffect?, Never> { promise in
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                promise(.success(.conditionalEffectExecuted))
            }
        }
        .eraseToAnyPublisher()
    } else {
        return Just(CEffect.operationFailed("计数已达到上限")).eraseToAnyPublisher()
    }

场景7:链式操作(Effect链)

实现复杂的链式操作,一个Effect可以触发下一个Effect。

// Intent触发链式操作
case startChainOperation

// Middleware产生第一个Effect
case .startChainOperation:
    return Future { promise in
        promise(.success(.chainEffect1))
    }

// Middleware处理Effect,产生下一个Effect
case .chainEffect1:
    return Future { promise in
        promise(.success(.triggerIntent(.chainStep1)))
    }

// ViewModel中处理链式Effect
case .chainEffect1, .chainEffect2, .chainEffect3:
    CMiddleware.handleEffect(effect, state: self.state)
        .sink { [weak self] nextEffect in
            guard let self = self, let nextEffect = nextEffect else { return }
            self.handleEffect(nextEffect)
        }
        .store(in: &cancellables)

最佳实践

1. State设计

  • 使用不可变结构体:确保状态不可变,所有更新都创建新状态
  • 使用派生状态:通过computed properties减少冗余数据
  • 避免UI特定数据:State应该只包含业务数据,不包含UI状态
  • ⚠️ 注意内存占用:对于历史记录等可能无限增长的数据,需要限制数量

2. Intent设计

  • 描述性命名:Intent应该描述用户想要做什么,而不是如何做
  • 区分同步/异步:明确哪些Intent需要Middleware处理
  • 单一职责:每个Intent只做一件事

3. Effect设计

  • 表示副作用结果:Effect应该表示异步操作的结果
  • 支持链式操作:Effect可以触发新的Intent或Effect
  • 统一错误处理:通过Effect统一处理错误状态

4. Reducer设计

  • 保持纯函数:Reducer不执行副作用,只做状态转换
  • 总是返回新状态:不修改原状态,总是创建新状态
  • 易于测试:纯函数易于单元测试

5. Middleware设计

  • 处理所有异步操作:所有网络请求、数据库操作等都在Middleware中
  • 可以访问State:Middleware可以访问当前State进行条件判断
  • 返回Publisher:使用Combine的Publisher处理异步操作

6. 性能优化建议

  • 使用Combine的debounce操作符:替代DispatchWorkItem实现防抖
  • 状态比较优化:使用Equatable协议优化State比较
  • 分页加载:对于大型列表,使用分页加载
  • 依赖注入:使用依赖注入管理服务依赖,提高可测试性

7. 代码组织

  • 拆分Middleware:将不同的业务场景拆分到不同的Middleware文件
  • 使用协议:定义Middleware接口,提高可测试性
  • 添加日志:记录Intent和Effect的流转,便于调试

总结

MVI架构通过单向数据流明确的职责分离,使iOS应用更易理解、测试和维护。本文通过7个实际场景展示了MVI在iOS开发中的应用:

  1. ✅ 同步操作:简单直接的状态更新
  2. ✅ 异步操作:网络请求和加载状态管理
  3. ✅ 搜索防抖:优化用户体验
  4. ✅ 表单验证:实时和提交验证
  5. ✅ 撤销/重做:状态历史管理
  6. ✅ 条件性操作:基于状态的业务逻辑
  7. ✅ 链式操作:复杂的异步流程

核心优势

  • 可预测性:单向数据流使状态变化可追踪
  • 可测试性:纯函数易于单元测试
  • 可维护性:清晰的代码结构
  • 可扩展性:易于添加新功能

适用场景

MVI架构特别适合:

  • 复杂的状态管理需求
  • 需要频繁状态同步的应用
  • 需要撤销/重做功能的应用
  • 团队协作开发

通过遵循最佳实践和持续优化,可以构建出健壮、可维护的iOS应用。希望本文能帮助你在iOS开发中更好地应用MVI架构!

项目地址

项目中使用了组件化方案的其他组件,如有疑问,请看这篇文章

如果觉得文章对你有帮助,欢迎点赞、收藏、评论! 🎉