新的iOS软件体系结构:4V引擎
原始文章:https://marcosantadev.com/new-ios-software-architecture-4v-engine/
您应该阅读这篇文章吗?
本文介绍的是一种新的软件体系结构,它具有比VIPER和MVVM-C更高的层次。 这意味着它可能比其他已知体系结构更复杂。
如果您想继续阅读本文,则必须接受我的观点,即如果我们想要一个干净且可测试的体系结构,则我们应该有几层责任。
我不想将其作为可以解决您所有问题的完美架构来出售。 它可能无法完全满足您的需求。 出于这个原因,我建议您通读这篇文章,并自己判断这种体系结构对您的项目是否有意义。
如果您想知道为什么MVC不足以进行iOS开发,建议您跳入为什么MVC不足。
介绍
我创建了这个博客,撰写了有关MVVM-C的文章。 然后,我写了一篇关于SOLID原理的文章。 在这一点上,您可能会认为我没有实践我的讲道。 如果我说的是“单一责任原则”,那么即使Coordinator
层承担多个责任,为什么我仍要使用MVVM-C? 它创建堆栈( View Model
, View
和Service
),在父UIKit
组件中添加View
并确定路由添加/删除子协调器。 MVVM-C必须进行一些重构,以免违反“单一职责原则”。
在本文中,我将解释主要的iOS软件体系结构的替代方案: 4V Engine 。
祝您阅读愉快!
为什么MVC不够
我在上一篇有关MVVM-C的文章中已经介绍了这一点,但是我想再次写出来,因为我认为它非常重要。
成为iOS开发人员的一种常见方法是查看文档,并按照Apple建议的模式编写一些简单项目。 这意味着我们大多数人已经开始使用MVC作为软件架构来创建我们的第一个应用程序。 逐步地,我们开始习惯于MVC。 在学习过程的这一点上,我们认为MVC是正确的方法。 它起作用了,为什么我们要迁移到另一个增加代码复杂性的体系结构中?
主要有两个原因:
- SOLID原则:视图控制器的职责过多。
- 可测试性:视图控制器与
UIKit
紧密结合,因此很难进行测试。
如果我们想解决以上问题,则应该采用另一种架构。 不幸的是,没有什么是免费的。 这种改变有一个代价:复杂性。 如果我们查看VIPER和MVVM-C,我们会注意到有多个层次可以管理并让它们一起通信。 如果我们拥有简单的应用程序,或者我们实际上并不关心SOLID和可测试性,则可能会适得其反。
我个人的观点是:
我喜欢编写遵循SOLID原则并经过适当测试以尽可能避免错误的编写良好的代码。 因此,我想花时间和精力来创建新的干净软件体系结构。
MVVM-C的问题
如前所述,MVVM-C还不够。 Coordinator
层违反了“单一责任原则”,必须分为三个部分,承担以下职责:
- 管理应用程序路由。
- 创建堆栈(
View Model
,View
和Service
)。 - 在父
UIKit
组件中显示View
。
分解Coordinator
想法使我转向4V Engine。
我要感谢我的朋友 Ennio Masi 指出了这个 Coordinator
问题,这促使我找到了解决方案。
VIPER的问题
另一个著名的iOS架构是VIPER。 它具有几层,与MVVM-C非常相似,只是命名不同。
如果分析VIPER,我们会发现MVVM-C的相同问题。 有罪的层称为Wireframe
,它是体系结构的路由器。
为了便于说明,我们使用从VIPER-SWIFT复制的以下代码:
class AddWireframe: NSObject, UIViewControllerTransitioningDelegate {
var addPresenter : AddPresenter?
var presentedViewController : UIViewController?
func presentAddInterfaceFromViewController(_ viewController: UIViewController) {
let newViewController = addViewController()
newViewController.eventHandler = addPresenter
newViewController.modalPresentationStyle = .custom
newViewController.transitioningDelegate = self
viewController.present(newViewController, animated: true, completion: nil)
presentedViewController = newViewController
}
//.....
我们可以注意到presentAddInterfaceFromViewController
太多:
- 管理路由。
- 创建
View
及其属性。 - 在父
UIViewController
显示View
。
我们注意到,就这三点而言,我们遇到了与MVVM-C Coordinator
相同的问题。
4V引擎
我们已经分析了常见的iOS软件架构,并且发现了一些问题。 如果我们愿意开发人员并且希望改进我们的代码,那么我们将需要一种重构先前代码的替代方法。 出于这个目标,我创建了这个新的软件架构:
不用担心,这可能会造成混淆,但是我们很快将对每个层进行解释。
如上图所示,该架构的核心由View Presenter
, View Factory
, View
和View Model
,因此该架构被称为4 V引擎。
入门
现在,是时候解释每个单独的层了。 由于我认为跳入代码是学习知识的最佳方法,因此我们将使用示例应用程序通过示例覆盖每一层。
您可以在这里找到Github仓库。
这是一个非常简单的应用程序,包含两个组件:
- 用户列表:从远程API获取的用户列表。
- 用户详细信息:具有在用户列表中选择的用户名的视图-我们可以在用户列表中点击
UITableViewCell
的信息按钮来选择用户。
层数
我认为解释这些层的最佳方法是从底部( Model
)到顶部( Router
)。 开始吧。
模型
该模型表示我们应用程序的数据。
在示例应用程序中,我们有一个模型User
:
struct User {
let name: String
}
代表从API响应中解析出的单个用户。
互动者
Interactor
器与VIPER中使用的相同。
该层管理Model
,以为View Model
准备数据。 View Model
不应直接在模型上执行任何操作,但应委托Interactor
进行任何数据操作。
在示例应用程序中,我们有一个交互器,该交互器HTTPClient
服务HTTPClient
从远程API获取用户,然后将在User
数组中接收到的JSON数据转换为多亏了助手类UsersParser
以便在View Model
:
final class UsersListInteractor {
private let httpClient: HTTPClientType
init(httpClient: HTTPClientType = HTTPClient()) {
self.httpClient = httpClient
}
func fetchUsers(completion: @escaping ([User]) -> Void) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
completion([])
return
}
let httpCompletionHandler: (Data) -> Void = { data in
guard let jsonData = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [[String: Any]] else {
completion([])
return
}
let users = UsersParser.parse(jsonData)
completion(users)
}
httpClient.get(at: url, completionHandler: httpCompletionHandler)
}
}
查看模型
我们可以认为View Model
是该体系结构中最重要的层。 它的职责是与UI交互,以决定在UI操作后显示什么以及如何表现。
该层不应包含任何UIKit
引用。 如果我们想在View
和View Model
之间进行通信,则应该使用UI绑定机制。 我已经在上一篇文章中介绍了主要机制。 对于此示例应用程序,我决定避免使用RxSwift进行绑定,因为它会增加示例的复杂性。 为了使所有内容尽可能简单,使用委托模式已实现了绑定。
我们可以将View Model
及其Interactor
一起使用,以将数据显示在UI中,如示例应用程序所示:
// Delegate used to bind the UI and the View Model
protocol UsersListViewModelDelegate: class {
func usersListUpdated()
}
final class UsersListViewModel {
// Value used in View to know how many table rows to show
var usersCount: Int {
return users.count
}
private weak var delegate: UsersListViewModelDelegate?
private weak var navigationDelegate: UsersListNavigationDelegate?
private let usersListInteractor: UsersListInteractor
private var users = [User]()
init(usersListInteractor: UsersListInteractor, navigationDelegate: UsersListNavigationDelegate) {
self.navigationDelegate = navigationDelegate
self.usersListInteractor = usersListInteractor
loadUsers()
}
// Asks the interactor the list of users to show in the UI
private func loadUsers() {
usersListInteractor.fetchUsers { [unowned self] users in
self.users = users
DispatchQueue.main.async {
// Method used to ask the View to update the table view with the new data
self.delegate?.usersListUpdated()
}
}
}
private func user(at indexPath: IndexPath) -> User {
return users[indexPath.row]
}
// Sets the delegate to bind the UI
func bind(_ delegate: UsersListViewModelDelegate) {
self.delegate = delegate
}
// Method used in View to know which user name to show in the cell
func userName(at indexPath: IndexPath) -> String {
let user = self.user(at: indexPath)
return user.name
}
// Method called in View when the user taps a cell detail button
func userDetailsSelected(at indexPath: IndexPath) {
let user = self.user(at: indexPath)
// Method used to notify the router that a user has been selected
navigationDelegate?.usersListSelected(for: user)
}
}
使用MVC,我们可以将业务逻辑保留在视图控制器中。 借助4V Engine,我们可以在View Model
内部移动业务逻辑并轻松对其进行测试,因为我们与UIKit
没有依赖关系。
注意:
-
navigationDelegate
用于与Router
通信。 我们将在路由器中看到它。 - 方法
bind
用于View
和View Model
之间的UI绑定。 我们将在“视图”中看到它。
视图
View
层表示用于在设备屏幕上显示内容的任何UIKit
组件。
在示例应用程序中, View
是用于用户详细信息的UIViewController
和用于用户列表的UITableViewController
。
好的架构的优点是我们可以轻松地测试我们的图层。 View
通常最难测试,因为它与依赖项UIKit
结合在一起。 因此,我们必须使该层尽可能平淡无奇,并在可测试的层中移动业务逻辑。 “可测试”层是View Model
。 正如我们在View Model中看到的那样,UI数据由View Model
驱动。 这样,我们可以在View Model
内部移动业务逻辑。 View
变成一个愚蠢的图层,仅用于在设备屏幕上显示某些内容。
使用View
理解的重要概念是UI绑定,它使我们能够设置View Model
和View
之间的通信。 如果您不知道什么是UI绑定,请看一下我以前的文章。
这是示例应用程序中的View
的示例:
class UsersListTableViewController: UITableViewController {
// The view model used for the binding
private unowned let viewModel: UsersListViewModel
init(viewModel: UsersListViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib.init(nibName: "UsersListTableViewCell", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "Cell")
// Binds View and View Model
viewModel.bind(self)
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Asks the View Model how many users are available
return viewModel.usersCount
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
// If it's the custom cell, configure it
if let usersListCell = cell as? UsersListTableViewCell {
// Asks the View Model the user name for a specific index path
let userName = viewModel.userName(at: indexPath)
// Sets the user name
usersListCell.configure(userName: userName)
}
return cell
}
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
// Notifies the View Model that a detail button has been tapped
viewModel.userDetailsSelected(at: indexPath)
}
}
在本示例中可以看到,UI绑定通常是双向的,有时我们向View Model
请求一些数据,有时又被View Model
通知以更新UI,例如使用usersListUpdated
。
注意:
属性viewModel
具有关键字viewModel
。 需要避免保留周期。 由于View Factory
已经保留了View Model
的强大参考(正如我们将在View Factory中看到的那样),因此View不需要为其View Model
保持强大的参考。
查看工厂
到目前为止,这些层与VIPER和MVVM-C非常相似。 现在,是时候解释一下乍一看可能令人困惑的图层了。
View Factory
的职责是创建架构的核心: View
, View Model
和Interactor
。
单独使用View Factory
可能没有多大意义,我们必须在正确的上下文中查看它。 我们将在View Presenter中了解其用法。
让我们从示例应用程序中查看一个示例:
final class UsersListViewFactory {
let viewController: UsersListTableViewController
private let viewModel: UsersListViewModel
init(navigationDelegate: UsersListNavigationDelegate) {
let interactor = UsersListInteractor()
viewModel = UsersListViewModel(usersListInteractor: interactor, navigationDelegate: navigationDelegate)
viewController = UsersListTableViewController(viewModel: viewModel)
}
}
注意:
- 我们公开了要在
View Presenter
使用的viewController
。 我们也可能出于特定原因公开View Model
。 我认为大多数时候它可以是私人的。 决定取决于您要实现的目标。 - 我们在
UsersListViewModel
中注入UsersListNavigationDelegate
,以使Router
以抽象方式与View Model
进行通信。 我们将在Router中看到此委托的详细信息。
查看演示者
该层的名称可能会有些混乱。 我们习惯将 Presenter
称为更新 View
层—在VIPER和MVP中具有此层。 在此体系结构中,演示者称为 View Model
并且该层不是演示者,而是 View 演示者。 继续阅读以了解其责任。
View Presenter
是使用4V Engine编写的组件的最后一个难题。
该层负责在设备屏幕中显示组件。
为了实现这个目标,它必须知道显示什么以及在哪里显示。 要添加的View Factory
由View Factory
提供,并且父View Factory
是从外部注入的。
让我们从示例应用程序中查看一个示例:
final class UsersListViewPresenter: ViewPresenter {
private let viewFactory: UsersListViewFactory
init(navigationDelegate: UsersListNavigationDelegate) {
viewFactory = UsersListViewFactory(navigationDelegate: navigationDelegate)
}
// Method to add the component in a parent view controller
func present(in parentViewController: UIViewController) {
// Method of UIViewControllerExtension.swift to add a child view controller filling the parent view with
// autolayout.
// Look at UIViewControllerExtension.swift for more details
parentViewController.addFillerChildViewController(viewFactory.viewController)
}
// Method to remove the component from the device screen
func remove() {
viewFactory.viewController.view.removeFromSuperview()
viewFactory.viewController.removeFromParentViewController()
}
}
在此示例中, present()
是添加子视图控制器的非常简单的方法。 如果您有精美的UIViewController
过渡,则此方法是管理过渡的正确位置。
您可能会注意到,我们正在 通过各层 传播 UsersListNavigationDelegate
以便在 View Model
使用它 。 这是将体系结构分为几层的缺点。
路由器
我们刚刚看完单个组件的各个层。 至此,我们几乎已经准备好在屏幕上显示该组件。 我们需要最后一步:决定何时显示组件。 这是Router
的职责。
我们通常每个故事都有一个Router
。 在这种情况下,我对故事的定义是:
这组组件一起在我们的应用程序中定义了流程。
在示例应用程序中,我们有故事“ Users
,这是一组用户列表和用户详细信息。 其他故事可以是:
- 入门:一组视图以显示如何使用该应用程序。
- 注册:一组用于创建新帐户,接受使用条款,验证电子邮件等的视图…
- 购买项目:显示购物篮的一组视图,添加送货地址,添加付款卡明细,…
让我们看看如何在示例应用程序中将Router
用于故事Users
:
// Delegate used to navigate from users list to user details
protocol UsersListNavigationDelegate: class {
func usersListSelected(for user: User)
}
// Delegate used to close the user details
protocol UserDetailsNavigationDelegate: class {
func userDetailsCloseDidTap()
}
final class UsersRouter {
// Parent view controller to add the components
fileprivate let parentViewController: UIViewController
// Dictionary of presenters used
fileprivate var presenters = [String: ViewPresenter]()
init(parentViewController: UIViewController) {
self.parentViewController = parentViewController
}
}
extension UsersRouter: Router {
// Shows first component, the users list
func showInitial() {
let usersListPresenter = UsersListViewPresenter(navigationDelegate: self)
usersListPresenter.present(in: parentViewController)
presenters["UsersList"] = usersListPresenter
}
// Closes the router removing all its components
func close() {
presenters.keys.forEach { [unowned self] in
self.removePresenter(for: $0)
}
}
fileprivate func removePresenter(for key: String) {
let userDetailsPresenter = presenters[key]
userDetailsPresenter?.remove()
presenters[key] = nil
}
}
extension UsersRouter: UsersListNavigationDelegate {
func usersListSelected(for user: User) {
let userDetailsPresenter = UserDetailsViewPresenter(user: user, navigationDelegate: self)
userDetailsPresenter.present(in: parentViewController)
presenters["UserDetails"] = userDetailsPresenter
}
}
extension UsersRouter: UserDetailsNavigationDelegate {
func userDetailsCloseDidTap() {
// Removes user details components from the parent view controller
removePresenter(for: "UserDetails")
}
}
注意:
-
Router
有一个与主持人一起使用的字典。 这样,如果我们要删除诸如userDetailsCloseDidTap
中的组件,则可以使用键轻松获得合适的演示者。
我们可以看看AppDelegate
来了解如何使用路由器:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var usersRouter: Router?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let rootViewController = UIViewController()
window = UIWindow()
window?.rootViewController = rootViewController
let usersRouter = UsersRouter(parentViewController: rootViewController)
usersRouter.showInitial()
window?.makeKeyAndVisible()
self.usersRouter = usersRouter
return true
}
}
结论
我认为本文是4V Engine 1.0.0
版的介绍。 我更改了很多时间,并且我相信仍有改进的空间。 因此,我想对您的意见发表一些意见,我们将不胜感激。 谢谢。