[译] MVVM-C 与 Swift


MVVM-C 与 Swift

简介

现今,iOS 开发者面临的最大挑战是构建一个健壮的应用程序,它必须易于维护、测试和扩展。javascript

在这篇文章里,你会学到一种可靠的方法来达到目的。html

首先,简要介绍下你即将学习的内容:
架构模式.前端

架构模式

它是什么

架构模式是给定上下文中软件体系结构中常见的,可重用的解决方案。架构与软件设计模式类似,但涉及的范围更广。架构解决了软件工程中的各类问题,如计算机硬件性能限制,高可用性和最小化业务风险。一些架构模式已经在软件框架内实现。java

摘自 Wikipediareact

在你开始一个新项目或功能的时候,你须要花一些时间来思考架构模式的使用。经过一个透彻的分析,你能够避免耗费不少天的时间在重构一个混乱的代码库上。android

主要的模式

在项目中,有几种可用的架构模式,而且你能够在项目中使用多个,由于每一个模式都能更好地适应特定的场景。ios

当你阅读这几种模式时,主要会遇到:git

Model-View-Controller

这是最多见的,也许在你的第一个 iOS 应用中已经使用过。不幸地是,这也是最糟糕的模式,由于 Controller 不得无论理每个依赖(API、数据库等等),包括你应用的业务逻辑,并且与 UIKit 的耦合度很高,这意味着很难去测试。github

你应该避免这种模式,用下面的某种来代替它。数据库

Model-View-Presenter

这是第一个 MVC 模式的备选方案之一,一次对 ControllerView 之间解耦的很好的尝试。

在 MVP 中,你有一层叫作 Presenter 的新结构来处理业务逻辑。而 View —— 你的 UIViewController 以及任何 UIKit 组件,都是一个笨的对象,他们只经过 Presenter 更新,并在 UI 事件被触发的时候,负责通知 Presenter。因为 Presenter 没有任何 UIKit 的引用,因此很是容易测试。

Viper

这是 Bob 叔叔的清晰架构的表明。

这种模式的强大之处在于,它合理分配了不一样层次之间的职责。经过这种方式,你的每一个层次作的的事变得不多,易于测试,而且具有单一职责。这种模式的问题是,在大多数场合里,它过于复杂。你须要管理不少层,这会让你感到混乱,难于管理。

这种模式并不容易掌握,你能够在这里找到关于这种架构模式更详细的文章。

Model-View-ViewModel

最后但也是最重要的,MVVM 是一个相似于 MVP 的框架,由于层级结构几乎相同。你能够认为 MVVM 是 MVP 版本的一个进化,而这得益于 UI 绑定。

UI 绑定是在 ViewViewModel 之间创建一座单向或双向的桥梁,而且二者之间以一种很是透明地方式进行沟通。

不幸地是,iOS 没有原生的方式来实现,因此你必须经过三方库/框架或者本身写一个来达成目的。

在 Swift 里有多种方式实现 UI 绑定:

RxSwift (或 ReactiveCocoa)

RxSwiftReactiveX 家族的一个 Swift 版本的实现。一旦你掌握了它,你就能很轻松地切换到 RxJava、RxJavascript 等等。

这个框架容许你来用函数式(FRP)的方式来编写程序,而且因为内部库 RxCocoa,你能够轻松实现 ViewViewModel 之间的绑定:

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

它的职责是显示一个新的视图,并注入 ViewViewModel 所须要的依赖。

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 类型的对象,并去除 UIKitCoordinator 之间的耦合。

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)
    }
}复制代码

在这个例子中,视图控制器中的标题被绑定到 ViewModelrx_title 属性上。这样在 ViewModel 更新 rx_title 值的时候,视图控制器中的标题就会根据新的值自动更新。

ViewModel

ViewModel 是这种架构模式的核心层。它的职责是保持 ViewModel 的更新。因为业务逻辑在这个类中,你须要用不一样的组件的单一职责来保证 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 类型(DriverObservable 等等)的时候,就会有一个 rx_ 前缀。这不是强制的,只是它能够帮助你更好的识别哪些属性是 RxSwift 对象。

结论

MVVM-C 有不少优势,能够提升应用程序的质量。你应该注意使用哪一种方式来进行 UI 绑定,由于 RxSwift 不容易掌握,并且若是你不明白你作的是什么,调试和测试有时可能会有点棘手。

个人建议是一点点地开始使用这种架构模式,这样你能够学习不一样层次的使用,而且能保证层次之间的良好的分离,易于测试。

FAQ

MVVM-C 有什么限制吗?

是的,固然有。若是你正作一个复杂的项目,你可能会遇到一些边缘案例,MVVM-C 可能没法使用,或者在一些小功能上使用过分。若是你开始使用 MVVM-C,并不意味着你必须在每一个地方都强制的使用它,你应该始终选择更适合你需求的架构。

我能用 RxSwift 同时使用函数式和命令式编程吗?

是的,你能够。可是我建议你在遗留的代码中保持命令式的方式,而在新的实现里使用函数式编程,这样你能够利用 RxSwift 强大的优点。若是你使用 RxSwift 仅仅为了 UI 绑定,你能够轻松使用命令式编写程序,而只用函数响应式编程来设置绑定。

我能够在企业项目中使用 RxSwift 吗?

这取决于你要开新项目,仍是要维护旧代码。在有遗留代码的项目中,你可能没法使用 RxSwift,由于你须要重构不少的类。若是你有时间和资源来作,我建议你新开一项目一点一点的作,不然仍是尝试其余的方法来解决 UI 绑定的问题。

须要考虑的一个重要事情是,RxSwift 最终会成为你项目中的另外一个依赖,你可能会由于 RxSwift 的破坏性改动而致使浪费时间的风险,或者缺乏要在边缘案例中实现功能的文档。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划

相关文章
相关标签/搜索