- 原文地址:MVVM-C with Swift
- 原文作者:Marco Santarossa
- 译文出自:掘金翻译计划
- 译者:Deepmissea
- 校对者:atuooo,1992chenlu
MVVM-C 与 Swift
简介
现今,iOS 开发者面临的最大挑战是构建一个健壮的应用程序,它必须易于维护、测试和扩展。
在这篇文章里,你会学到一种可靠的方法来达到目的。
首先,简要介绍下你即将学习的内容:
架构模式.
架构模式
它是什么
架构模式是给定上下文中软件体系结构中常见的,可重用的解决方案。架构与软件设计模式相似,但涉及的范围更广。架构解决了软件工程中的各种问题,如计算机硬件性能限制,高可用性和最小化业务风险。一些架构模式已经在软件框架内实现。
摘自 Wikipedia。
在你开始一个新项目或功能的时候,你需要花一些时间来思考架构模式的使用。通过一个透彻的分析,你可以避免耗费很多天的时间在重构一个混乱的代码库上。
主要的模式
在项目中,有几种可用的架构模式,并且你可以在项目中使用多个,因为每个模式都能更好地适应特定的场景。
当你阅读这几种模式时,主要会遇到:
Model-View-Controller
这是最常见的,也许在你的第一个 iOS 应用中已经使用过。不幸地是,这也是最糟糕的模式,因为 Controller
不得不管理每一个依赖(API、数据库等等),包括你应用的业务逻辑,而且与 UIKit
的耦合度很高,这意味着很难去测试。
你应该避免这种模式,用下面的某种来代替它。
Model-View-Presenter
这是第一个 MVC 模式的备选方案之一,一次对 Controller
和 View
之间解耦的很好的尝试。
在 MVP 中,你有一层叫做 Presenter
的新结构来处理业务逻辑。而 View
—— 你的 UIViewController
以及任何 UIKit
组件,都是一个笨的对象,他们只通过 Presenter
更新,并在 UI 事件被触发的时候,负责通知 Presenter
。由于 Presenter
没有任何 UIKit
的引用,所以非常容易测试。
Viper
这是 Bob 叔叔的清晰架构的代表。
这种模式的强大之处在于,它合理分配了不同层次之间的职责。通过这种方式,你的每个层次做的的事变得很少,易于测试,并且具备单一职责。这种模式的问题是,在大多数场合里,它过于复杂。你需要管理很多层,这会让你感到混乱,难于管理。
这种模式并不容易掌握,你可以在这里找到关于这种架构模式更详细的文章。
Model-View-ViewModel
最后但也是最重要的,MVVM 是一个类似于 MVP 的框架,因为层级结构几乎相同。你可以认为 MVVM 是 MVP 版本的一个进化,而这得益于 UI 绑定。
UI 绑定是在 View
和 ViewModel
之间建立一座单向或双向的桥梁,并且两者之间以一种非常透明地方式进行沟通。
不幸地是,iOS 没有原生的方式来实现,所以你必须通过三方库/框架或者自己写一个来达成目的。
在 Swift 里有多种方式实现 UI 绑定:
RxSwift (或 ReactiveCocoa)
RxSwift 是 ReactiveX 家族的一个 Swift 版本的实现。一旦你掌握了它,你就能很轻松地切换到 RxJava、RxJavascript 等等。
这个框架允许你来用函数式(FRP)的方式来编写程序,并且由于内部库 RxCocoa,你可以轻松实现 View
和 ViewModel
之间的绑定:
class ViewController: UIViewController {
@IBOutlet private weak var userLabel: UILabel!
private let viewModel: ViewModel
private let disposeBag: DisposeBag
private func bindToViewModel() {
viewModel.myProperty
.drive(userLabel.rx.text)
.disposed(by: disposeBag)
}
}复制代码
我不会解释如何彻底地使用 RxSwift,因为这超出本文的目标,它自己会有文章来解释。
FRP 让你学习到了一种新的方式来开发,你可能对它或爱或恨。如果你没用过 FRP 开发,那你需要花费几个小时来熟悉和理解如何正确地使用它,因为它是一个完全不同的编程概念。
另一个类似于 RxSwift 的框架是 ReactiveCocoa,如果你想了解他们之间主要的区别的话,你可以看看这篇文章。
代理
如果你想避免导入并学习新的框架,你可以使用代理作为替代。不幸地是,使用这种方法,你将失去透明绑定的功能,因为你必须手动绑定。这个版本的 MVVM 非常类似于 MVP。
这种方式的策略是通过 View
内部的 ViewModel
保持一个对代理实现的引用。这样 ViewModel
就能在无需引用任何 UIKit
对象的情况下更新 View
。
这有个例子:
class ViewController: UIViewController, ViewModelDelegate {
@IBOutlet private weak var userLabel: UILabel?
private let viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
viewModel.delegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func userNameDidChange(text: String) {
userLabel?.text = text
}
}
protocol ViewModelDelegate: class {
func userNameDidChange(text: String)
}
class ViewModel {
private var userName: String {
didSet {
delegate?.userNameDidChange(text: userName)
}
}
weak var delegate: ViewModelDelegate? {
didSet {
delegate?.userNameDidChange(text: userName)
}
}
init() {
userName = "I ? hardcoded values"
}
}复制代码
闭包
和代理非常相似,不过不同的是,你使用的是闭包来代替代理。
闭包是 ViewModel
的属性,而 View
使用它们来更新 UI。你必须注意在闭包里使用 [weak self]
,避免造成循环引用。
关于 Swift 闭包的循环引用,你可以阅读这篇文章。
这有一个例子:
class ViewController: UIViewController {
@IBOutlet private weak var userLabel: UILabel?
private let viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
viewModel.userNameDidChange = { [weak self] (text: String) in
self?.userNameDidChange(text: text)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func userNameDidChange(text: String) {
userLabel?.text = text
}
}
class ViewModel {
var userNameDidChange: ((String) -> Void)? {
didSet {
userNameDidChange?(userName)
}
}
private var userName: String {
didSet {
userNameDidChange?(userName)
}
}
init() {
userName = "I ? hardcoded values"
}
}复制代码
抉择: MVVM-C
在你不得不选择一个架构模式时,你需要理解哪一种更适合你的需求。在这些模式里,MVVM 是最好的选择之一,因为它强大的同时,也易于使用。
不幸地是这种模式并不完美,主要的缺陷是 MVVM 没有路由管理。
我们要添加一层新的结构,来让它获得 MVVM 的特性,并且具备路由的功能。于是它就变成了:Model-View-ViewModel-Coordinator (MVVM-C)
示例的项目会展示 Coordinator
如何工作,并且如何管理不同的层次。
入门
你可以在这里下载项目源码。
这个例子被简化了,以便于你可以专注于 MVVM-C 是如何工作的,因此 GitHub 上的类可能会有轻微出入。
示例应用是一个普通的仪表盘应用,它从公共 API 获取数据,一旦数据准备就绪,用户就可以通过 ID 查找实体,如下面的截图:
应用程序有不同的方式来添加视图控制器,所以你会看到,在有子视图控制器的边缘案例中,如何使用 Coordinator
。
MVVM-C 的层级结构
Coordinator
它的职责是显示一个新的视图,并注入 View
和 ViewModel
所需要的依赖。
Coordinator
必须提供一个 start
方法,来创建 MVVM 层次并且添加 View
到视图的层级结构中。
你可能会经常有一组 Coordinator
子类,因为在你当前的视图中,可能会有子视图,就像我们的例子一样:
final class DashboardContainerCoordinator: Coordinator {
private var childCoordinators = [Coordinator]()
private weak var dashboardContainerViewController: DashboardContainerViewController?
private weak var navigationController: UINavigationControllerType?
private let disposeBag = DisposeBag()
init(navigationController: UINavigationControllerType) {
self.navigationController = navigationController
}
func start() {
guard let navigationController = navigationController else { return }
let viewModel = DashboardContainerViewModel()
let container = DashboardContainerViewController(viewModel: viewModel)
bindShouldLoadWidget(from: viewModel)
navigationController.pushViewController(container, animated: true)
dashboardContainerViewController = container
}
private func bindShouldLoadWidget(from viewModel: DashboardContainerViewModel) {
viewModel.rx_shouldLoadWidget.asObservable()
.subscribe(onNext: { [weak self] in
self?.loadWidgets()
})
.addDisposableTo(disposeBag)
}
func loadWidgets() {
guard let containerViewController = usersContainerViewController() else { return }
let coordinator = UsersCoordinator(containerViewController: containerViewController)
coordinator.start()
childCoordinators.append(coordinator)
}
private func usersContainerViewController() -> ContainerViewController? {
guard let dashboardContainerViewController = dashboardContainerViewController else { return nil }
return ContainerViewController(parentViewController: dashboardContainerViewController,
containerView: dashboardContainerViewController.usersContainerView)
}
}复制代码
你一定能注意到在 Coordinator
里,一个父类 UIViewController
对象或者子类对象,比如 UINavigationController
,被注入到构造器之中。因为 Coordinator
有责任添加 View
到视图层级之中,它必须知道那个父类添加了 View
。
在上面的例子里,DashboardContainerCoordinator
实现了协议 Coordinator
:
protocol Coordinator {
func start()
}复制代码
这便于你使用多态)。
创建完第一个 Coordinator
后,你必须把它作为程序的入口放到 AppDelegate
中:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private let navigationController: UINavigationController = {
let navigationController = UINavigationController()
navigationController.navigationBar.isTranslucent = false
return navigationController
}()
private var mainCoordinator: DashboardContainerCoordinator?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow()
window?.rootViewController = navigationController
let coordinator = DashboardContainerCoordinator(navigationController: navigationController)
coordinator.start()
window?.makeKeyAndVisible()
mainCoordinator = coordinator
return true
}
}复制代码
在 AppDelegate
里,我们实例化一个新的 DashboardContainerCoordinator
,通过 start
方法,我们把新的视图推入 navigationController
里。
你可以看到在 GitHub 上的项目是如何注入一个 UINavigationController
类型的对象,并去除 UIKit
和 Coordinator
之间的耦合。
Model
Model
代表数据。它必须尽可能的简洁,没有业务逻辑。
struct UserModel: Mappable {
private(set) var id: Int?
private(set) var name: String?
private(set) var username: String?
init(id: Int?, name: String?, username: String?) {
self.id = id
self.name = name
self.username = username
}
init?(map: Map) { }
mutating func mapping(map: Map) {
id <- map["id"]
name <- map["name"]
username <- map["username"]
}
}复制代码
实例项目使用开源库 ObjectMapper 将 JSON 转换为对象。
ObjectMapper 是一个使用 Swift 编写的框架。它可以轻松的让你在 JSON 和模型对象(类和结构体)之间相互转换。
在你从 API 获得一个 JSON 响应的时候,它会非常有用,因为你必须创建模型对象来解析 JSON 字符串。
View
View
是一个 UIKit
对象,就像 UIViewController
一样。
它通常持有一个 ViewModel
的引用,通过 Coordinator
注入来创建绑定。
final class DashboardContainerViewController: UIViewController {
let disposeBag = DisposeBag()
private(set) var viewModel: DashboardContainerViewModelType
init(viewModel: DashboardContainerViewModelType) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
configure(viewModel: viewModel)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(viewModel: DashboardContainerViewModelType) {
viewModel.bindViewDidLoad(rx.viewDidLoad)
viewModel.rx_title
.drive(rx.title)
.addDisposableTo(disposeBag)
}
}复制代码
在这个例子中,视图控制器中的标题被绑定到 ViewModel
的 rx_title
属性上。这样在 ViewModel
更新 rx_title
值的时候,视图控制器中的标题就会根据新的值自动更新。
ViewModel
ViewModel
是这种架构模式的核心层。它的职责是保持 View
和 Model
的更新。由于业务逻辑在这个类中,你需要用不同的组件的单一职责来保证 ViewModel
尽可能的干净。
final class UsersViewModel {
private var dataProvider: UsersDataProvider
private var rx_usersFetched: Observable<[UserModel]>
lazy var rx_usersCountInfo: Driver<String> = {
return UsersViewModel.createUsersCountInfo(from: self.rx_usersFetched)
}()
var rx_userFound: Driver<String> = .never()
init(dataProvider: UsersDataProvider) {
self.dataProvider = dataProvider
rx_usersFetched = dataProvider.fetchData(endpoint: "http://jsonplaceholder.typicode.com/users")
.shareReplay(1)
}
private static func createUsersCountInfo(from usersFetched: Observable<[UserModel]>) -> Driver<String> {
return usersFetched
.flatMapLatest { users -> Observable<String> in
return .just("The system has \(users.count) users")
}
.asDriver(onErrorJustReturn: "")
}
}复制代码
在这个例子中,ViewModel
有一个在构造器中注入的数据提供者,它用于从公共 API 中获取数据。一旦数据提供者返回了取得的数据,ViewModel
就会通过 rx_usersCountInfo
发射一个新用户数量相关的新事件。因为绑定了观察者 rx_usersCountInfo
,这个新事件会被发送给 View
,然后更新 UI。
可能会有很多不同的组件在你的 ViewModel
里,比如一个用来管理数据库(CoreData、Realm 等等)的数据控制器,一个用来与你 API 和其他任何外部依赖交互的数据提供者。
因为所有 ViewModel
都使用了 RxSwift,所以当一个属性是 RxSwift 类型(Driver
、Observable
等等)的时候,就会有一个 rx_
前缀。这不是强制的,只是它可以帮助你更好的识别哪些属性是 RxSwift 对象。
结论
MVVM-C 有很多优点,可以提高应用程序的质量。你应该注意使用哪种方式来进行 UI 绑定,因为 RxSwift 不容易掌握,而且如果你不明白你做的是什么,调试和测试有时可能会有点棘手。
我的建议是一点点地开始使用这种架构模式,这样你可以学习不同层次的使用,并且能保证层次之间的良好的分离,易于测试。
FAQ
MVVM-C 有什么限制吗?
是的,当然有。如果你正做一个复杂的项目,你可能会遇到一些边缘案例,MVVM-C 可能无法使用,或者在一些小功能上使用过度。如果你开始使用 MVVM-C,并不意味着你必须在每个地方都强制的使用它,你应该始终选择更适合你需求的架构。
我能用 RxSwift 同时使用函数式和命令式编程吗?
是的,你可以。但是我建议你在遗留的代码中保持命令式的方式,而在新的实现里使用函数式编程,这样你可以利用 RxSwift 强大的优势。如果你使用 RxSwift 仅仅为了 UI 绑定,你可以轻松使用命令式编写程序,而只用函数响应式编程来设置绑定。
我可以在企业项目中使用 RxSwift 吗?
这取决于你要开新项目,还是要维护旧代码。在有遗留代码的项目中,你可能无法使用 RxSwift,因为你需要重构很多的类。如果你有时间和资源来做,我建议你新开一项目一点一点的做,否则还是尝试其他的方法来解决 UI 绑定的问题。
需要考虑的一个重要事情是,RxSwift 最终会成为你项目中的另一个依赖,你可能会因为 RxSwift 的破坏性改动而导致浪费时间的风险,或者缺少要在边缘案例中实现功能的文档。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。