UIFlow 2.3.3

UIFlow 2.3.3

Ricardo Rauber Pereira 维护。



UIFlow 2.3.3

  • By
  • Ricardo Rauber Pereira

UIFlow - iOS 项目的导航和数据交互框架

Build Status CocoaPods Version License Platform

App Sample

TL; DR

UIFlow 是一个框架,允许 Coordinator 处理 UIViewControllers 之间的导航。您可以使用您的 ViewControllers(或从 UIFlowViewController 类派生)并创建 Coordinators 来处理所有内容。

还有一个 Instantiable 协议,可以从同名的故事板或 xib 文件实例化您的 ViewController。如果您真的很想了解这如何使您的生活更轻松,请完全阅读此文档,只需几分钟,但您也可以下载代码,并根据提供的 demo 项目查看其运行。

最后,您的 ViewController 不需要了解任何关于导航的内容,可能看起来像这样

import Combine
import UIFlow

class LoginViewController: UIFlowViewController {

    // MARK: - Dependencies

    var viewModel: LoginViewModel!

    // MARK: - VC Actions

    var finishedLogin: (() -> Void)?
    var goToUserRegistration: (() -> Void)?

    // MARK: - IB Outlets

    @IBOutlet private weak var emailTextField: UITextField!
    @IBOutlet private weak var passwordTextField: UITextField!

    // MARK: - Life Cycle

	override func viewDidLoad() {
        super.viewDidLoad()
        observe(viewModel.$state) { [weak self] state in
            guard let self = self else { return }
            switch state {
            case .loggedIn: self.finishedLogin?()
            case .loginError: self.showError()
            }
        }
    }

    // MARK: - IB Actions

    @IBAction func userRegistrationButtonTouchUpInside(_ sender: Any) {
        goToUserRegistration?()
    }

    @IBAction func loginButtonTouchUpInside(_ sender: Any) {
        viewModel.login(email: emailTextField.text, password: passwordTextField.text)
    }
}

谢谢!

它是什么?

有许多关于如何构建 iOS 应用的设计模式、架构和想法。嗯,这是另一个,但这是一个不错的框架,它不会让你感到震惊,这是结合了一些对我需求来说非常好用 ideas。

在多年的不同尺寸和类型的 app 工作中,我发现我可以使用一个简单的结构来处理它们中的大多数,但也应该遵循一些原则:

  • 易于理解
  • 易于使用
  • 将模型交互与用户交互分离
  • 使所有的 ViewController 都可以独立和可测试
  • 使 ViewController 可在不同的流中重用,而无需在它们中添加自定义代码
  • 使其具有可扩展性

是很棒的清单,对吧?遵循这些概念,我想出了 UIFlow,我希望它可以帮助您在项目中。

你可能想知道"关于 SwiftUI 应该怎么做?" 。这是 iOS 开发的一个重要步骤,但是由于许多项目正在使用 UIKit,因此要迁移到 SwiftUI 几乎是不可能的事情,而且旧的 iOS 版本可能无法使用,因此 UIFlow 可能是一个不错的选择。

设置

CocoaPods

如果你正在使用 CocoaPods,请将其添加到你的 Podfile 中,并运行 pod install

target 'Your target name' do
    pod 'UIFlow', '~> 2.1'
end

手动安装

如果你想要手动将库添加到你的项目中而不使用包管理器,只需将 Classes 文件夹中的所有文件复制到你的项目中即可。

它是如何工作的?

我们先从基础开始。 UIFlow 非常简单,它仅包含这些协议/模型:

  • 可实例化
  • 协调器
  • UIFlowViewController

真的就这么简单吗?是的!一般来说,一个简单的应用看起来大概就是这样。

General flow

可见?接下来,我们来谈谈它的每个部分。

概念

可实例化的

我见过一些协议用于从 storyboardsxibs 按文件名实例化 ViewControllers,所以我将它们组合成一个独特的协议。这里您可以看到使用是多么简单,比如我们创建了这个 LoginViewController 并将其设置为初始视图控制器

  • LoginViewController.storyboard
  • LoginViewController.swift

Storyboard

这是在 Coordinator 中使用它的方法

func navigateToLogin(animated: Bool) {
    guard let scene = LoginViewController.instantiate() else { return }
    move(to: scene, animated: animated)
}

就这么简单!😎

协调器

您可能已经看到了许多不同版本的 Coordinator 实现。它与那些类似,但最大的区别是它一次只运行一个子项,而在通常情况下,您会看到很多子项。

View Controllers 不应该知道用户从哪里来以及会去哪里。这几乎类似于 依赖注入 的想法,因为 ViewController 只会做它本应做的事情,并告诉 Coordinator 用户的 意图

例如

LoginViewController 上,将有一些带有验证和登录服务的输入。登录成功后,用户应该去哪里?去菜单?去只有登录用户才能看到的特定区域?好吧,这没关系!

LoginViewController 将只会告诉 Coordinator 登录成功,并将导航留给 Coordinator。这样一来,一个 AccessCoordinator 就可以简单地导航到菜单,而一个 PremiumCoordinator 就可以导航到秘密区域。

看发生了什么?LoginViewController 不关心它应该去哪里,只需要做它打算做的事情。

让我们看看它是如何实际工作的

import UIFlow

class AppCoordinator: Coordinator {

    // MARK: - Properties
    
    var navigation: UINavigationController
    weak var startViewController: UIViewController?
    weak var topViewController: UIViewController?
    var parent: Coordinator?
    var child: Coordinator?

    var firstTime = true
    var loggedIn = false

    // MARK: - Initialization

    init(navigation: UINavigationController) {
        self.navigation = navigation
    }

    // MARK: - Coordinator

    func start(animated: Bool) {
        if firstTime {
            navigateToOnboarding(animated: animated)
        } else if loggedIn {
            navigateToMenu(animated: animated)
        } else {
            navigateToLogin(animated: animated)
        }
    }

    // MARK: - Login 

    func navigateToLogin(animated: Bool) {
        guard let scene = LoginViewController.instantiate() else { return }
        scene.goToUserRegistration = { [weak self] in
            self?.navigateToUserRegistration(animated: true)
        }
        scene.finishedLogin = { [weak self] in
            self?.loggedIn = true
            self?.start(animated: true)
        }
        move(to: scene, animated: animated)
    }

    ...
}
...

子协调器

Coordinator 是一个将执行的不同流程,当它完成后,它将返回到父流程。您可以将其视为涉及许多场景的 app 的不同功能。唯一不同的是,Coordinator 一次只执行一个子项,但一个子项可以有它自己的子项,依此类推。

为什么一次只养一个孩子,而不是同时养育多个孩子?很简单,在大多数情况下,你只会看到一个流程,完成时会返回给父流程。正因为如此,在 UIFlow 中,你一次只能运行一个子流程。

当你启动子流程时,父流程会保留最后一个正在显示的 ViewController 的引用,当子流程结束时,父流程会返回导航栈中的那个 ViewController

这是一个在你的项目中可以做的例子。

Children Coordinators

那么如何转到子流程呢?就像这样

func goToItems(_ sender: MenuViewController) {
    let itemsCoordinator = ItemsCoordinator(navigation: navigation)
    start(child: itemsCoordinator, animated: true)
}

很简单。你在流程中完成业务了吗?你可以这样返回

func closeItemsList(_ sender: ItemsListViewController) {
    finish(animated: true)
}

就这样了!它会返回到父流程。

TabBarCoordinator

有时在你的应用中需要一个 TabBar,因此为了处理这种类型的导航,我创建了 TabBarCoordinator。正如预期的那样,它非常简单。

import UIFlow

class MenuCoordinator: TabBarCoordinator {
    
    // MARK: - Coordinator Properties
    
    var navigation: UINavigationController
    weak var startViewController: UIViewController?
    weak var topViewController: UIViewController?
    var parent: Coordinator?
    var child: Coordinator?
    
    // MARK: - TabBarCoordinator Properties
    
    let tabBar: UITabBarController
    var items: [Coordinator]
    
    // MARK: - Initialization
    
    init(navigation: UINavigationController,
         tabBar: UITabBarController = UITabBarController()) {
        
        self.navigation = navigation
        self.tabBar = tabBar
        items = []
    }
}

使用也非常简单,你只需要使用以下方法之一添加标签栏项

let tabBarItem = UITabBarItem(title: "Item 1",
                              image: nil,
                              selectedImage: nil)
coordinator.addItem(tabBarItem: tabBarItem,
                    coordinator: TestCoordinator(navigation: UINavigationController()))
                    
coordinator.addItem(title: "Item 2",
                    image: nil,
                    selectedImage: nil,
                    coordinator: AnotherCoordinator())

最后,如果你需要移除一个项,只需这样做

coordinator.removeItem(at: 0)

UIFlowViewController

UIFlowViewController 是一个非常简单的 UIViewController,它实现了使用 observe() 方法观察模型的 Combine 框架的基本功能。因此,如果你想要从中受益,可以简单地这样使用它

import UIFlow

class NewItemViewController: UIFlowViewController {

    // MARK: - Dependencies

    var viewModel: NewItemViewModel!

    // MARK: - IB Outlets

    @IBOutlet weak var itemNameTextField: UITextField!

    // MARK: - VC Actions

    var newItemCompleted: (() -> Void)?
    var newItemCanceled: (() -> Void)?

     // MARK: - Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
        observe(viewModel.$state) { [weak self] value in
            guard let self = self else { return }
            if value == .itemAdded {
                self.itemNameTextField.text = nil
                self.showAlert(title: "Nice!", message: "Item added!")
            }
        }
    }

    // MARK: - Alerts

    func showAlert(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        present(alert, animated: true, completion: nil)
    }

    // MARK: - IB Outlets 

    @IBAction func addItemButtonTouchUpInside(_ sender: Any) {
        guard let itemName = itemNameTextField.text, !itemName.isEmpty else { return }
        viewModel.addItem(name: itemName)
    }

    @IBAction func cancelButtonTouchUpInside(_ sender: Any) {
        if viewModel.state == .itemAdded {
            newItemCompleted?()
        } else {
            newItemCanceled?()
        }
    }
}

看看 NewItemViewController 多么简单,以及它对数据本身和导航的关注有多少?这正是我试图实现的目标!NewItemViewController 只顾用户的交互,让 ViewModelCoordinator 做它们的工作。

移除观察者

UIFlowViewController 会在 deinit 时移除所有观察者,但如果存在另一个对该视图控制器的强引用,则不会调用 deinit,但你可以通过调用 removeObservers() 轻松移除所有观察者。

@IBAction func backButtonTapped(sender: UIButton) {
    removeObservers()
    backAction()
}

感谢👍

本框架的创建得以实现,得益于以下令人惊叹的人们

欢迎反馈

如果您注意到任何问题、遇到了困难,或者只想聊天,请自由地创建一个问题。我们将很乐意为您提供帮助。

许可证

UIFlow 采用 MIT 许可证 发布。