SwiftyFlow 0.7.1

SwiftyFlow 0.7.1

Felipe F Garcia 维护。



SwiftyFlow

Build Status codecov codebeat badge Version Carthage compatible License Platform Documentation

SwiftyFlow

它是第一个允许您将导航控制为“流”的库,您可以使用 .NIB 或 Storyboard,您可以声明您想要的预期流并简单地创建它。

主要功能
🙅 使用 goNextgetBack 进行导航
🏠 清晰的架构和简单实现
🔠 在导航时传递和接收参数,所有类型化!
🔑 解耦并方便您的导航
完全单元测试
📱 只需一个容器 ContainerFlowStackFlowManager 即可导航
🚀 最后,您可以使用单元测试从头到尾测试您的导航流
💯 100% Swift 代码

除了所有可能性之外,最大的优势是测试您的流的可能。您可以单元测试您的流,确保您没有破坏任何流/导航,您将能够继续升级和更改您的导航,同时保持清晰的控制,以确保您没有破坏任何内容。

阅读更多...使用 Swifty Flow,您可以将参数传递到您要调用的下一个视图控制器,并且可以注入这些值来实例化此下一个视图控制器,除此之外,您传递的所有值都是类型化的,这确保了您可以控制任何更改,并且可以在测试中进行测试。

导航可以有两种方式:如果您不希望传递任何参数,可以仅声明您的流导航,并且您声明的顺序将发生,只需调用 goNext()getBack() 就这么简单。

或者,您可以说在任何时候您想去或进行导航的方式,为此,只需说出下一个视图控制器的类型,仅使用您声明的类的类型,库将为您解决所有问题。

您想一键关闭整个流吗?您想要在流关闭时收到回调吗?您想知道流何时加载完成吗?当您完成流时传递任何值回去?!?!所有这些都可以使用这个库,无需关心委托、通知或其他任何模式来完成此简单任务 :D :D .

概念

您应用内的所有导航都属于一个流程。今天,我们在iOS中使用原生框架处理这个流程的方式是使用导航控制器。问题是,我们无法测试。如果您运行单元测试,可能视图不会显示(因为它速度快),您调用下一个或上一个视图的方法也不会被触发。

我制作了两个图表来展示仅使用原生方式和使用SwiftyFlow框架之间的主要区别,请看以下图表,您将在项目中的示例中看到SwiftyFlow在图表中的应用。

UINavigationController图表,当前问题

显示

SwiftyFlow图表方法

显示

实现SwiftyFlow - 基础

  • 有一个容器,您在其中声明了视图控制器:ContainerFlowStack();
  • 有一个FlowManager实例,该实例将负责您的导航栈;
  • 您的类符合FlowNavigator迁徙协议;

关于FlowNavigator协议:这是在您类中如何保持对您流程管理器的引用,这样当您导航时,您就不需要自己设置什么流程迁移器了,每次我们解决您的依赖时,我们都会自动设置引用。

如果不符合也可以继续工作吗?可能的,但您需要手动设置,当然,这取决于您的选择。

让我们尝试构建一个简单的流程。

实现SwiftyFlow

创建一个类,它将成为您的“容器”,它是拥有我们将要导航的所有视图控制器声明的那个;

import Foundation
import SwiftyFlow

class ContainerView {

    func setupStackNavigation(using containerStack: ContainerFlowStack) {

        containerStack.registerModule(for: ViewController.self) { () -> ViewController in
            return ViewController()
        }

        containerStack.registerModule(for: FirstViewController.self) { () -> FirstViewController in
            return FirstViewController()
        }

        containerStack.registerModule(for: SecondViewController.self) { () -> SecondViewController in
            return SecondViewController()
        }

        containerStack.registerModule(for: ThirdViewController.self) { () -> ThirdViewController in
            return ThirdViewController()
        }
    }
}

这个容器是什么?为什么需要注册?

这个容器是我们声明所有要在这个用于导航的流程管理器中拥有的类型的地方。

这里还有一个重要的部分,我们只是声明类型,目前只是注册我们后面将要使用的类,我们在声明时并不会实例化这些类,只是在开始导航并请求框架将要询问我们是否声明了这个类型的实例时才会进行实例化,如果已经有了,将会到你的容器中查找注册并解析实例,然后返回以展示。

在你想开始导航流程的类中,需要创建你的流程管理器。

想象一下,我们有一个名为AppInitialViewController的类,在这个类中,我将创建一个方法来创建我们的堆栈并设置给我们的FlowManager以便使用其容器。

func createOurNavigation() {
	let navigationStack = ContainerFlowStack()
    ContainerView().setupStackNavigation(using: navigationStack)

    FlowManager(root: FirstViewController.self, container: navigationStack)
}

有一个重要信息要知道,我们的流程管理器是如何知道我们使用Storyboard文件或NIB文件的?

当我们创建FlowManager时,默认会创建预期我们的导航将使用NIB文件作为视图控制器,如果你想使用Storyboard,你需要这样指定

FlowManager(root: AutomaticallyInitialViewController.self,
			container: navigationStack,
			setupInstance: .storyboard("AutomaticallyNavigationFlow"))
  • 我们说我们想要这个流程使用Storyboard,并指定Storyboard的名称。

让我们深入了解并理解“为什么”。

如果你注意到,实际上你不需要有一个名为ContainerView的类,我只是添加了以避免在一个地方放所有的声明,我们需要的只是一个ContainerFlowStack,在那里声明所有依赖,如果你想的话,你可以创建“快捷格式”

let navigationStack = ContainerFlowStack { container in
            container.registerModule(for: SecondViewController.self, resolve: { () -> SecondViewController in
                return SecondViewController()
            })
        }

FlowManager(root: FirstViewController.self, container: navigationStack)

这与之前一样,但问题是想象一下,在这个闭包中声明所有这些类的内部,会非常繁琐,但实际上是可能的;)。

关于配置本身,现在你能够开始设置了。

要开始,只需从你的流程管理器调用start()方法,你可以设置一个类变量并在之后调用,或者直接从流程管理器中调用,例如

  1. 直接调用实例
FlowManager(root: FirstViewController.self, container: navigationStack).start()

这将只是开始你的导航流程。

  1. 稍后开始,只需将流程管理器设置到某个变量中,稍后调用
flowManagerVariable = FlowManager(root: FirstViewController.self, container: navigationStack)


flowManagerVariable.start()

关于开始/创建,这就是所有的内容,关于导航,你基本上只需要使用3个方法。

  1. 要使用“自动导航”转到下一个屏幕,只需使用goNext()方法转到下一个屏幕,将会遵循你在容器中声明的顺序;

  2. 后退也是同样的,只需调用getBack()就会返回到上一个屏幕;

  3. 关闭流程,任何时间都可以调用dismissFlowController()

这就全部了,用这个设置,你可以轻松导航。

当然,这甚至不是最重要的,在示例中,你将看到许多其他方法,可以提供更多的功能,比如传递参数,转到容器内部声明的任何其他屏幕。

实际上最重要的一点是,你可以测试,可以准确地以你声明的方式测试这个流程,因此你可以通过单元测试确保你的流程仍然有效和正常工作!

示例

示例项目包含了许多你可能在日常中可能会使用的情况,这个项目的创建主要是出于这个原因。在这里,我将逐一命名并描述每一个,只需运行示例并查看代码即可轻松了解。

要运行示例项目,首先克隆仓库,然后从示例目录中运行pod install

使用 Storyboard 自动导航并传递参数

首先,“自动导航”意味着什么?这意味着你声明的视图控制器的顺序,当你调用 goNext()getBack() 时,你不需要指定目的地。

本示例中的功能:使用 Storyboard 中的视图控制器自动导航。使用框架从视图控制器传递参数到另一个视图控制器。

  1. 导航将使用自动方式,使用两种方法:

  2. goNext()

  3. getBack()

  4. 当使用 Storyboard 传递参数时,采用略微不同的方法,让我们理解这个问题:当使用 Storyboard 实例化时,实际上发生了什么,这是 Storyboard 实例化了你的实例,即你的视图控制器类。由于这个原因(直到 2019 年 WWDC 的最后一个苹果更新),在初始化器中无法传递任何参数,唯一的解决方法是,在你获得实例后,使用方法注入、变量注入。

但远未结束,为了能够测试,我们在调用下一个时发送参数,在你的实例中“监听”参数,让我们看看。

首先,你需要使用不同的方法来调用下一个视图,你将使用

navigationFlow?.goNextWith(screen: AutomaticallyFirstViewController.self, parameters: { ("Felipe", 3123.232, "Florencio", 31) })
  • 我们需要指定我们想去的地方,即哪个屏幕;
  • 参数是有类型的,这意味着在另一边,你需要以相同的方式指定类型,这是测试的一个重要部分,确保你所发送的内容以相同的方式接收。

参数:我们使用元组,就像在泛型中一样,我们需要有类型,所以你可以只是发送一个 Swift 能够理解类型的元组,然后在内部声明其他值,可以是单一的项目,自定义对象,任何东西。

在你的调用类中,在我们的场景中是 AutomaticallyFirstViewController.self,你需要实现将接收的方法。

我创建了一个在 func viewWillAppear(_ animated: Bool) 方法上被调用的方法,该方法将监听,监听的方法将是这样的

func requestData() {
        navigationFlow?.dataFromPreviousController(data: { (arguments: (String, Double, String, Int)) in
            let (first, second, third, fourth) = arguments
            debugPrint("First parameter: \(first) - Storyboard Automatically Navigation")
            debugPrint("Second parameter: \(second) - Storyboard Automatically Navigation")
            debugPrint("Third parameter: \(third) - Storyboard Automatically Navigation")
            debugPrint("Fourth parameter: \(fourth) - Storyboard Automatically Navigation")
        })
    }

如你所见,dataFromPreviousController( 是一个闭包,它接收一个 "类型",类型需要与我们在上一个屏幕上发送的类型相同,甚至顺序也一样。

这就是这个示例流程的主要功能,查看代码。


使用 NIB 自动导航

这基本上与使用 Storyboard 时一样,但在这次操作中,你使用的是 NIB,你使用将被生成的实例。

本示例中的功能:使用 NIB 进行自动导航。

将 NIB 的名称与你的视图控制器类名称相同是强制性的,因为当两者具有相同名称时,iOS 知道如何实例化。

  • 在此需要注意的是,使用此方法时,我们将使用的实例是您已注册到容器中的实例,例如
containerStack.registerModule(for: AutomaticallyFirstViewController.self) { () -> AutomaticallyFirstViewController in
            return AutomaticallyFirstViewController()
        }

当我们的流程管理器请求类AutomaticallyFirstViewController时,它将来到此注册并获取此对象/实例。如果您想的话,您可以自己实例化,使用某些方法或变量设置一些值并返回,例如

containerStack.registerModule(for: AutomaticallyFirstViewController.self) { () -> AutomaticallyFirstViewController in
			var viewController = AutomaticallyFirstViewController()
			viewController.myVariable = "Test Data"
			viewController.myMethod("With some data")
            return viewController
        }

这也同样有效,当您使用NIB时,我会看到更多优势,因为您有更好的方法来实例化对象。以这种方式,您可以在实际显示之前用任何需要填充您的实例,在示例中使用NIB传递参数时,您将看到更多的优势。

  1. 导航将使用自动方式,使用两种方法:
  2. goNext()
  3. getBack()

使用NIB自动导航,并可选择转到您选择的任何其他视图控制器

此格式与使用NIB自动导航相同,但在这里我们使用其他方法来转到我们在Container内部声明的任何视图控制器,因此我们仍然可以使用goNext(),但我们使用另一个名为goNext(screen: ViewController.self)的方法。

例如,在我们的示例中,从GoAnywhereFirstViewControllerGoAnywhereSeventhViewController,我们唯一需要做的是从这个流程管理器使用此方法goNext,指定将自动解析的类型。

示例

navigationFlow?.goNext(screen: GoAnywhereSeventhViewController.self)

deeplink概念

这基本上是使用此框架的优势的“想法”,deeplink有时用于接收一些数据,这些数据将使我们转到应用内的某个屏幕/视图,我们当然需要知道在哪里,而且有很多方法可以做到这一点,在这里我将介绍如何使用此框架来简化这个过程。

  • 示例:您需要有一个逻辑,即当收到deeplink用户点击时,我们发送一些参数。我将使用示例来说明您作为参数传递您想去的位置,控制器的名称,然后我们需要使用navagation controller的根根据类型生成一个新的Flow Manager。

  • 另一个要求是,当用户完成此流程时,您希望将自动生成的“flow”发送回一些值,就像一个布尔值,您可以在回调中进行评估以知道用户是否完成。

您可以使用任何作为参数,只要您有一个接收此并“转换”为希望启动视图类型的对象即可。

示例

// MARK: - Create your flow dinamicaly
    private func createYourNewFlow(for view: UIViewController.Type) {

        guard let navigationStack = self.navigationFlow?.container() else { return }

        FlowManager(root: view,
                    container: navigationStack)
            .dismissedFlowWith { [weak self] closeAll in

            // Using this parameter for the situation that we want to dismiss both navigation from the top one
            if (closeAll as? Bool) == true {
                self?.navigationFlow?.dismissFlowController()
            }
        }.start()
    }
  1. 为了知道我们想去哪里,我们需要知道任何UIViewController类型,这就是为什么我们将此作为参数接收在这个方法中。
  2. 您必须在其中声明所有可能的视图控制器以可选方式前往容器的必填项。
  3. 创建您的FlowManager
  4. 实现参数为接收Any类型的闭包的dismissedFlowWith方法。您只需检查类型,或接收任何值即可。
  5. 在这种情况下,我使用 dismissal 回调来关闭流程,但您也可以自行在最后关闭。但因为我们想验证回调,所以建议在这里结束。我们通过调用self?.navigationFlow?.dismissFlowController()结束使用。

如何通过回调结束流程?在DeeplinkFirstViewController中,我们传递一个boolean值给 dismiss 方法。

navigationFlow?.dismissFlowController().finishFlowWith(parameter: true)

在这个流程中您将看到的另一种方法来返回,除了getBack()dismissFlowController()之外,还有

navigationFlow?.getBack(pop: .popToRoot(animated: true))

如名字所示,此方法只是根据我们是否想动画化返回导航控制器的根。


使用NIB和模态视图展示的导航流程

在这个导航中,关于导航本身没有差异,这里的目标是如何以 modal 格式打开一个新的视图。

为此,我们有两种实现方式,我们都会使用不同的方法来以模态形式展示,我们使用的是方法 .goNextAsModal(),遵循自动导航的原则,如果我没有指定要进入哪个屏幕,我们将查找栈中的下一个屏幕并将其自动处理。

进入 AutomaticallyThirdModalViewController,您将看到以下实现

navigationFlow?.goNextAsModal().dismissedModal(callback: { [unowned self] in
            debugPrint("Finished close modal view")
            self.getSomeDataFromClosedModal()
        })
  • 在这里我们有另一个辅助方法,这不是强制性的,但我们使用了它,它是 dismissModal,当我们关闭模态视图时是一个回调,如果我们想在关闭后做什么,在这种情况下,我们创建了一个方法,该方法将获取对模态视图的引用并查看实例中的参数
private func getSomeDataFromClosedModal() {
        let modalViewController = self.navigationFlow?.container()?.getModuleIfHasInstance(for: AutomaticallyFourthModalViewController.self)
        // Getting from variable
        if let fromVariable = modalViewController?.someData {
            debugPrint("Getting data from modal as variable: \(fromVariable)")
        }
        // Getting from function
        if let fromFunction = modalViewController?.modalViewSampleData() {
            debugPrint("Getting data from modal as function: \(fromFunction)")
        }
    }

如果我们留意一下,这里有一些重要的事情要分析,每次我们关闭任何模态视图,甚至返回,导航控制器都会始终 销毁 那个对象引用,那么我们如何仍然保留关闭的对象的引用呢?

在这个容器中,当我注册时,我说我希望这个对象一旦实例化就有 引用,这意味着当我们返回时将不会销毁引用。唯一将被销毁的情况是我们完全关闭我们的 FlowManager

如何注册为强

containerStack.registerModule(for: AutomaticallyFourthModalViewController.self) { () -> AutomaticallyFourthModalViewController in
            return AutomaticallyFourthModalViewController()
        }.inScope(scope: .strong)

我们使用的是方法 .inScope(scope: .strong),这将告诉我们的框架在实例化后不要取消对此对象的引用,并且这里需要特别注意,我们通常的行为是在返回并继续下一个时,我们会得到一个新的实例,在这种情况下将使用相同的实例,因此,需要特别注意。

回到 getSomeDataFromClosedModal,我们如何访问我们的实例?为此我们在流管理器中可以访问我们的 容器,并且在我们内部有一个方法来获取对象实例,它是:.getModuleIfHasInstance(for: AutomaticallyFourthModalViewController.self),传入我们想要检查的实例类型,如果存在则返回。

要销毁已展示的模态视图,我们需要使用另一个方法,因为作为模态视图展示的关闭方式不同,为此我们使用模态视图控制器中的方法 navigationFlow?.getBack(pop: .modal(animated: true)),传入类型指示 模态,以及是否将进行动画处理将销毁。


使用NIB进行导航并通过参数传递给下一个视图

这里我们将遵循用于NIB文件导航的相同原则,但是有一个向被调用的下一个视图发送参数的可能性。

为此,我们需要更改为如何在我们的 容器 中注册我们的视图,因为我们需要说明我们期望哪些参数,为此这些参数是 类型化 的,这意味着我声明的期望类型必须是那个类型,否则将不会解析。

转到 ParameterNavigationContainer 类,在这里注册对模块的类,这是一个示例

containerStack.registerModuleWithParameter(for: ParameterInitialViewController.self) { (arguments: (String, Double, String, Int)) -> ParameterInitialViewController? in

            let (first, second, third, fourth) = arguments

            let initialViewController = ParameterInitialViewController()
            initialViewController.setParameters(first: first, second, third, fourth)

            return initialViewController
        }
  1. 首先我们需要使用另一个方法进行注册,我们需要使用 .registerModuleWithParameter(for:,并且我们有一个闭包将接收参数,并且为此我们需要指定我们将接收什么。
  2. 在我们的场景中,我们将创建 ParameterInitialViewController() 的实例,并将我们正在接收的参数设置,然后返回我们调用此方法时想要解析的实例。

现在让我们看看如何调用这个类。由于这个更改,我将展示两种可能性,一种是创建我们需要的 FlowManager 时,必须指定第一个要显示的视图控制器,并且为此可以传递我们想要的任何参数或参数。

  1. FlowManager 将参数设置到根视图;
FlowManager(root: ParameterInitialViewController.self, container: container.setup(), parameters: {
                    return (("Felipe", 3123.232, "Florencio", 31))
        })
  • 如你所见,我们指定了相同的数据类型和顺序,因此,正如我们在容器中描述的那样,预期一个符合此模式 (String, Double, String, Int) 的元组将被成功解析。
  1. 调用导航方法跳转到下一个页面,传递任何参数。
  2. 首先是注册样本
containerStack.registerModuleWithParameter(for: ParameterFirstViewController.self) { (arguments: (String, Int)) -> ParameterFirstViewController? in
          let (first, second) = arguments

          let firstViewController = ParameterFirstViewController()
          firstViewController.setParameters(first: first, second)

          return firstViewController
      }

现在,在 ParameterInitialViewController 内,我们使用以下方式调用 ParameterFirstViewController 方法

navigationFlow?.goNextWith(screen: ParameterFirstViewController.self, parameters: { () -> ((String, Int)) in
            return ("Felipe Garcia", 232)
        })
  1. 如你所见,我们使用了名为 goNextWith 的方法,该方法需要知道我们想要跳转到哪个屏幕的类型,以及一个闭包,它期望返回发送到注册器以进行解析的参数。

需求

安装

SwiftyFlow 可通过 CocoaPods 获得。要安装它,只需将以下行添加到您的 Podfile 中

pod 'SwiftyFlow'

Carthage

要使用 Carthage 安装 Swinject,请将以下行添加到您的 Cartfile 中。

github "felipeflorencio/SwiftyFlow" ~> 0.6.0

文档

在这里,您将找到完整的文档,其中包括如何使用可用方法的解释和示例。 访问文档

作者

Felipe F Garcia, [email protected]

许可协议

SwiftyFlow采用GPL-3.0许可协议。更多信息请参阅LICENSE文件。