协调器模式 - 使用 Storyboard 而不是 Xib

Coordinator pattern - Using Storyboard instead of Xib

这是我第一次使用协调器模式。虽然我已经意识到它的重要性,但我有一个主要问题。
我浏览了 this 篇有关此模式的精彩文章。事实上,我能够使用它自己构建一个演示项目。不过有一点 - 建议使用 Xib。没有专门提到 Storyboards 不能使用,但是在文章末尾通过这些行,让我觉得不是这样:

With great power comes great responsibility (and limitations). To use this extension, you need to create a separate storyboard for each UIViewController. The name of the storyboard must match the name of the UIViewController‘s class. This UIViewController must be set as the initial UIViewController for this storyboard.

有人提到,在 Storyboards 的情况下,我们应该创建一个扩展并在 UIViewController 中使用它:

extension MyViewController: StoryboardInstantiable {
}  

故事板可实例化:

import UIKit

protocol StoryboardInstantiable: NSObjectProtocol {
  associatedtype MyType  // 1
  static var defaultFileName: String { get }  // 2
  static func instantiateViewController(_ bundle: Bundle?) -> MyType // 3
}

extension StoryboardInstantiable where Self: UIViewController {
  static var defaultFileName: String {
    return NSStringFromClass(Self.self).components(separatedBy: ".").last!
  }

  static func instantiateViewController(_ bundle: Bundle? = nil) -> Self {
    let fileName = defaultFileName
    let sb = UIStoryboard(name: fileName, bundle: bundle)
    return sb.instantiateInitialViewController() as! Self
  }
}

查询:

  1. 正如作者提到的,必须为每个 UIViewController 创建单独的 Storyboard,如何在 Coordinator 模式中更好地使用 Xib ?
  2. 为什么我们需要为每个 UIViewController 创建一个单独的 故事板?我们不能通过不使用 segues 链接任何 UIViewController 来使用 UIViewController 的故事板标识符吗?这样可以使用标识符调整上述扩展并轻松实现相同。

我已经多次阅读该教程,它为每个 View 控制器使用了一个 Coordinator,这对我来说没有意义。我认为 Coordinator 的目的是将导航逻辑从视图控制器转移到可以管理整个流程的更高级别的对象中。

如果您想从主故事板初始化 ViewController,请改用此协议和扩展:

import UIKit

protocol Storyboarded {
    static func instantiate() -> Self
}

extension Storyboarded where Self: UIViewController {
    static func instantiate() -> Self {
        // this pulls out "MyApp.MyViewController"
        let fullName = NSStringFromClass(self)

        // this splits by the dot and uses everything after, giving "MyViewController"
        let className = fullName.components(separatedBy: ".")[1]

        // load our storyboard
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)

        // instantiate a view controller with that identifier, and force cast as the type that was requested
        return storyboard.instantiateViewController(withIdentifier: className) as! Self
    }
}

唯一的要求是它使用的每个View controller都有这个协议,并且有一个与class同名的StoryboardID。

你可以这样使用它:

private func startBlueFlow() {
    let vc = BlueViewControllerOne.instantiate()
    vc.coordinator = self
    self.navigationController.push(vc, animated: true)
}

免责声明: 协议取自 this article 这也可能对您有帮助

更新:(添加参考)

Soroush Khanlou is commonly credited and referenced in other articles and tutorials regarding the coordinator pattern in iOS and Redux. He has an article here(2015 年,objective-c 中的代码)您可能会发现这是一本有趣的读物。

我将协调器与故事板一起使用的方法是使用多个故事板。每个 feature/module 一个故事板。

为什么有多个故事板而不是一个?当与一个团队一起处理很多功能时,最好拆分你的故事板,因为只使用一个故事板会导致很多合并冲突和修复故事板git冲突是成为 iOS 开发人员的痛苦之一。

这是我的做法。

首先我有一个名为 AppStoryboardType 的协议,我将在一个包含我所有故事板名称的枚举上实现它。

protocol AppStoryboardType {
    var instance: UIStoryboard { get }

    func instantiate<T: UIViewController>(_ viewController: T.Type, function: String, line: Int, file: String) -> T

    func instantiateInitialViewController() -> UIViewController?
}

extension AppStoryboardType {
    func instantiateInitialViewController() -> UIViewController? {
        return self.instance.instantiateInitialViewController()
    }
}

extension AppStoryboardType where Self: RawRepresentable, Self.RawValue == String {
    var instance: UIStoryboard {
        return UIStoryboard(name: self.rawValue, bundle: nil)
    }

    func instantiate<T: UIViewController>(
        _ viewController: T.Type,
        function: String = #function,
        line: Int = #line,
        file: String = #file) -> T {

        let storyboardID: String = T.storyboardIdentifier

        guard let vc = self.instance.instantiateViewController(withIdentifier: storyboardID) as? T else {
            fatalError("ViewController with identifier \(storyboardID), not found in \(self.rawValue) Storyboard.\nFile : \(file) \nLine Number : \(line) \nFunction : \(function)")
        }

        return vc
    }
}

enum AppStoryboard: String, AppStoryboardType {
    case Main /* ... Insert your other storyboards here. */

    // These are the refactored modules that use coordinator pattern.
    case PasswordRecovery, Registration
}

extension UIViewController {
    public static var defaultNibName: String {
        return self.description().components(separatedBy: ".").dropFirst().joined(separator: ".")
    }

    static var storyboardIdentifier: String {
        return "\(self)"
    }

    static func instantiate(fromAppStoryboard appStoryboard: AppStoryboard) -> Self {
        return appStoryboard.instantiate(self)
    }
}

既然我已经向您展示了我使用的基础,下面是它在代码上的实现方式。

let viewController = AppStoryboard.Login.instantiate(LoginViewController.self)
viewController./// set properties if ever you need to set them
presenter.present(viewController, animated: true, completion: nil)

PS:大多数时候每个 module/feature 我都有自己的 StoryboardCoordinator,但这取决于您将使用的 UIViewController 的可重用性。

编辑:

1 年后,我现在已经停止使用我的 AppStoryboard 方法,现在使用 Reusable 库。这背后的原因是它更干净,更不容易出现人为错误。

现在程序员(我们)不需要知道特定的 VC 附加到哪个故事板,我们现在可以简单地将 viewcontroller 子类化为 StoryboardSceneBased,提供viewcontroller 的故事板并通过 CustomViewController.instantiate()

简单地实例化它
// From this code
let viewController = AppStoryboard.Login.instantiate(LoginViewController.self)

// To this code
let viewController = LoginViewController.instantiate()

我使用了枚举并更改了 instanciate() 方法。对我来说一切正常

enum OurStoryboards: String{
    case MainPage = "MainPage"
    case Catalog = "Catalog"
    case Search = "Search"
    case Info = "Info"
    case Cart = "Cart"
}

protocol Storyboarded {
    static func instantiate(_ storyboardId: OurStoryboards) -> Self
}

extension Storyboarded where Self: UIViewController {
    static func instantiate(_ storyboardId: OurStoryboards) -> Self {

        let id = String(describing: self)
        // load our storyboard
        var storyboard = UIStoryboard()
        switch storyboardId {
        case .MainPage:
            storyboard = UIStoryboard(name: OurStoryboards.MainPage.rawValue ,bundle: Bundle.main)
        case .Catalog:
            storyboard = UIStoryboard(name: OurStoryboards.Catalog.rawValue ,bundle: Bundle.main)
        case .Search:
            storyboard = UIStoryboard(name: OurStoryboards.Search.rawValue ,bundle: Bundle.main)
        case .Info:
            storyboard = UIStoryboard(name: OurStoryboards.Info.rawValue ,bundle: Bundle.main)
        case .Cart:
            storyboard = UIStoryboard(name: OurStoryboards.Cart.rawValue ,bundle: Bundle.main)
        }
        // instantiate a view controller with that identifier, and force cast as the type that was requested
        return storyboard.instantiateViewController(withIdentifier: id) as! Self
    }

}

你实际上问了两个问题,所以我也将我的回答分成两部分:

关于 XIB 与 Storyboard

我看到很少有理由在使用协调器模式时使用 xib 而不是故事板作为视图控制器。我想到的 xibs 的一个优点是,当使用 xib 时,您可以稍后为给定的视图控制器使用不同的 subclass 并使用相同的 xib。例如,如果您为 EmployeesViewController class 创建一个 xib,您可以稍后创建具有修改功能的 AdministratorsViewControllers subclass 并使用相同的 xib 初始化它你之前创建的。使用情节提要,您不能这样做,因为视图控制器的 class 已经在情节提要中设置并且无法更改。类似的东西可能会有用,例如,如果您正在创建一个框架,并且您想让用户有可能在保留您的 UI 的同时,subclass 您的基础 class。然而,在大多数情况下,您可能不需要做那样的事情。另一方面,使用故事板可以让您访问故事板上的 table 视图单元原型、table 视图控制器中的静态单元以及使用 xibs 时无法使用的其他功能。因此,虽然在某些情况下 xibs 更好,但在大多数情况下,故事板可能更有用。

关于为每个 UIViewController

创建一个单独的故事板

正如您所注意到的,您可以使用情节提要标识符,而不是将每个视图控制器拆分为一个单独的情节提要(如其他答案所示)。将每个视图控制器放入一个单独的故事板中可能看起来不是很典型,但它实际上并不像最初看起来那样毫无意义。可能最大的优势是,当您将每个视图控制器放在一个单独的故事板中时,您在团队中工作时通常会在 git 上获得较少的合并冲突(特别是因为有时 xcode 会更改某些属性的某些值故事板中的其他视图控制器,即使您不修改它们)。这也使您的团队的代码审查更快、更愉快。除此之外,如果它们有一些共同点 UI,那么将这些故事板复制到不同的项目中也更容易。这可能特别有用,例如,如果您在一家为各种客户创建特定类型的应用程序的公司工作。因此,正如您所见,这里有一些优势,但选择取决于您。我不会说这种方法是好是坏。我认为两者都很好,更多的是偏好问题。选一个你喜欢的,更适合你的吧。