协调器模式 - 使用情节提要而不是 Xib



这是我第一次使用协调器模式。虽然我已经意识到它的重要性,但我有一个主要的担忧。
我浏览了这篇关于这种模式的惊人文章。事实上,我能够使用它自己构建一个演示项目。不过有一点 - 建议使用Xib。并没有专门提到不能使用故事板,但是在文章末尾通过这些行,让我不这么认为:

权力越大,责任越大(和局限越大)。要使用 此扩展,您需要为每个扩展创建一个单独的情节提要 UIViewController.情节提要的名称必须与 UIViewController的类。此 UIViewController 必须设置为 此情节提要的初始 UIViewController 。

有人提到,在故事板的情况下,我们应该创建一个扩展并在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创建单独的故事板,在协调器模式中使用 Xib 是一种更好的方法?
  2. 为什么我们需要为每个UIViewController创建一个单独的故事板?我们不能使用UIViewController的故事板标识符来通过使用 segue 链接任何UIViewController吗?这种方式可以使用标识符调整上述扩展并轻松实现相同的目的。

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

如果要从主情节提要初始化 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
}
}

唯一的要求是它使用的每个视图控制器都具有此协议,并且具有与类同名的情节提要 ID。

您可以通过以下方式使用它:

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

免责声明:本文中的协议也可能对您有所帮助

更新:(添加参考)

Soroush Khanlou 通常在有关 iOS 和 Redux 中的协调器模式的其他文章和教程中被引用和引用。他这里有一篇文章(日期为2015年,objective-c代码),你可能会发现这是一篇有趣的读物。

我将协调器与情节提要一起使用的方法是使用多个情节提要。每个功能/模块一个情节提要。

为什么是多个故事板而不是一个?当使用许多功能和团队合作时,最好拆分故事板,因为只使用一个故事板会导致很多合并冲突,修复故事板 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:大多数时候,每个模块/功能都有自己的故事板协调器,但这取决于您将使用的UIViewController的可重用性。

编辑

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

现在,程序员(我们)不需要知道特定 VC 附加到哪个故事板,我们现在可以简单地对视图控制器进行子类化以StoryboardSceneBased,提供该视图控制器的故事板,并通过执行CustomViewController.instantiate()来实例化它

// From this code
let viewController = AppStoryboard.Login.instantiate(LoginViewController.self)
// To this code
let viewController = LoginViewController.instantiate()

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

关于 XIB 与故事板

我看到为什么在使用协调器模式时将 xib 而不是故事板用于视图控制器的原因很小。我想到的 xibs 的一个优势是,在使用 xib 时,您可以稍后为给定的视图控制器使用不同的子类,并使用相同的 xib。例如,假设您为 EmployeesViewController 类创建了一个 xib,您可以稍后创建具有修改功能的 AdministratorsViewControllers 子类,并使用您之前创建的相同 xib 初始化它。使用情节提要时,您无法执行此操作,因为视图控制器的类已在情节提要上设置,无法更改。例如,如果您正在创建一个框架,并且希望让用户在保留 UI 的同时对基类进行子类化,则类似的东西可能会很有用。但是在大多数情况下,您可能不需要做这样的事情。另一方面,使用情节提要将使您能够访问诸如情节提要上的表视图单元格原型、表视图控制器中的静态单元格以及使用 xibs 时不可用的其他功能等功能。因此,虽然在某些情况下 xib 更好,但在大多数情况下,故事板可能更有用。

关于为每个 UIViewController 创建单独的情节提要

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

我使用了枚举并更改了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
}
}