MVVMCoordinatorKit 0.1.0

MVVMCoordinatorKit 0.1.0

Dino Bartošak 维护。



  • 作者:
  • Dino Bartošak

MVVMCoordinatorKit

一个 Swift 工具包,帮助您使用 MVVM 模式结合 Coordinator 模式创建屏幕(UIViewController),进行导航和组织,以可重用的一致流程。

MVVMCoordinatorKit.mov

Version Swift Package Manager

描述

本工具包旨在加快您的开发速度,并帮助您利用 Coordinator 模式将屏幕组织成易于重用的一致流程,使屏幕间的导航变得简单且易于阅读。

本工具包还包括创建 UIViewControllerMVVM 模式中的 View)及其 ViewModel 的功能。

Model 并不包含在本工具包中,因为开发人员需要在自己的应用程序中定义模型。

注意本README文件的目的不是描述 MVVM 模式的细节或从 MVCMVVM 的演变。关于 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

Screenshot 2023-09-25 at 13 07 25

示例

要运行示例项目,克隆仓库,然后打开 Example/MVVMCoordinatorKit.xcworkspace

命名约定

在传统的 MVVM 模式中

  • M 代表 Model
  • V 代表 View
  • VM 代表 ViewModel

由于 Apple 强制我们通过其 API 使用 MVC 模式(是的,我们在谈论 UIViewController),所以我们习惯于使用 ViewController 后缀来命名我们的自定义 ViewController,这并不符合 MVVM 命名约定。我们希望将 UIViewController 视为“屏幕”。

UIView 代表 iOS 中一个视图,它是 UIViewController 视图层的一部分,我们将 UIViewController 视为“主要”视图 -> “屏幕”。

由于我们的“屏幕”是 UIViewController,因此本工具包为 MVVMView 部分使用了不同的命名约定

  • View -> Screen
  • ViewModel -> ScreenModel

我们可以将我们的 MVVM 称为 MSSM(模型-屏幕-屏幕模型)。

这是为了区分 UIViewControllerUIView 文件名,因为在您的应用中,您可能有很多自定义的 UIView,您几乎肯定会在这些自定义视图中添加 View 后缀。另外,在创建 UIViewController 时,您可能还会用 ViewController 后缀命名,如前所述,这并不适合 MVVM 命名约定。本套件鼓励使用 Screen 后缀为 UIViewController

关注的主要类

Coordinator<DeepLinkType, ResultType>

封装特定流程的屏幕及其业务逻辑,并具有推送/present/setRoot 子协调器的功能。

Coordinator 有一个 ResultType 类型,用于在完成其流程后通知其父级 Coordinator

它还有一个 DeepLinkType,您可以使用它来实现针对您的应用需求的特定深度链接。请注意,在一个 "协调器树" 中创建的每个 Coordinator 都必须具有完全相同的 DeepLinkType 具体实现(在 99.99% 的情况下,这将是一个名为 DeepLinkOptionenum)。

Router

包含对 UINavigationController 的引用并处理导航逻辑(推送/弹出/present/dismiss/setRoot/popToRoot)。每个 Coordinator 都有一个对 Router 的引用(每个 Coordinator 只有一个)。

Screen<ScreenModel>

包含其 ScreenModel 的基 UIViewController。每个 Screen 都使用其 ScreenModel 定义。

ScreenModel<Result>

一个与其所持的 Screen 配对的 ScreenModel。每个 ScreenModel 定义其 Result 类型,该类型用于在生成值得导航更改的结果时通知负责的 Coordinator

绑定(在 ScreenScreenModel 之间)

MVVMCoordinatorKit 设计为不依赖于任何特定的绑定实现。示例应用使用 CombineScreen 和其 ScreenModel 之间进行绑定。您可以使用 Combine 或其他您喜欢的绑定实现。

模板

为了减少创建特定 Screen + ScreenModelCoordinator 所需的时间,您可以下载 为本套件制作的自定义模板。将提取的根目录 MVVMCoordinatorKit 移动到以下两个文件夹之一

~/Library/Developer/Xcode/Templates
/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File Templates

注意:如果您将模板添加到第二个位置,它们将无法在 Xcode 更新后保留。

269855421-998eb21b-d248-4f69-b031-78d37d26ac00

使用这些模板,您可以更快地创建文件,而无需每次都添加样板代码。有适用于 Screen + ScreenModelCoordinator 的模板。

此外,当使用 Screen + ScreenModel 模板时,您可以选择 视图类型

  • 代码
    • 创建一个没有 .xib 文件的 *Module Name*Screen.swift(加上 *Module Name*ScreenModel.swift)。
  • 使用 XIB
    • 创建一个包含配套 .xib 文件的 *Module Name*Screen.swift(加上 *Module Name*ScreenModel.swift)。

268934845-4a9b65c2-7a4c-41e3-9cf2-561ab410089f

可选地选择是否导入 Combine 框架,并使用示例代码作为您 ScreenScreenModel 的起点。

268934859-aea9591f-5d2e-4fd1-81c8-1e9a572bb11f

在上面的屏幕截图示例中,模板将生成这些 3 个文件

268934861-da04e732-39eb-4e09-9050-4b35633c7fa5

用法

探索 MVVMCoordinatorKit 的最好方法是检查示例应用,其中包含所有示例。

Coordinator + Router

导航(推送/展示)

每个Coordinator都有自己的Router,您可以使用它进行所有push/pop/present/dismiss调用。然而,BaseCoordinator类为pushpresentsetRootCoordinator提供了方便的函数,它会在从视图栈中移除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)

通常,如果我们想呈现一个流程,我们会创建一个新的具有新的UINavigationControllerRouter

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) -> ())?

ResultTypeCoordinator类中定义,它最可能是某个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)
  }
}

ScreenModelScreen的绑定

由于这个Kit不依赖于任何特定的绑定实现,您需要选择使用哪一个。 Screen + ScreenModel模板会在ScreenModel内部生成两个空的struct,您可以使用它来指定Screenoutput

  • 输入
    • 填充来自视图的所有可能的输入(例如buttonTapswipeGestureActivated,或任何其他...)
  • 输出
    • 填充视图的所有可能的输出(例如loginButtonTitlescreenTitleactionButtonEnabled,或任何其他...)

例如(使用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 文件。