CLE-架构工具

文件模板可用于创建新的 Rx 场景(使用这些模板中的一个,而不是直接创建 View Controller)。将文件夹放置在 Library/Developer/Xcode/Templates/ 中。

Utilities 文件夹包含应包含在项目中的支持代码(如果您使用 SPM 或 Cocoapods,将自动包含)。从架构角度来看,最重要的文件是 "Stage.swift" 和 "Scene.swift"。其他文件包含我在 80% 或更多的项目中使用的代码。

Tools 文件夹包含我开发的一些助手工具,但大多数项目都不需要。如果您想使用其中任何一个,您必须手动将它们拖到您的项目中。

要求


CLE 的一个基本理念是“命令式外壳与函数式内核”的思想(最初由 Gary Bernhardt 解释),并在 Swift 中由 Matt Diephouse 扩展。这意味着,除其他事项外,副作用(命令式部分)位于系统的外部边缘,不会注入到逻辑中。这与我所知道的任何其他架构风格都有很大的不同。这种架构风格的重点是您在测试中永远不需要模拟或存根任何代码。

请注意,我在上面没有提到 Fakes。只有 Mocks 和 Stubs。这是因为即使 CLE 也要求您使用 TestObservable,它是一个 Fake。如果您仔细使用 Store 类中使用的环境类型,并确保在测试时只需要通过它传入 Fakes,您可以在使用它的同时保持这种理念,但它不像我的架构的其余部分那样严格地执行它。

我很乐意为那些感兴趣的人深入探讨。如果您有任何问题或想讨论,您可以在社交媒体上联系我,或者在 github 上提出 issue。


我最近在帮助别人,并用这种方式表达了 CLE 架构。也许它会对其他人有所帮助...

每当您需要执行副作用并且需要结果时。模板如下所示:

let resultOfEffect = trigger
    .withLatestFrom(additionalDataNeeded) // if any
    .map { logic($0) } // handle any logic to convert the input data into something the effect needs
    .flatMap { performSideEffect($0) }
    .share(replay: 1) // if you are using the result in multiple places.

如果您不需要结果,那么它是:

trigger
    .withLatestFrom(additionalDataNeeded) // if any
    .map { logic($0) } // handle any logic to convert the input data into something the effect needs
    .subscribe(onNext: { performEffect($0) })
    .disposed(by: disposeBag)

_ = trigger
    .withLatestFrom(additionalDataNeeded) // if any
    .take(until: event) // often `rx.deallocating`
    .subscribe(onNext: { performEffect($0) })

有时触发器很复杂。有时 additionalDataNeeded 很大。无论哪种方式,上面的两个模板都将满足大多数需求。


2.0 版本已经推送。我不得不提高版本号,因为取决于人们构建场景的方式,这可能是一个重大更改。对于库的大多数应用程序,您根本不必更新您的代码。

什么是不改变什么的大变化?

在 1.x 版本的库中,场景创建方法创建了 view controller,然后加载了 view controller 的 view,然后调用了 connect closure。必须加载 view 以确保构建所有 VC 的 subviews,以便您可以连接到按钮、文本字段等。但是,这意味着 view 在添加到导航或呈现堆栈之前已完全加载,因此您无法从 connect 函数内部判断 controller 是如何呈现的。对于 2.x 版本,我使 connect closure 逃逸,现在库在 UIKit 加载 view 之后才调用它。这在 controller 附加到导航或呈现堆栈之后完成。这意味着对于 2.x,您将可以访问所有 UIViewController 属性,即使是那些直到 controller 附加到 view controller 层次结构之后才分配的属性。


安装

三种安装方式

  1. 将 Utilities 文件夹拖放到您 xcode 工作区中的项目名称文件夹下

  2. 使用 Swift Package Manager。

  3. 使用 CocoaPods

use_frameworks!

target 'YOUR_TARGET_NAME' do
   pod 'Cause-Logic-Effect'
end

替换 YOUR_TARGET_NAME,然后在 Podfile 目录中输入

$ pod install

使用

该库背后的想法是提供一种简单的方法将 view controller 包装为 Observable 资源,以便将其变成一个简单的异步事件。本质上,您的代码将能够像处理服务器或数据库一样处理它的 view controller,通过 flatMap 和 bind。

Scene (场景)

核心类型是 Scene,它由 view controller 以及一个 Observable 组成,该 Observable 发出任何所需的用户提供的值,并在完成时发出停止事件。或者,调用代码可以 dispose() Scene 的 Observable,如果它想在完成之前关闭 view controller。

可以通过调用 scene 方法从 view controller 创建一个 Scene。有一个实例方法可以从已经存在的 view controller 构建一个 Scene,或者您可以使用 scene 静态方法,该方法将从 storyboard 加载 view controller 并使用它创建一个 Scene。

显示场景

创建 Scene 后,可以通过多种方式显示它。您可以 present 它,push 它到 navigation controller 上,或者 show 它。所有您可以处理普通 view controller 的方式。无论您选择以何种方式显示您的 Scene,您都可以确信,当它被 disposed 时,它将使用显示方法的正确逆方法隐藏自己。(所以如果 present 了它会 dismiss,如果 push 了它会 pop,等等。)用于显示 Scene 的函数在 "Stage.swift" 文件中。

一个场景示例

实际上每个程序都会在某个时候使用 Alert controller。该库在 UIAlertController 上提供了一些方法,用于 view controller 的一些最常见操作。它们是 connectOK(),它返回一个 Observable,当用户点击 OK 按钮时发出 Void,以及一个 connectChoice,它将为提供的每个选择添加一个按钮,以及一个取消按钮,并返回一个 Observable,它发出用户所做的选择(如果用户取消则为 nil)。

处理一个流程

一个 Scene 也可以代表许多子 Scenes,它们一起作为一个“流程”或“过程”工作。下面是一个这样的构造的例子。一个流程是一个返回 Scene 的函数,并且该 Scene 实际上封装了许多子 Scenes,它们一起工作以完成一项工作。如果任何一个子场景完成,则整个流程完成。请注意,与典型的 Coordinator 类型不同,您不需要管理任何资源。在任何时候您都不需要手动跟踪当前正在显示哪些 view controller。

使用示例

当用户点击我的一个应用程序中的“忘记密码”链接时

forgotPasswordButton.rx.tap
    .bind(onNext: presentScene(animated: true, scene: forgotPasswordFlow))
    .disposed(by: disposeBag)

忘记密码流程包括按顺序呈现三个屏幕

func forgotPasswordFlow() -> Scene<Void> {
    // This scene asks the user to enter their phone number.
    let forgotPassword = ForgotPasswordViewController.scene { $0.phoneNumber() }

    // Once we get the phone number, send a one-time password to the user and ask them to enter it.
    let otpResult = forgotPassword.action
        .observe(on: MainScheduler.instance)
        .flatMapFirst(presentScene(animated: true) { phoneNumber in
            OTPViewController.scene { $0.passwordResetToken(forPhoneNumber: phoneNumber) }
        })

    // After they enter the OTP, allow them to reset their password.
    let resetPasswordResult = otpResult
        .observe(on: MainScheduler.instance)
        .flatMapFirst(presentScene(animated: true) { token in
            ResetPasswordViewController.scene { $0.resetPassword(withToken: token) }
        })

    // When `resetPasswordResult` completes, the entire flow will automatically unwind.
    return Scene(controller: forgotPassword.controller, action: resetPasswordResult)
}

命令式使用

如果您正在从命令式代码转换为使用此库,以下函数将会派上用场

func call<T>(_ fn: (()) -> Observable<T>) -> Observable<T> {
	fn(())
}

func final(_ fn: (()) -> Void) {
	fn(())
}

call 函数用于您的 view controller 将信息返回给其调用者的情况,否则使用 final 函数。您可以将这些函数包装在 Stage.swift 文件中的函数周围。所以举个例子

final(presentScene(animated: true) {
    UIAlertController(title: "Greeting", message: "Hello World", preferredStyle: .alert)
        .scene { $0.connectOK() }
})

_ = call(presentScene(animated: true, over: button) {
    UIAlertController(title: nil, message: "Which One?", preferredStyle: .actionSheet)
        .scene { $0.connectChoice(choices: ["This One", "That One"]) }
})
.subscribe(onNext: {
    print("A choice was made:", $0 as Any)
})

其他类型

库中的其他类型/方法/函数是我在至少 80% 的应用程序中使用到的辅助功能。

ActivityTrackerErrorRouter 类型用于跟踪网络请求。当您有一个 Observable 反馈自身时,可以使用 cycle 函数。Identifier 类型用于为 Identifiable 类型创建 id。RxHelpers 包含一些 misc 方法,使映射和 Observer success 更容易处理。"UIColor+Extensions" 文件包含一个便利的 init 函数,用于从十六进制值创建颜色。"UIViewController+Rx.swift" 文件包装了一些用于在内部处理 view controller 的基本函数。它包括 dismissSelfpopSelf,在您想在 view controller 完成之前将其删除的那些罕见情况下。