FlowKit 0.2.0

FlowKit 0.2.0

测试已测试
Lang语言 SwiftSwift
许可证 MIT
发布最后发布2017年3月
SwiftSwift版本3.0
SPM支持SPM

Filip Zawada 维护。



FlowKit 0.2.0

Flow Kit

FlowKit iOS/Swift

使用 FlowKit 轻松定义屏幕流程。优雅的语法,明确分离关注点以及可测试性,使其成为您当前 MV* 设置的完美插件。

let tutorialScreen = Flow(with: TutorialViewController()) { vc, lets in
    vc.onContinue = lets.push(loginScreen)
}
let loginScreen = Flow(with: LoginViewController()) { vc, lets in
    vc.onLogin = lets.push(dashboardScreen)
    vc.onBack = lets.pop()
}
let dashboardScreen = Flow(with: DashboardViewController()) { vc, lets in
    vc.onBack = lets.pop()
    vc.onLogOut = lets.popToRoot()
}

这支持以下流程

 ____________                _________             _____________
|            |              |         |           |             |  
| TutorialVC | onContinue() | LoginVC | onLogin() | DashboardVC |
|            | -----------> |         | --------> |             |
|____________|              |_________|           |_____________|

以下是我们的任何视图控制器可能看起来像什么

class DashboardViewController: UIViewController {
    var onBack: () -> Void = {}
    var onLogOut: () -> Void = {}

    @IBAction func backButtonTapped(button: UIButton) {
        onBack()
    }

    @IBAction func logOutButtonTapped(button: UIButton) {
        onLogOut()
    }
}

定义一个流程

Flow 是任何 UIViewController 的包装器。通过 Flow,您可以定义您的 ViewController 如何与其他视图控制器交互。可能的主要交互包括

  • push(otherViewController)
  • present(otherViewController)
  • pop()
  • dismiss()

这种方法有几个主要优点,因为您的 ViewController

  • 不需要了解镇上的其他视图控制器 #松耦合
  • 专注于管理自己的视图,而不是管理应用的导航 #单一职责
  • 更容易测试 #可测试性
  • 代码更容易阅读,因为导航相关的部分可以放在一个地方 #可读性
  • 您的 ViewController 的入口点和出口点被明确定义 #清晰的API

有多种初始化流程的方法

  1. 不带交互

    let yourScreen = Flow(with: YourViewController())
    
    // or e.g. with a custom xib
    let yourScreen = Flow(with: YourViewController(nibName: "YourView", bundle: nil))
    

    注意,由于 @autoclosureYourViewController() 是懒加载的,即仅在需要时才会实例化。#swiftmagic

  2. 带交互(简短版本)

    let yourScreen = Flow(with: YourViewController()) { vc, lets in
       vc.onBack = lets.pop()
       vc.onAbout = lets.present(otherScreen)
    }
    
  3. 带交互(详细版本)

    let yourScreen = Flow<YourViewController> { lets in
       // let's initialize our ViewController from a storyboard 
       let storyboard = UIStoryboard(name: "YourView", bundle: nil)
       let vc = storyboard.instantiateViewController(withIdentifier: "YourView") as! YourViewController
    
       vc.onBack = lets.pop()
       vc.onAbout = lets.present(otherScreen)
    
       return vc
    }
    

传递参数

所以,让我们假设我们有一个购物应用。"ItemViewController" 展示我们的 GreatProduct™。如果用户决定购买它,"CheckoutViewController" 就会被带到屏幕上,引导用户通过结账过程。那么,"CheckoutViewController" 如何知道实际正在购买哪个产品呢?显然,它应该从 "ItemViewController" 接收这些信息。这就是如何做到这一点

class ItemViewController: UIViewController {
    var item: Item? // = GreatProduct™
    var quantity = 0

    var onCheckout: (Item, Int) -> Void?

    @IBAction func checkout(button: UIButton) {
        if let item = item, onCheckout = onCheckout {
            onCheckout(item, quantity)        
        }
    }
}

class CheckoutViewController: UIViewController {
    func prepare(item: Item, quantity: Int) {
        print("User wants to purchase \(item) × \(quantity)")
    }
}

// our flow

let checkoutScreen = Flow(with: CheckoutViewController(nibName: "CheckoutView", bundle: nil))

let itemScreen = Flow(with: ItemViewController()) { vc, lets in

    vc.onCheckout = lets.push(checkoutScreen) { $0.prepare }

}

这里的技巧是将从 "onCheckout()" 传递的参数带到 "prepare()" 函数中。正是在这里 vc.onCheckout = lets.push(checkoutScreen) { $0.prepare } 实现的。简单来说,我们会这样说法

                   vc.  onCheckout= lets.push( checkoutScreen){$0.prepare }
hey itemViewController, on checkout lets push checkout screen and prepare it

在此需要注意的是,必须确保onCheckout方法的签名与prepare方法的签名完全一致,这样才能成功传递参数。

分组流程

在常见场景中,您可以将流程分组到单独的类中,例如可以拥有LoginFlow(登录流程)、SignUpFlow(注册流程)、CheckoutFlow(结账流程)等。如果您的应用规模较小,可能只需要一个MainFlow(主流程)就足够了。

class MainFlow {
    lazy var tutorialScreen: Flow<TutorialViewController> = Flow { [unowned self] lets in
        let screen = TutorialViewController()

        screen.onContinue = lets.push(self.loginScreen) { $0.prepare }

        return screen
    }

    lazy var dashboardScreen: Flow<DashboardViewController> = Flow { [unowned self] lets in
        let screen = DashboardViewController()

        screen.onBack = lets.pop()
        screen.onLogOut = lets.popTo(self.loginScreen)
        screen.onExit = lets.popToRoot()

        return screen
    }

    lazy var loginScreen: Flow<LoginViewController> = Flow { [unowned self] lets in
        let screen = LoginViewController()

        screen.onLogin = lets.push(self.dashboardScreen)
        screen.onBack = lets.pop()

        return screen
    }
}

备注1。我们不得不使用lazy var,以便允许dashboardScreen引用loginScreen,反之亦然。使用常规的let变量,编译器不会允许我们在dashboardScreen中使用loginScreen

备注2。遗憾的是,由于编译器中的错误,我们必须声明变量类型,否则我们无法使用self。

可测试性

建设中

我想创建自定义的nimble匹配器,以便使测试我们的流程变得与编写它们一样简单。

let mainScreen = Flow(with: MainViewController())
mainScreen.letsFactory = LetsSpyFactory()
let spy = mainScreen.letsFactory.makeSpy()

let vc = mainScreen.viewController

expect(vc.onBack).to(haveBeenBackedWith(spy.pop))
expect(vc.onLogOut).to(haveBeenBackedWith(spy.popTo(loginScreen)))
expect(vc.onExit).to(haveBeenBackedWith(spy.popToRoot()))

扩展

建设中

FlowKit将很好地与以下内容集成:

  • RxSwift(正在准备中)

      let tutorialScreen = Flow(with: TutorialViewController()) { vc, lets in
          vc.onContinue
            .asDriver()
            .drive(lets.push(loginScreen))
            .addToDisposeBag(disposeBag)
      }

    这只是个示意图,我还不确定这将会是怎样的。

关于

此项目由Filip Zawada创建和维护。它的创建是为了解决我在之前的几个应用中遇到的导航问题。

这个项目是针对Krzysztof Zabłocki所提出的Flow Controllers理念的下一迭代版本,他在这里进行了描述。

许可协议

即将选择。

设计于波兰,用Swift构建🙃