SwiftUI开发中我扔掉MVVM了

引言

前提说明:本文是为探究MVVM在现有SwiftUI开发模式下的合理性,非工程实践类文章 观点说明:本文基于本人在SwiftUI领域为数不多的经验以及其他服务端和前端的全栈经验,得出的主观性判定,也有可能有说的不对的地方,欢迎大家一起讨论

背景

我是一个新入门不久的SwiftUI开发者,因此在学习完成SwiftUI基础语法后,本着实践就是最好的学习老师,投入了一款macOS软件的开发。

在这个过程中,参考并学习了《iOS 17 程序设计实战心法 Swift + Swift UI》这本书,而当我在查阅资料的时候,了解到备受推崇的MVVM软件架构设计模式,甚至很多文章冠名以 "苹果公司开始推广MVVM模式"。但是,当我在实践中应用后,灾难就发生了。

我反思,是我的打开方式不对,还是我跟MVVM八字不合?

问题点

要了解这个过程,就得先来看两个实例;第一个实例是直接使用SwiftData及@Query语义,第二个实例是采用了MVVM后的SwfitUI工程代码,两者都是展示一个列表并添加一个项的功能:

less 复制代码
@Model
class Person {
  var name: String
} 

struct PersonListView: View {
  @Environment(.modelContext) private var modelContext
  @Query
  private var persons: [Person] = []

  var body: some View {
    List(persons) { person in
      Text(person.name)
    }
    .toolbar {
      ToolbarItem {
        Button(action: {
          var person = Person()
          person.name = "test"
          modelContext.insert(person)
        }) {
          Text("Add")
        }
      }
    }
  }
}
swift 复制代码
@Model
class Person {
  var name: String
} 

// ①
@Observable
class PersonViewModel {
  private var modelContext: ModelContext
  @Published var persons: [Person] = []

  init(modelContext: ModelContext) {
    self.modelContext = modelContext
    
    // 获取默认的数据
    let descriptor = FetchDescriptor<Person>()
    let data = try? modelContext.fetch(descriptor)
    self.encoder = data ?? []
  }

  func addPerson(_ name: String) {
    modelContext.insert(name)
  }

}

struct PersonListView: View {
  // ②
  @StateObject var personViewModel: PersonViewModel

  var body: some View {
    // ③
    List(personViewModel.persons) { person in
      Text(person.name)
    }
    .toolbar {
      ToolbarItem {
        Button(action: {
          personViewModel.addPerson("test")
        }) {
          Text("Add")
        }
      }
    }
  }
}

这两者的区别主要是将数据的操作逻辑与UI层分离(可看后者 ①②③ 三处),那么真实的开发中,可能会造成什么样的实际影响呢?

简单事情复杂化

可以在上述的两者对比中,明显能够看出来,为了抽离表示层逻辑,需要额外增加一个ViewModel类来完成这个职责。

预览测试复杂化

如果需要提取ViewModel,那么就需要在构建的时候诸如ModelContext;相对的,如果我需要预览该ViewModel所对应的View,那么就需要在预览代码中手动创建PersonViewModel,为整个预览测试增大了测试难度

php 复制代码
#Preview {
  let config = ModelConfiguration(isStoredInMemoryOnly: true)
  let container = try! ModelContainer(for: Person.self, configurations: config)
  let viewModel = PersonViewModel(modelContext: container.mainContext)
  
  return PersonListView(personViewModel: viewModel)
}

而仅仅使用MV模式下:

php 复制代码
#Preview {
  let config = ModelConfiguration(isStoredInMemoryOnly: true)
  let container = try! ModelContainer(for: Person.self, configurations: config)
  
  return PersonListView()
    .modelContainer(container)
}

可能一下子看不太出来,但是,当你这个页面涉及的ViewModel元素过多的时候,你的预览测试可能会变成这样:

php 复制代码
#Preview {
  let config = ModelConfiguration(isStoredInMemoryOnly: true)
  let container = try! ModelContainer(for: Person.self, configurations: config)
  let viewModel = PersonViewModel(modelContext: container.mainContext)

  let container2 = try! ModelContainer(for: Person2.self, configurations: config)
  let viewModel2 = PersonViewModel(modelContext: container2.mainContext)

  let container3 = try! ModelContainer(for: Person3.self, configurations: config)
  let viewModel3 = PersonViewModel(modelContext: container3.mainContext)

  let container4 = try! ModelContainer(for: Person4.self, configurations: config)
  let viewModel4 = PersonViewModel(modelContext: container3.mainContext)

  // other viewModels

  return PersonListView(personViewModel: viewModel, model2: viewModel2, model3: viewModel3, model4: viewModel4, ...)
}

显而易见,对于复杂页面来说,这简直就是一个灾难。

无限的构造传递

这个在第2点测试复杂性上也有一点涉及说明,但不仅仅限于ViewModel的传递;比如EnvironmentObject对象的传递,就需要增加一个构造函数,因为其无法直接直接在ViewModel内使用。

虽然,你可以使用构造器模式来避免构造函数一次性传递过多的参数,但是本质上,你仍旧需要一个个将需要的对象传入到ViewModel内。

共享状态问题

ViewModel双向绑定的是具体的某个页面,如果涉及到数据的多方使用和更新,那么将会是个非常复杂的兼容过程

MVVM的必要性

上述这三个点是我一些实践过程中的体验,然后我不禁思考,我现在使用的MVVM模式是否真的适合于SwiftUI开发?

那么探究这个问题,首先需要了解,什么是MVVM,它的前世今生、解决的主要矛盾问题是什么。

什么是MVVM

MVVM(Model - View -> ViewModel)是一种软件架构设计模式。 其主要分为三层: Model:Model代表的是你的数据 View:视图,直接和用户打交道的 ViewModel:ViewModel 是 View 和 Model 之间的桥梁。View 和 ViewModel 之间会进行一个数据的绑定。当用户在 View 中进行操作的时候,ViewModel 会去更新 Model 的状态。同理,当 Model 数据发生改变的时候,ViewModel 也会通知 View,让 View 自动显示最新的内容

MVVM由微软架构师Ken Cooper和Ted Peters开发,通过利用WPF(微软.NET图形系统)和Silverlight(WPF的互联网应用派生品)的特性来简化用户界面的事件驱动程序设计。

其主要的好处是:

  • 降低了View和Model之间的耦合度,使得它们可以独立地开发和测试。
  • 提高了代码的可重用性和可维护性,因为ViewModel可以在不同的View之间共享。
  • 简化了单元测试,因为ViewModel不依赖于具体的UI控件。
  • 支持双向数据绑定,使得View可以自动更新Model的变化,反之亦然。

UIKit时代下的架构模式演进历史

乍一看,MVVM的好处甚多,那么为什么在实际的SwiftUI的开发过程中,我或者说其他很多不管是新手还是沉浸在此多年的老手会这么痛苦呢?

先来看下UIKit的时代,那时候苹果推崇以MVC的模式来构建一款APP,但是随着大规模的业务数据逻辑和视图控制逻辑,导致其任务庞杂;后MVVM时代,为了解决这个问题,将数据逻辑从控制器内抽离,形成了ViewModel层。

可以发现,这种演进都只是为了弱化视图控制器的存在,开发者本身只需要关注业务就可。因此,在WWDC19上,SwiftUI的发布有了较为大的跨越。

SwiftUI架构模式

苹果在WWDC19会议上对于SwiftUI的开篇一句话,阐述了苹果对于SwfitUI的期望:The shortest path to a great app.(用以构建一个伟大APP的最短路径)。因此,SwiftUI主要就是为简化开发者的开发难度,能够快速构建一个产品。

苹果在WWDC19上推出的SwiftUI的一大特性就是取消了视图控制器。苹果自身的SwiftUI完成了视图控制器大部分的功能(随着SwiftUI的逐步演进,未来必能够完全取代上古时代ViewController的所有职能),那么按照上述UIKit的演进历史,不难发现,其也将取代ViewModel的职能。

以下苹果官方在WWDC会议上公布的一张SwiftUI的数据流向图。

在此借用苹果官方的一句话,更能佐证上述的一个推论:

SwiftUl offers a declarative approach to user interface design. As you compose a hierarchy of views, you also indicate data dependencies for the views. When the data changes, either due to an external event or because of an action taken by the user, SwiftUl automatically updates the affected parts of the interface. As a result, the framework automatically performs most of the work traditionally done by view controllers.(作为结果,框架会自动完成大部分在传统意义上被ViewControllers完成的工作,即:No ViewControllers And No ViewModels)

最佳实践

那么,如果没有了MVVM,我们在编写iOS APP的时候,什么才是最佳的一种方式?

我认为的答案是:领域模型(本地/远端)+View。

领域模型

很多人对于领域可能没有什么概念,但是对于领域驱动设计应该大有耳闻。在软件层面,领域是要解决的现实问题空间(此篇文章不赘述该概念,读者可自己百科领域相关的知识),而领域模型则是在领域下,特定解决空间内,对于该问题的解。

说的有点抽象,简而言之,以用户注册为例,用户注册这个领域下,如何实现用户的注册校验、用户信息如何转化为标准结构化数据,就是领域模型做的事情。

领域模型(本地/远端)+View

在APP顶层将领域模型传入作为Environment,在每个View页面上,直接将EnvironmentObject注入使用。

以我现在开发的一个App举例(一个开发者工具),我需要在对应的功能上执行完成对应的能力后,需要记录下该命令,并且在该界面上查看对应的命令执行记录。

AB页面上层:

scss 复制代码
struct TopView: View {
  @Query
  private var commandHistories: [CommandHistory] = []
  
  var body: some View {
    VStack {
      AViwe()
      BView()
    }
    .environmentObject(commandHistories)
  }
}

A/B页面:

less 复制代码
struct AORBView: View {
  @Environment(.modelContext) private var modelContext
  @EnvironmentObject var commandHistories: [CommandHistory]
  
  var body: some View: {
    VStack {
      List(commandHistories) { item in
        Text(item.command)
      }

      Button {
        commandHistory = CommandHistory("test command")
        modelContext.insert(commandHistory)
      } label: {
        Text("添加命令")
      }
    }
  }
}

小结

  1. 不要过度设计你的代码
  1. 不要被网上教程所引入而仅使用非理解
  1. 不要被某个人的观念所束缚,包括本篇文章
  1. 不要对抗系统(SwiftUI已经帮我们做的很好了)

有意思的小事情

  1. 关于 "苹果公司开始推广MVVM" 这件事情

我看到好多文章,都有关于这句话的描述,然后我找了一通,在苹果官网并没有发现有相关的资料佐证。最终,在一篇文章看到了国外有个开发者的原文

I noticed an interesting thing when Apple moved the ObservableObject protocol to the Combine framework. It looks like Apple began promoting the MVVM pattern.

很搞笑的是文章作者说的是:这看起来像是苹果公司推广MVVM模式(It looks like Apple began promoting the MVVM pattern) ,但是很多文章直接下定义:苹果公司开始推广MVVM模式。所以,很多事情还是得仔细探究以及需要有自己的理解。

参考链接

MVVM - 维基百科,自由的百科全书

什么是 MVVM ?

WPF MVVM模式简介

Building ViewModels with Combine framework

如何实现SwiftUI微服务? - 腾讯云开发者社区-腾讯云

相关推荐
FreeCultureBoy1 天前
Swift 与 SwiftUI 学习系列:变量与常量篇 🚀
swiftui·xcode·swift
FreeCultureBoy1 天前
Swift 与 SwiftUI 学习系列: print 函数详解 🚀
swiftui·xcode·swift
文件夹__iOS4 天前
[SwiftUI 开发] 嵌套的ObservedObject中的更改不会更新UI
swiftui
lph65827 天前
ios入门实例(五):随机选中姓名
ios·swiftui·swift
酒茶白开水8 天前
SwiftUI八与UIKIT交互
ios·swiftui·交互·轮播图·page·uikit·扫动
Daniel_Coder10 天前
Swift Combine — Notification、URLSession、Timer等Publisher的理解与使用
ios·swiftui·swift·notification·publisher·combine·urlsession
安卓小小白21 天前
WPF Prism框架搭建
wpf·mvvm·prism
fatfishccc21 天前
SpringMVC
mvc·springmvc·restful·mvvm·过滤器·controller·拦截器
荔枝lizhi22 天前
swiftUI 属性包装器
swiftui
酒茶白开水24 天前
SwiftUI六组合复杂用户界面
ui·swiftui·组合·复杂界面