美文网首页
SwiftUI - 偶遇 MVI

SwiftUI - 偶遇 MVI

作者: 猴子的饼干 | 来源:发表于2024-01-14 20:55 被阅读0次

上个月某天和往常一样、突然就被拉入了一个群, 新项目, 用 SwiftUI !
终于跟上潮流了啊, 但此时对 SwiftUI 的印象几乎剩下它的名字...
“立刻开发”、 “马上就要”
那时候, 我的内心只剩下:


阅读难度: [简单]
要求: 了解 SwiftUI、常用设计模式
本文是一次 SwiftUI 项目中对于 MVVM 架构适配实际业务场景的记录, 不构成任何开发建议


闲话少说、直接开干

又双叒叕快速刷了一遍苹果官方的 SwiftUI 和部分 WWDC 教程, 这次印象最深的是两个属性包装器@Observable@Environment, 这也开启了与 MVI 的偶遇之旅.
@Observable: iOS17 支持 , 为自定义类型添加观察, 使其支持 Observable 协议, 省去了老版本中的样板代码.
@Environment: 环境值, 自定义环境值需与 .environment() 配套使用. 使得我们在视图树中可以共享/访问特定的环境值.

原本 APP 上业务多而复杂、更新迭代非常快, APP 中从逻辑上独立了事件流, 这在一定程上减小多人合作中可能会出现的并行开发逻辑混乱的问题. 但依旧存在出现状态混乱的可能性. 在只求迭代速度的前提下, APP 出于“稳定性”、“成本”的考虑, 几乎不能从架构上来大刀阔斧的重构.

这次是全新的项目, 这俩属性包装器让我萌生了一个在 APP 侧想做却苦于没有机会的想法. 为了追求速度, 最初始的架构设计上依旧以 MVVM 为基础. 有了 @Observable 的助力, 使得 ViewModel 与 View 的双向绑定更加简化, 再通过 @State@Bindable@Observable 的关联、 SwiftUI 局部刷新也更加高效.

每个事件的处理都可能非常复杂, 涉及到的事件、交互成百上千. 一开始习惯性的通过拆分业务来避免 viewModel 过于臃肿, 可随着业务越来越多越来越复杂, 各类“子ViewModel” 越来越多, 关联逻辑越来越复杂冗余, 而这样的模块基本不会是1个人开发, 不可避免的, 又将滑向了屎山
(Oh ! holy...shit).

有了APP的前车之鉴, 这次一开始就隔离出了事件流, 为后续的改进省去了不少工作量. 我们用简单的订单列表页面为例子:

// VM
@Observable
class OrderDetailVM {
    var statusSectionModel:OrderDetailStatusSectionModel?
    var infoCardSectionModel:OrderDetailInfoCardSectionModel?
    var feeDetailSectionModel:OrderDetailFeeDetailSectionModel?
    var schedulingSectionModel:OrderDetailSchedulingSectionModel?
    // ...
}

extension OrderDetailVM {
    enum OrderDetailEvent {
        case statusButton(Int?)
        case refreshAllData
        //...
    }
    
    func runEvent(event: OrderListEvent) {
        switch event {
        case .statusButton(let actionID):
            solveStatusButton(buttonActionID: actionID)
            break
        case .refreshAllData:
            fetchAll()
            break
        //.....
        }
    }
}

// View
struct OrderDetailView: View {
    @State var viewModel = OrderDetailVM()
    
    var body: some View {
        VStack{
            OrderDetailStatusSectionView(viewModel: viewModel.statusSectionModel)
            //....
        }
        .environment(viewModel)
    }
}

struct OrderDetailStatusSectionView: View {
    @Binding var sectionModel: OrderDetailStatusSectionModel?
    @Environment(OrderDetailVM.self) private var viewModel
    var body: some View {
        //...
        HStack {
            if let buttons = sectionModel.actions {
                ForEach(buttons, id: \.actionId) { button in
                    Button {
                        viewModel?.runEvent.statusButton(button.actionId)
                    } label: {
                        Text("测试按钮")
                    }
                }
            }
        }
        // ...
    }
}

上面的代码我们可以发现:

  1. 结构上, View 层能够直接与 model 层通信, 不太严谨
  2. 事件流还是归属于 ViewModel, 并未将其从架构上独立出来

根源都在于“事件流仅仅在逻辑上进行了区分”,
视图层只需要通过事件流告知 ViewModel 变化, 针对这一点可以将事件流协议化, 使得 View 层强制遵循事件流的协议进行通信. 并将所有与 ViewModel 的通信都经过事件流处理, 从架构上限制业务.
优化优化:

// 事件流基础协议
protocol EventBusProtocol: AnyObject {
    associatedtype ViewModelType
    associatedtype EventBusType
    
    var eventBusVM: ViewModelType { get }
    func runEvent(_ event: EventBusType)
}

// 订单详情事件协议及实现
protocol OrderDetailVMEventProtocol: EventBusProtocol where ViewModelType == OrderDetailVM, EventBusType == OrderDetailEvent {
}

// 关联实现事件流对应的VM
extension OrderDetailVM: OrderDetailVMEventProtocol {
    var eventBusVM: OrderDetailVM {
        self
    }
}

// 在协议扩展中实现具体的处理
extension OrderDetailVMEventProtocol  {
    
    func runEvent(_ event: EventBusType) {
        switch event {
        case .statusButton(let actionID):
            solveStatusButton(buttonActionID: actionID)
            break
        case .refreshAllData:
            eventBusVM.fetchAll()
            break
        //...
        }
    }
}

这样改动后:

  1. 对事件流进行了协议式分离, View 层仅通过环境值来调用事件协议声明的方法. 杜绝了View直接与Model的通信.
  2. 完全由数据流驱动, 使应用状态管理更加明确可预测
  3. 面向协议的方式也使得事件流方式更加通用化,易理解, 也拥有较好的扩展性, 无论是严格按照事件流来处理交互响应、或是只有某一部分采用事件流的方式都能很好的适配.

相应的, View层上更新下对应环境值的获取与调用即可, 同样以上面的订单状态视图为例:

struct OrderDetailView: View {
    @State var viewModel = OrderDetailVM()
    
    var body: some View {
        VStack{
            OrderDetailStatusSectionView(viewModel: viewModel.statusSectionModel)
            //....
        }
        .environment(\.orderDetailEventBus, viewModel)
    }
}
struct OrderDetailStatusSectionView: View {
    @Binding var sectionModel: OrderDetailStatusSectionModel?
    @Environment(\.orderDetailEventBus) private var viewModel
    var body: some View {
        //...
        HStack {
            if let buttons = sectionModel.actions {
                ForEach(buttons, id: \.actionId) { button in
                    Button {
                        viewModel?.runEvent.statusButton(button.actionId)
                    } label: {
                        Text("测试按钮")
                    }
                }
            }
        }
        // ...
    }
}

这怎么有点像 MVI ? 我们的“事件流”(EventBus)实际上与MVI中的“意图”(Intent)一样, 都有着“单向数据流”的特点.

上述的改进细节上还需优化, 并且保留着 ViewModel . 虽然有了“MVI”中“单一状态流”的概念, 也只是协议化了事件流, 依旧在ViewModel 层上.

[ MVVM ]: ViewModel负责处理与UI无关的业务逻辑, 并提供数据供视图View显示. 使用数据绑定来保持ViewModel 与View之间的同步

MVVM.png

[ MVI ]: 侧重于通过Intent来驱动应用程序的状态变化. 通常包含一个单一的状态来作为数据流的核心, 通过处理Intent来更新状态, 并通过订阅来更新View

MVI.png

反过来想, 把我们现在的ViewModel层看作是Model层、而Intent层, 是ViewModel被抽象成了协议的事件流部分.从模块通信上看, Intent包含着 ViewModel 层, 那就假装它是Intent层吧.
或者, 把 MVI 当作是 ViewModel 带了个Intent 帽子的 MVVM. 应该也能凑合说的过去吧.

结语

一个 View 可以搞定静态页面, 经久耐用的MVVM , 分工协作更加细化独立的VIPER......
有的架构模糊了部分模块之间的界限, 有的架构需要大量成本来维持其自身的通信规范. 这次与 MVI 的偶遇也再次加深了自己的看法:
架构是重构过程思想的重要体现, 而重构是需要一直进行的.
“没有最好的架构、只有目前最合适的”

相关文章

网友评论

      本文标题:SwiftUI - 偶遇 MVI

      本文链接:https://www.haomeiwen.com/subject/huhcodtx.html