在这篇文章中,我将向你展示如何在真实的 iOS应用程序
示例中将 SwiftUI
与 VIPER架构
结合起来。
SwiftUI-初学者
网上确实有成百上千的 SwiftUI
教程,但是我只能找到一两个专门针对现实用例的东西,而不是像在 SwiftUI
中配置/制作X这样的较小细节。!
我对 SwiftUI
也有自己的 “挣扎” ,因为我的集合视图框架的结构与编写 SwiftUI
代码的方式完全相同。😂
无论如何,我从零开始就知道会有大量新的 SwiftUI
教程到来,并且每个人都会对新的声明性 UI框架
大肆宣传,但是老实说,我已经有了通用的工具包。这就是为什么我不想写它。坦率地说, 比起 SwiftUI
更喜欢 Combine
。
最终,因为到底是什么让我们尝试了新事物,并且我对 SwiftUI
如何适合我的应用程序构建方法感到好奇,所以我开始根据这些视图创建新的 VIPER模板
。我还想使用最新的新框架制作一个有用的,可扩展的,模块化的实际应用示例。 😛
了解现代VIPER架构
我在过去两年中一直使用 VIPER架构
。 有人说“这太复杂了”或“这不适合小型团队”。 我只能告诉他们一个字:扯!
我相信我已经创建了一种现代且相对简单的模式,几乎可以用于任何东西。得益于简洁的架构和 SOLID原理
,学习 VIPER
肯定会提高你的代码质量。你将更好地了解较小的部分如何协同工作并相互交流。
孤立的较小组件可以加快开发速度,因为你只需一次做一点工作,而且你可以为特定的事物创建测试,这对于可测试性和代码覆盖率来说是一个巨大的胜利(你不必运行你的应用程序始终都在运行,如果你想测试某些东西,则可以使用你只需要的模块)。
我通常使用一个非常简单的代码生成器来启动新模块,这样我可以节省很多时间。如果你必须独自处理项目,则模块生成器和预定义的结构甚至可以为你节省更多时间。而且,如果遵循基本 VIPER规则
,你真的不会弄乱搞乱项目结构。 ⏰
VIPER到底是什么?
你以前从未听说过 VIPER
,首先应该知道 VIPER模块
包含以下组件:
- View = UIViewController子类或SwiftUI视图
- Interactor = 以正确的格式提供所需的数据
- Presenter = 独立于UI的业务逻辑(具体操作)
- Entity = 数据对象(有时模块中不存在)
- Router = 建立视图控制器层次结构(显示,显示,关闭等)
在这些文件旁边,我总是有一个模块文件,其中我定义了一个模块构建器,该构建器从上面的组件中构建了整个组件,在该文件中,我还定义了特定于模块的协议。 我通常将这些协议称为接口,它们使使用依赖注入可以替换任何组件成为可能。 这样,我们可以在单元测试中使用模拟对象来测试任何东西。
**提示:
**
有人说带有 Builder
的 VIPER模块
称为 VIPER/B
。 我认为模块文件是存储模块构建器对象,模块接口和模块委托(如果需要的话)的理想场所。
面向协议的VIPER架构
因此,关键是连接 View-Interactor-Presenter-Router
的6个主要协议。 这些协议确保 VIPER组件
看不到超出要求的内容。 🐛
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| View-to-Presenter Presenter-to-View
Router-to-Presenter Presenter-to-Router
Interactor-to-Presenter Presenter-to-Interactor
Module # --- builds up pointers and returns a UIViewController
View implements View-to-Presenter # --- strong presenter as Presenter-to-View-interface
Presenter implements Presenter-to-Router, Presenter-to-Interactor, Presenter-to-View # --- strong router as Router-to-Presenter-interface strong interactor as Interactor-to-Presenter-interface weak view as View-to-Presenter-interface
Interactor implements Interactor-to-Presenter # --- weak presenter as Presenter-to-Interactor-interface
Router implemenents Presenter-to-Router # --- weak presenter as Presenter-to-Router-interface
|
如你所见,视图(可以是 UIViewController
子类)牢固地保持了 presenter
,并且 presenter
将保留 interactor
和 router
类。 其他所有东西都是弱指针,因为我们不喜欢持有。 乍一看似乎有些复杂,但是在编写了最初的几个模块之后,你将发现将逻辑组件彼此分离是多么的好。 🐍
请注意,并非所有内容都是 VIPER模块
。 不要尝试将你的 API通信层
或 CoreLocation服务
编写为模块,因为这类东西是独立的,例如:服务。 我将在下一篇文章中介绍它们,但现在,我们仅关注 VIPER模块
的剖析。
Swift 5中的VIPER实现
你准备好编写一些 Swift
代码了吗? 好吧,让我们创建一些通用的 VIPER接口
,以后可以扩展它们,不要担心不会那么难。 😉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
|
public protocol RouterPresenterInterface: class {
}
public protocol InteractorPresenterInterface: class {
}
public protocol PresenterRouterInterface: class {
}
public protocol PresenterInteractorInterface: class {
}
public protocol PresenterViewInterface: class {
}
public protocol ViewPresenterInterface: class {
}
public protocol RouterInterface: RouterPresenterInterface { associatedtype PresenterRouter
var presenter: PresenterRouter! { get set } }
public protocol InteractorInterface: InteractorPresenterInterface { associatedtype PresenterInteractor
var presenter: PresenterInteractor! { get set } }
public protocol PresenterInterface: PresenterRouterInterface & PresenterInteractorInterface & PresenterViewInterface { associatedtype RouterPresenter associatedtype InteractorPresenter associatedtype ViewPresenter
var router: RouterPresenter! { get set } var interactor: InteractorPresenter! { get set } var view: ViewPresenter! { get set } }
public protocol ViewInterface: ViewPresenterInterface { associatedtype PresenterView
var presenter: PresenterView! { get set } }
public protocol EntityInterface {
}
public protocol ModuleInterface {
associatedtype View where View: ViewInterface associatedtype Presenter where Presenter: PresenterInterface associatedtype Router where Router: RouterInterface associatedtype Interactor where Interactor: InteractorInterface
func assemble(view: View, presenter: Presenter, router: Router, interactor: Interactor) }
public extension ModuleInterface {
func assemble(view: View, presenter: Presenter, router: Router, interactor: Interactor) { view.presenter = (presenter as! Self.View.PresenterView)
presenter.view = (view as! Self.Presenter.ViewPresenter) presenter.interactor = (interactor as! Self.Presenter.InteractorPresenter) presenter.router = (router as! Self.Presenter.RouterPresenter)
interactor.presenter = (presenter as! Self.Interactor.PresenterInteractor)
router.presenter = (presenter as! Self.Router.PresenterRouter) } }
|
关联类型只是特定类型的占位符,通过使用通用接口设计,我可以使用通用模块接口扩展来组装模块,如果缺少某些协议,则应用程序将崩溃,就像我尝试初始化不良模块一样。
我喜欢这种方法,因为它使我省去了许多样板模块构建器代码。 同样,所有内容都将具有基本协议,因此我可以以一种真正整洁的面向协议的方式扩展任何内容。 无论如何,如果你不了解泛型没什么大不了的,那么在实际的模块实现中,你将几乎无法满足它们。
那么实际模块的代码如何?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
|
protocol TodoRouterPresenterInterface: RouterPresenterInterface {
}
protocol TodoPresenterRouterInterface: PresenterRouterInterface {
}
protocol TodoPresenterInteractorInterface: PresenterInteractorInterface {
}
protocol TodoPresenterViewInterface: PresenterViewInterface {
}
protocol TodoInteractorPresenterInterface: InteractorPresenterInterface {
}
protocol TodoViewPresenterInterface: ViewPresenterInterface {
}
final class TodoModule: ModuleInterface {
typealias View = TodoView typealias Presenter = TodoPresenter typealias Router = TodoRouter typealias Interactor = TodoInteractor
func build() -> UIViewController { let view = View() let interactor = Interactor() let presenter = Presenter() let router = Router()
self.assemble(view: view, presenter: presenter, router: router, interactor: interactor)
router.viewController = view
return view } }
final class TodoPresenter: PresenterInterface { var router: TodoRouterPresenterInterface! var interactor: TodoInteractorPresenterInterface! weak var view: TodoViewPresenterInterface! }
extension TodoPresenter: TodoPresenterRouterInterface {
}
extension TodoPresenter: TodoPresenterInteractorInterface {
}
extension TodoPresenter: TodoPresenterViewInterface {
}
final class TodoInteractor: InteractorInterface { weak var presenter: TodoPresenterInteractorInterface! }
extension TodoInteractor: TodoInteractorPresenterInterface {
}
final class TodoRouter: RouterInterface { weak var presenter: TodoPresenterRouterInterface! weak var viewController: UIViewController? }
extension TodoRouter: TodoRouterPresenterInterface {
}
final class TodoView: UIViewController, ViewInterface { var presenter: TodoPresenterViewInterface! }
extension TodoView: TodoViewPresenterInterface {
}
|
VIPER模块
由五个文件组成,与我的旧方法相比,这是一个巨大的改进(我为单个模块使用了9个文件,这仍然比2000行代码的大规模视图控制器要好,但是是的,它的文件很多 …😂)。
如果需要,可以使用 VIPER协议库
,也可以将这些接口复制并粘贴到你的项目中。 我还有一个完全用 Swift
编写的 VIPER模块生成器
,它可以基于此模板生成模块(或者你可以自己创建)。
如何创建VIPER接口?
一起看一个示例流程,请考虑以下示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| protocol TodoRouterPresenterInterface: RouterPresenterInterface { func dismiss() }
protocol TodoPresenterRouterInterface: PresenterRouterInterface {
}
protocol TodoPresenterInteractorInterface: PresenterInteractorInterface { func didLoadWelcomeText(_ text: String) }
protocol TodoPresenterViewInterface: PresenterViewInterface { func ready() func close() }
protocol TodoInteractorPresenterInterface: InteractorPresenterInterface { func startLoadingWelcomeText() }
protocol TodoViewPresenterInterface: ViewPresenterInterface { func setLoadingIndicator(visible: Bool) func setWelcomeText(_ text: String) }
|
视图在某个时间点在 presenter
上调用 ready()
,因此 presenter
可以开始。首先,它通过调用 setLoadingIndicator(visible:true)
告诉视图显示加载指示符,然后要求 interactor
异步加载start欢迎文本(startLoadingWelcomeText())
。数据返回到 interactor
之后,它可以使用 didLoadWelcomeText(“”)
方法通知 presenter
。现在, presenter
可以使用相同的方法 setLoadingIndicator(visible:false)
和 false
参数告诉视图隐藏加载指示器,并使用 setWelcomeText(“”)
显示欢迎文本。
另一个用例是有人点击视图上的按钮以关闭控制器。该视图在 presenter
上调用close(), presenter
只需在 router
上调用 dismiss()
。在要求 router
关闭视图控制器之前,演示者还可以做其他事情(例如清理一些资源)。
我希望你能得到例子,自己动手做所有的事情,这是一个很好的练习任务。当然,你可以利用区块,承诺或全新的 Combine框架
使你的生活更轻松。例如,你可以在某些异步数据加载完成后自动通知演示者。 😉
因此,既然你对现代 VIPER架构
有了基本的了解,就可以讨论如何用 SwiftUI
替换传统的 ViewController
子类。
如何设计基于VIPER的SwiftUI应用程序?
SwiftUI
是相当独特的。 视图是结构,因此我们的通用 VIPER协议
需要进行一些更改才能使所有功能正常工作。
你要做的第一件事是摆脱 ViewPresenterInterface
协议。 接下来,你可以从 PresenterInterface
中删除 view属性
,因为我们将使用可观察的 view-model模式
来自动更新数据视图。 最后的修改是你必须从 ModuleInterface
扩展内的 assemble函数
的默认实现中删除 view
参数。
所以我提到了一个视图模型,让我们做一个。 为了简单起见,我将使用 Bool
来指示是否出了问题,但是你可以使用其他视图,也可以使用独立的 VIPER模块
来显示 提示消息
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import Combine import SwiftUI
final class TodoViewModel: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
@Published var error: Bool = false { willSet { self.objectWillChange.send() } }
@Published var todos: [TodoEntity] = [] { willSet { self.objectWillChange.send() } } }
|
此类符合 ObservableObject
,它使 SwiftUI
可以检查更新并在发生某些更改时重新呈现视图层次结构。 你只需要具有 ObservableObjectPublisher
类型的属性,并从字面上发送一条消息(如果发生某些更改会触发此消息,从而触发视图中的自动更新)。 🔥
TodoEntity
只是一个基本结构,它遵循一堆协议,例如 SwiftUI
的新 Identifiable
,因为我们希望在列表中显示实体。
1 2 3 4 5 6 7 8
| import Foundation import SwiftUI
struct TodoEntity: EntityInterface, Codable, Identifiable { let id: Int let title: String let completed: Bool }
|
基本的 SwiftUI
视图仍将实现 ViewInterface
,并且将具有对 presenter
的引用。 我们的 view-model属性
还将在这里使用 @ObservedObject
属性包装器进行标记。 到目前为止的代码是这样的:
1 2 3 4 5 6 7 8 9 10 11 12
| import SwiftUI
struct TodoView: ViewInterface, View {
var presenter: TodoPresenterViewInterface!
@ObservedObject var viewModel: TodoViewModel
var body: some View { Text("SwiftUI ❤️ VIPER") } }
|
presenter
还将拥有一个弱的 var viewModel:TodoViewModel!
参考以能够更新视图模型。 好像我们通过使用视图模型在视图和 presenter
之间存在双向通信流。 在我看来很好。 👍
如果我们想在视图层次结构中传递一些数据,我们还可以使用全新的 @EnvironmentObject
。 你只需在环境对象中实现与对视图模型相同的观察协议即可。 例如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import Foundation import Combine
final class TodoEnvironment: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
@Published var title: String = "Todo list" { willSet { self.objectWillChange.send() } } }
|
最后,让我向你展示如何实现模块构建器,因为这非常棘手。 你必须使用新的通用 UIHostingController
,这是一个 UIViewController
子类,因此可以在完成模块构建后将其返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| final class TodoModule: ModuleInterface { typealias View = TodoView typealias Presenter = TodoPresenter typealias Router = TodoRouter typealias Interactor = TodoInteractor
func build() -> UIViewController { let presenter = Presenter() let interactor = Interactor() let router = Router()
let viewModel = TodoViewModel() let view = View(presenter: presenter, viewModel: viewModel) .environmentObject(TodoEnvironment()) presenter.viewModel = viewModel
self.assemble(presenter: presenter, router: router, interactor: interactor)
let viewController = UIHostingController(rootView: view) router.viewController = viewController return viewController } }
|
从现在开始放在一起只是小菜一碟。 如果需要,你可以挑战自己构建东西,而无需下载 最终项目
。 🍰