- 原文地址:MVVM-C with Swift
- 原文做者:Marco Santarossa
- 译文出自:掘金翻译计划
- 译者:Deepmissea
- 校对者:atuooo,1992chenlu
现今,iOS 开发者面临的最大挑战是构建一个健壮的应用程序,它必须易于维护、测试和扩展。javascript
在这篇文章里,你会学到一种可靠的方法来达到目的。html
首先,简要介绍下你即将学习的内容:
架构模式.前端
架构模式是给定上下文中软件体系结构中常见的,可重用的解决方案。架构与软件设计模式类似,但涉及的范围更广。架构解决了软件工程中的各类问题,如计算机硬件性能限制,高可用性和最小化业务风险。一些架构模式已经在软件框架内实现。java
摘自 Wikipedia。react
在你开始一个新项目或功能的时候,你须要花一些时间来思考架构模式的使用。经过一个透彻的分析,你能够避免耗费不少天的时间在重构一个混乱的代码库上。android
在项目中,有几种可用的架构模式,而且你能够在项目中使用多个,由于每一个模式都能更好地适应特定的场景。ios
当你阅读这几种模式时,主要会遇到:git
这是最多见的,也许在你的第一个 iOS 应用中已经使用过。不幸地是,这也是最糟糕的模式,由于 Controller
不得无论理每个依赖(API、数据库等等),包括你应用的业务逻辑,并且与 UIKit
的耦合度很高,这意味着很难去测试。github
你应该避免这种模式,用下面的某种来代替它。数据库
这是第一个 MVC 模式的备选方案之一,一次对 Controller
和 View
之间解耦的很好的尝试。
在 MVP 中,你有一层叫作 Presenter
的新结构来处理业务逻辑。而 View
—— 你的 UIViewController
以及任何 UIKit
组件,都是一个笨的对象,他们只经过 Presenter
更新,并在 UI 事件被触发的时候,负责通知 Presenter
。因为 Presenter
没有任何 UIKit
的引用,因此很是容易测试。
这是 Bob 叔叔的清晰架构的表明。
这种模式的强大之处在于,它合理分配了不一样层次之间的职责。经过这种方式,你的每一个层次作的的事变得不多,易于测试,而且具有单一职责。这种模式的问题是,在大多数场合里,它过于复杂。你须要管理不少层,这会让你感到混乱,难于管理。
这种模式并不容易掌握,你能够在这里找到关于这种架构模式更详细的文章。
最后但也是最重要的,MVVM 是一个相似于 MVP 的框架,由于层级结构几乎相同。你能够认为 MVVM 是 MVP 版本的一个进化,而这得益于 UI 绑定。
UI 绑定是在 View
和 ViewModel
之间创建一座单向或双向的桥梁,而且二者之间以一种很是透明地方式进行沟通。
不幸地是,iOS 没有原生的方式来实现,因此你必须经过三方库/框架或者本身写一个来达成目的。
在 Swift 里有多种方式实现 UI 绑定:
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 是最好的选择之一,由于它强大的同时,也易于使用。
不幸地是这种模式并不完美,主要的缺陷是 MVVM 没有路由管理。
咱们要添加一层新的结构,来让它得到 MVVM 的特性,而且具有路由的功能。因而它就变成了:Model-View-ViewModel-Coordinator (MVVM-C)
示例的项目会展现 Coordinator
如何工做,而且如何管理不一样的层次。
你能够在这里下载项目源码。
这个例子被简化了,以便于你能够专一于 MVVM-C 是如何工做的,所以 GitHub 上的类可能会有轻微出入。
示例应用是一个普通的仪表盘应用,它从公共 API 获取数据,一旦数据准备就绪,用户就能够经过 ID 查找实体,以下面的截图:
应用程序有不一样的方式来添加视图控制器,因此你会看到,在有子视图控制器的边缘案例中,如何使用 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
表明数据。它必须尽量的简洁,没有业务逻辑。
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
是一个 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
是这种架构模式的核心层。它的职责是保持 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 不容易掌握,并且若是你不明白你作的是什么,调试和测试有时可能会有点棘手。
个人建议是一点点地开始使用这种架构模式,这样你能够学习不一样层次的使用,而且能保证层次之间的良好的分离,易于测试。
MVVM-C 有什么限制吗?
是的,固然有。若是你正作一个复杂的项目,你可能会遇到一些边缘案例,MVVM-C 可能没法使用,或者在一些小功能上使用过分。若是你开始使用 MVVM-C,并不意味着你必须在每一个地方都强制的使用它,你应该始终选择更适合你需求的架构。
我能用 RxSwift 同时使用函数式和命令式编程吗?
是的,你能够。可是我建议你在遗留的代码中保持命令式的方式,而在新的实现里使用函数式编程,这样你能够利用 RxSwift 强大的优点。若是你使用 RxSwift 仅仅为了 UI 绑定,你能够轻松使用命令式编写程序,而只用函数响应式编程来设置绑定。
我能够在企业项目中使用 RxSwift 吗?
这取决于你要开新项目,仍是要维护旧代码。在有遗留代码的项目中,你可能没法使用 RxSwift,由于你须要重构不少的类。若是你有时间和资源来作,我建议你新开一项目一点一点的作,不然仍是尝试其余的方法来解决 UI 绑定的问题。
须要考虑的一个重要事情是,RxSwift 最终会成为你项目中的另外一个依赖,你可能会由于 RxSwift 的破坏性改动而致使浪费时间的风险,或者缺乏要在边缘案例中实现功能的文档。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。