MVVMCoordinatorKit
一个 Swift 工具包,帮助您使用 MVVM
模式结合 Coordinator
模式创建屏幕(UIViewController
),进行导航和组织,以可重用的一致流程。
MVVMCoordinatorKit.mov
描述
本工具包旨在加快您的开发速度,并帮助您利用 Coordinator
模式将屏幕组织成易于重用的一致流程,使屏幕间的导航变得简单且易于阅读。
本工具包还包括创建 UIViewController
(MVVM
模式中的 View
)及其 ViewModel
的功能。
Model
并不包含在本工具包中,因为开发人员需要在自己的应用程序中定义模型。
注意: 本README文件的目的不是描述 MVVM
模式的细节或从 MVC
到 MVVM
的演变。关于 MVVM
和它为何优于 MVC
的文章网上有很多。如果您正在阅读此README,您可能已经熟悉了 MVVM
,只是想找到一个帮助您进行 MVVM
开发的框架。
要求
- iOS 11.0+
- Swift 5.0+
安装
CocoaPods
MVVMCoordinatorKit 通过 CocoaPods 提供。要安装,只需将以下行添加到您的 Podfile
pod 'MVVMCoordinatorKit'
Swift 包管理器
Swift 包管理器 是一个用于自动化 Swift 代码分发的工具,并与 swift
编译器集成。
在 Xcode 的 包依赖 中搜索此包,并添加它
https://github.com/Dino4674/MVVMCoordinatorKit
示例
要运行示例项目,克隆仓库,然后打开 Example/MVVMCoordinatorKit.xcworkspace
。
命名约定
在传统的 MVVM
模式中
M
代表Model
V
代表View
VM
代表ViewModel
由于 Apple 强制我们通过其 API 使用 MVC
模式(是的,我们在谈论 UIViewController
),所以我们习惯于使用 ViewController 后缀来命名我们的自定义 ViewController
,这并不符合 MVVM
命名约定。我们希望将 UIViewController
视为“屏幕”。
UIView
代表 iOS 中一个视图,它是 UIViewController
视图层的一部分,我们将 UIViewController
视为“主要”视图 -> “屏幕”。
由于我们的“屏幕”是 UIViewController
,因此本工具包为 MVVM
的 View
部分使用了不同的命名约定
View
->Screen
ViewModel
->ScreenModel
我们可以将我们的 MVVM
称为 MSSM
(模型-屏幕-屏幕模型)。
这是为了区分 UIViewController
和 UIView
文件名,因为在您的应用中,您可能有很多自定义的 UIView
,您几乎肯定会在这些自定义视图中添加 View 后缀。另外,在创建 UIViewController
时,您可能还会用 ViewController 后缀命名,如前所述,这并不适合 MVVM
命名约定。本套件鼓励使用 Screen 后缀为 UIViewController
。
关注的主要类
Coordinator<DeepLinkType, ResultType>
封装特定流程的屏幕及其业务逻辑,并具有推送/present/setRoot 子协调器的功能。
Coordinator
有一个 ResultType
类型,用于在完成其流程后通知其父级 Coordinator
。
它还有一个 DeepLinkType
,您可以使用它来实现针对您的应用需求的特定深度链接。请注意,在一个 "协调器树" 中创建的每个 Coordinator
都必须具有完全相同的 DeepLinkType
具体实现(在 99.99% 的情况下,这将是一个名为 DeepLinkOption
的 enum
)。
Router
包含对 UINavigationController
的引用并处理导航逻辑(推送/弹出/present/dismiss/setRoot/popToRoot)。每个 Coordinator
都有一个对 Router
的引用(每个 Coordinator
只有一个)。
Screen<ScreenModel>
包含其 ScreenModel
的基 UIViewController
。每个 Screen
都使用其 ScreenModel
定义。
ScreenModel<Result>
一个与其所持的 Screen
配对的 ScreenModel
。每个 ScreenModel
定义其 Result
类型,该类型用于在生成值得导航更改的结果时通知负责的 Coordinator
。
绑定(在 Screen
和 ScreenModel
之间)
MVVMCoordinatorKit 设计为不依赖于任何特定的绑定实现。示例应用使用 Combine
在 Screen
和其 ScreenModel
之间进行绑定。您可以使用 Combine
或其他您喜欢的绑定实现。
模板
为了减少创建特定 Screen + ScreenModel
或 Coordinator
所需的时间,您可以下载 为本套件制作的自定义模板。将提取的根目录 MVVMCoordinatorKit
移动到以下两个文件夹之一
~/Library/Developer/Xcode/Templates
/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File Templates
注意:如果您将模板添加到第二个位置,它们将无法在 Xcode 更新后保留。
使用这些模板,您可以更快地创建文件,而无需每次都添加样板代码。有适用于 Screen + ScreenModel
和 Coordinator
的模板。
此外,当使用 Screen + ScreenModel
模板时,您可以选择 视图类型
- 代码
- 创建一个没有
.xib
文件的*Module Name*Screen.swift
(加上*Module Name*ScreenModel.swift
)。
- 创建一个没有
- 使用 XIB
- 创建一个包含配套
.xib
文件的*Module Name*Screen.swift
(加上*Module Name*ScreenModel.swift
)。
- 创建一个包含配套
可选地选择是否导入 Combine
框架,并使用示例代码作为您 Screen
和 ScreenModel
的起点。
在上面的屏幕截图示例中,模板将生成这些 3 个文件
用法
探索 MVVMCoordinatorKit 的最好方法是检查示例应用,其中包含所有示例。
Coordinator
+ Router
导航(推送/展示)
每个Coordinator
都有自己的Router
,您可以使用它进行所有push/pop/present/dismiss调用。然而,BaseCoordinator
类为push
、present
和setRoot
Coordinator
提供了方便的函数,它会在从视图栈中移除Screen
时自动为您处理资源释放。无论如何移除Screen
,所有情况都支持资源自动释放。
- 推入的
Coordinator
- 返回按钮从
UINavigationController
- 交互式的左屏幕边缘滑动弹出手势
- 手动调用
router.popModule
- 返回按钮从
- 呈现的
Coordinator
- 交互式的从上到下的滑动取消手势
- 手动调用
router.dismissModule
public func pushCoordinator(_ coordinator: BaseCoordinator, deepLink: DeepLinkType? = nil, animated: Bool = true, onPop: RouterCompletion? = nil)
public func presentCoordinator(_ coordinator: BaseCoordinator, deepLink: DeepLinkType? = nil, animated: Bool = true, onDismiss: RouterCompletion? = nil)
public func setRootCoordinator(_ coordinator: BaseCoordinator, deepLink: DeepLinkType? = nil, animated: Bool = true, onPop: RouterCompletion? = nil)
通常,如果我们想呈现一个流程,我们会创建一个新的具有新的UINavigationController
的Router
let navigationController = UINavigationController()
let router = Router(navigationController: navigationController)
let coordinator = ExampleCoordinator(router: router)
presentCoordinator(coordinator)
如果我们想推入一个流程,我们将使用当前Coordinator
的相同Router
let coordinator = ExampleCoordinator(router: router)
pushCoordinator(coordinator)
观察子Coordinator
的结果
当一个Coordinator
添加一个子Coordinator
(push、present,无所谓)时,它需要观察其子结果,它通过此回调来实现
public var finishFlow: ((ResultType) -> ())?
ResultType
在Coordinator
类中定义,它最可能是某个enum
。
Coordinator
模板会自动为您生成这个enum
,以便您可以填充使用场景
例如。
enum ProfileCoordinatorResult {
case didLogout
}
class ProfileCoordinator: Coordinator<DeepLinkOption, ProfileCoordinatorResult>
let coordinator = ProfileCoordinator(router: router)
coordinator.finishFlow = { [weak self] result in
switch result {
case .didLogout: // do something here...
// push or present another flow,
// or call self?.finishFlow to propagate the event up the tree and let the parent Coordinator decide what to do next
break
}
}
pushCoordinator(coordinator)
Screen
+ ScreenModel
每个Screen
都需要定义自己的ScreenModel
。
例如。
class ProfileScreen: Screen<ProfileScreenModel>
使用ScreenModel
实例化Screen
Screen
类有两个方便的函数用于实例化Screen
public static func createWithNib(screenModel: T) -> Self // Screen with .xib file
public static func create(screenModel: T) -> Self // in-code Screen
如果我们使用的是Code模板
let screenModel = ProfileScreenModel()
let screen = ProfileScreen.create(screenModel: screenModel)
如果我们使用的是With XIB模板
let screenModel = ProfileScreenModel()
let screen = ProfileScreen.createWithNib(screenModel: screenModel)
观察ScreenModel
的结果
ScreenModel
需要定义它的Result
类型(如果不需要,可以是),这是其父
Coordinator
观察结果所需的
ProfileScreenModel
Screen + ScreenModel
模板会自动为您生成这个enum
,以便您可以填充使用场景
例如。
extension ProfileScreenModel {
enum Result {
case didLogout
}
}
class ProfileScreenModel: ScreenModel<ProfileScreenModel.Result>
一个父Coordinator
,它设置ScreenModel
,会通过此回调接收ScreenModel
的结果
public var onResult: ((Result) -> Void)?
例如。
// We are in ProfileCoordinator here
let screenModel = ProfileScreenModel()
screenModel.onResult = { [weak self] result in
switch result {
case .didLogout: self?.finishFlow?(.didLogout)
}
}
ScreenModel
与Screen
的绑定
由于这个Kit不依赖于任何特定的绑定实现,您需要选择使用哪一个。 Screen + ScreenModel
模板会在ScreenModel
内部生成两个空的struct
,您可以使用它来指定Screen
的和output
。
输入
- 填充来自视图的所有可能的输入(例如
buttonTap
、swipeGestureActivated
,或任何其他...)
- 填充来自视图的所有可能的输入(例如
输出
- 填充视图的所有可能的输出(例如
loginButtonTitle
、screenTitle
、actionButtonEnabled
,或任何其他...)
- 填充视图的所有可能的输出(例如
例如(使用Combine
)
extension ProfileScreenModel: ScreenModelType {
struct Input {
let logout: PassthroughSubject<Void, Never>
}
struct Output {
let screenTitle: AnyPublisher<String?, Never>
let logoutButtonTitle: AnyPublisher<String?, Never>
}
}
日志记录
关于这个Kit中的日志记录的简要说明。有MVVMCoordinatorKitLogger
类用于调试这个Kit,以确保所有资源都被正确释放。默认情况下日志记录是禁用的。您可以调用此(例如从AppDelegate
)来启用它
MVVMCoordinatorKitLogger.loggingEnabled = true
您可能不需要这个功能,因为它只会污染您的日志。
致谢和来源
这个Kit是由一些其他探索并写过关于Coordiantor的人实现的。这个Kit是这些人的思想的综合。它并非完美,它总是会留有改进的空间。欢迎您提出建议、功能请求、pull requests,或只是打个招呼!
这一切是如何开始的
https://khanlou.com/2015/01/the-coordinator/
https://khanlou.com/2015/10/coordinators-redux/
当第一个问题出现(处理从视图栈中移除Screen,UINavigationController的返回按钮,和屏幕边缘的手势)时
https://khanlou.com/2017/05/back-buttons-and-coordinators/
当返回按钮问题得到解决(使用Router
)时
https://hackernoon.com/coordinators-routers-and-back-buttons-c58b021b32a
MVVMCoordinatorKit 还对交互式关闭展示的《UIViewController》(面板)和资源释放进行了升级,这是原始的《Router》方案所不支持的。此外,MVVMCoordinatorKit 还对《setRootModule》及其完成块进行了升级,这些用于资源的释放。
当有人将这些内容整合得很好时(这两篇文章加上上面的Hackernoon文章是MVVMCoordinatorKit最大的灵感来源)
https://medium.com/blacklane-engineering/coordinators-essential-tutorial-part-i-376c836e9ba7
https://medium.com/blacklane-engineering/coordinators-essential-tutorial-part-ii-b5ab3eb4a74
特别致谢(没有从这两篇文章中汲取太多想法,但如果你想要探索Coordinator模式,这些文章仍然值得一读)
https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps
https://www.hackingwithswift.com/articles/175/advanced-coordinator-pattern-tutorial-ios
作者
迪诺·巴托萨克(Dino Bartosak),[email protected]
许可
MVVMCoordinatorKit 基于 MIT 许可证提供。有关更多信息,请参阅 LICENSE 文件。