[译] MVVM-C 与 Swift


MVVM-C 与 Swift

简介

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

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

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

架构模式

它是什么

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

摘自 Wikipedia

在你开始一个新项目或功能的时候,你需要花一些时间来思考架构模式的使用。通过一个透彻的分析,你可以避免耗费很多天的时间在重构一个混乱的代码库上。

主要的模式

在项目中,有几种可用的架构模式,并且你可以在项目中使用多个,因为每个模式都能更好地适应特定的场景。

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

Model-View-Controller

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

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

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前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值