可重用 4.1.2

Reusable 4.1.2

测试测试
语言语言 SwiftSwift
许可证 MIT
发布最新发布2021年9月
SPM支持SPM

Olivier Halligon 维护。



Reusable 4.1.2

Reusable

Reusable

一个Swift mixin,以强类型方式使用UITableViewCellsUICollectionViewCellsUIViewControllers,无需操作它们的字符串类型reuseIdentifiers。此库还支持使用简单的调用loadFromNib()加载通过XIB加载的任意UIView

CircleCI Platform Version Language: Swift 3 Language: Swift 4 Language: Swift 5

安装

要求:对于每个Swift版本,使用哪个Reusable版本?
Swift版本 Reusable版本
2.2 & 2.3 2.5.1
3.0 (†) 3.0.0 +
4.0 4.0.2 +
5.0 4.1.0 +

(†) Reusable 3.0代码也可以与Swift 4一起编译,您只需要4.0.2+,如果您使用Carthage进行集成

Reusable可以通过以下方式之一集成到您的Xcode项目中

Swift Package Manager (SPM)的安装说明

Swift Package Manager 是苹果公司用于将库集成到Swift项目的去中心化依赖管理器。它现在与Xcode 11完全集成

要使用SPM将Reusable集成到您的项目中,请在您的 Package.swift 文件中指定它

let package = Package(
    
    dependencies: [
        .package(url: "https://github.com/AliSoftware/Reusable.git", from: "4.1.0"),
    ],
    targets: [
        .target(name: "YourTarget", dependencies: ["Reusable", ])
        
    ]
)
Carthage 的安装说明

Carthage 是一个去中心化依赖管理器,用于将预构建框架添加到您的Cocoa应用程序中。

要使用Carthage将Reusable集成到您的Xcode项目中,请在您的 Cartfile 中指定它

github "AliSoftware/Reusable"
CocoaPods 的安装说明

CocoaPods 是一个依赖管理器,用于自动化将框架集成到Swift和Objective-C Cocoa项目中。

要使用CocoaPods将Reusable集成到您的Xcode项目中,请在您的 Podfile 中指定它

pod 'Reusable'

介绍

这个库旨在让创建、出列和实例化可重用视图变得非常简单:从显然的 UITableViewCell 到自定义 UIViews,甚至支持从 Storyboards 中的 UIViewControllers
这一切都只需将你的类标记为遵循一个协议,而无需添加任何代码,并且还能创建一个没有更多基于 String 的 API 的类型安全的 API。

// Example of what Reusable allows you to do
final class MyCustomCell: UITableViewCell, Reusable { /* And that's it! */ }
tableView.register(cellType: MyCustomCell.self)
let cell: MyCustomCell = tableView.dequeueReusableCell(for: indexPath)

这个概念被称为Mixin(为所有方法提供一个默认实现的协议),在我的博客文章中有详细解释。

目录


类型安全的 UITableViewCell / UICollectionViewCell

✍️下面的例子和解释将使用 UITableViewUITableViewCell,但对 UICollectionViewUICollectionViewCell 也同样适用。

1. 声明你的 cells 遵循 ReusableNibReusable

  • 使用 Reusable 协议,如果它们不依赖于 NIB(这将使用 registerClass(…) 来注册 cell)
  • 使用 NibReusable typealias(= Reusable & NibLoadable)如果它们使用 XIB 文件作为其内容(这将使用 registerNib(…) 来注册 cell)
final class CustomCell: UITableViewCell, Reusable { /* And that's it! */ }

✍️ 注意

  • 对于Storyboard的tableView中嵌入的cells,这两个协议中的任何一个都会工作(因为您不需要手动注册cell,因为注册是由Storyboard自动处理的)
  • 如果您创建了基于 XIB 的 cell,请记得在 Interface Builder 中将其 Reuse Identifier 字段设置为其类名相同的字符串。
  • 💡 NibReusable 是一个 typealias,因此您仍然可以使用两个协议符合的 Reusable, NibLoadable 而不是 NibReusable
📑基于代码的自定义 tableView cell 示例
final class CodeBasedCustomCell: UITableViewCell, Reusable {
  // By default this cell will have a reuseIdentifier of "CodeBasedCustomCell"
  // unless you provide an alternative implementation of `static var reuseIdentifier`
  
  // No need to add anything to conform to Reusable. You can just keep your normal cell code
  @IBOutlet private weak var label: UILabel!
  func fillWithText(text: String?) { label.text = text }
}
📑基于 NIB 的自定义 tableView cell 示例
final class NibBasedCustomCell: UITableViewCell, NibReusable {
// or
// final class NibBasedCustomCell: UITableViewCell, Reusable, NibLoadable {
  
  // Here we provide a nib for this cell class (which, if we don't override the protocol's
  // default implementation of `static var nib: UINib`, will use a XIB of the same name as the class)
  
  // No need to add anything to conform to Reusable. You can just keep your normal cell code
  @IBOutlet private weak var pictureView: UIImageView!
  func fillWithImage(image: UIImage?) { pictureView.image = image }
}
📑基于代码的自定义 collectionView cell 示例
// A UICollectionViewCell which doesn't need a XIB to register
// Either because it's all-code, or because it's registered via Storyboard
final class CodeBasedCollectionViewCell: UICollectionViewCell, Reusable {
  // The rest of the cell code goes here
}
📑基于 NIB 的自定义 collectionView cell 示例
// A UICollectionViewCell using a XIB to define it's UI
// And that will need to register using that XIB
final class NibBasedCollectionViewCell: UICollectionViewCell, NibReusable {
// or
// final class NibBasedCollectionViewCell: UICollectionViewCell, Reusable, NibLoadable {
  
  // The rest of the cell code goes here
  
}

2. 注册您的单元格

除非您已在Storyboard中原型化您的单元格,否则您必须通过代码注册单元格类或Nib。

为此,请勿使用基于String的reuseIdentifier调用registerClass(…)registerNib(…),只需调用

tableView.register(cellType: theCellClass.self)
📑UITableView注册示例
class MyViewController: UIViewController {
  @IBOutlet private weak var tableView: UITableView!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // This will register using the class (via `register(AnyClass?, forCellReuseIdentifier: String)`)
    // because the CodeBasedCustomCell type conforms to Reusable, but not NibLoadable (nor the NibReusable typealias)
    tableView.register(cellType: CodeBasedCustomCell.self)
    // This will register using NibBasedCustomCell.xib (via `register(UINib?, forCellReuseIdentifier: String)`)
    // because the NibBasedCustomCell type conforms to NibLoadable (via the NibReusable typealias)
    tableView.register(cellType: NibBasedCustomCell.self)
  }
}

3. 拉取您的单元格

为了拉取一个单元格(通常在您的cellForRowAtIndexPath实现中),只需调用dequeueReusableCell(indexPath:)

// Either
let cell = tableView.dequeueReusableCell(for: indexPath) as MyCustomCell
// Or
let cell: MyCustomCell = tableView.dequeueReusableCell(for: indexPath)

只要Swift可以通过类型推理来理解您将想要一个类型为MyCustomCell的单元格(使用as MyCustomCell或显式指定接收变量cell: MyCustomCell),它将神奇地推断单元格类及其所必需的reuseIdentifier,以及确切需要返回的类型,以避免重新进行类型转换。

  • 不再需要您手动操作reuseIdentifiers字符串!
  • 也不再需要将返回的UITableViewCell实例强制转换为MyCustomCell类!
📑使用Reusable的`cellForRowAtIndexPath`实现示例
extension MyViewController: UITableViewDataSource {
  func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    if indexPath.section == 0 {
      let cell = tableView.dequeueReusableCell(indexPath: indexPath) as CodeBasedCustomCell
      // Customize the cell here. You can call any type-specific methods here without the need for type-casting
      cell.fillWithText("Foo")
      return cell
    } else {
      let cell = tableView.dequeueReusableCell(indexPath: indexPath) as NibBasedCustomCell
      // Customize the cell here. no need to downcasting here either!
      cell.fillWithImage(UIImage(named:"Bar"))
      return cell
    }
  }
}

现在您拥有的只有一组漂亮的代码和类型安全的单元格,具有编译时检查,并且不再存在基于String的API!

💡如果要在运行时计算要拉取的单元格类并将其存储在变量中,显然无法使用as theVariablelet cell: theVariable。相反,您可以使用可选参数cellType(否则它由返回类型推断,因此不需要显式提供)

📑运行时确定单元格类型的示例
class ParentCell: UITableViewCell, Reusable {}
class Child1Cell: ParentCell {}
class Child2Cell: ParentCell {}

func cellType(for indexPath: NSIndexPath) -> ParentCell.Type {
  return indexPath.row.isMultiple(of: 2) ? Child1Cell.self : Child2Cell.self
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  let cellClass = self.cellType(for: indexPath)
  // As `self.cellType(for:)` always returns a `ParentCell` (sub-)class, the type
  // of the variable `cell` below is infered to be `ParentCell` too. So only methods
  // declared in the parent `ParentCell` class will be accessible on the `cell` variable.
  // But this code will still dequeue the proper type of cell (Child1Cell or Child2Cell).
  let cell = tableView.dequeueReusableCell(for: indexPath, cellType: cellClass)
  // Then fill the content of your cell (using methods/properties from `ParentCell` type)
  return cell  
}

基于XIB的具有类型安全和可重用视图

Reusable还可以让您创建可重用的自定义视图,这些视图在Interface Builder中设计,可以在其他XIB或Storyboard中以及通过代码重用。这允许将这些视图视为在应用程序中可多处使用的自定义UI小部件。

1. 声明您的视图以符合NibLoadableNibOwnerLoadable

在Swift源代码中声明您的自定义视图类时

  • 如果您使用的XIB不使用其“文件所有者”,且您正在设计的可重用视图是XIB的根视图,请使用NibLoadable协议
  • 如果您的XIB使用的是您可重用视图的类“文件所有者”,并且XIB的根视图将被设置为其内容的子视图,请使用NibOwnerLoadable协议
// a XIB-based custom UIView, used as root of the XIB
final class NibBasedRootView: UIView, NibLoadable { /* and that's it! */ }

// a XIB-based custom UIView, used as the XIB's "File's Owner"
final class NibBasedFileOwnerView: UIView, NibOwnerLoadable { /* and that's it! */ }

💡如果您计划在其他XIB或Storyboard中使用您自定义的视图,应该使用第二种方法
这将允许您在XIB/Storyboard中放置一个UIView,并在IB的检查器中将它的类改为您的自定义XIB视图类以使用它。在包含它的Storyboard实例化时,该自定义视图将自动从关联的XIB加载其内容,而无需编写额外的代码来手动加载自定义视图的内容。

2. 在Interface Builder中设计您的视图

例如,如果您的类名为MyCustomWidget,并使其成为NibOwnerLoadable

  • 文件所有者的类设置为MyCustomWidget
  • 通过该XIB的根视图(这是一个没有自定义类的标准UIView)及其子视图来设计视图的内容
  • 文件所有者(即MyCustomWidget)和其内容之间连接任何@IBOutlets@IBActions
🖼📑已配置为`NibOwnerLoadable`的视图

NibOwnerLoadable view in Interface Builder

final class MyCustomWidget: UIView, NibOwnerLoadable {
  @IBOutlet private var rectView: UIView!
  @IBOutlet private var textLabel: UILabel!

  @IBInspectable var rectColor: UIColor? {
    didSet {
      self.rectView.backgroundColor = self.rectColor
    }
  }
  @IBInspectable var text: String? {
    didSet {
      self.textLabel.text = self.text
    }
  }

}

然后,该小部件可以通过在Storyboard场景(或任何其他XIB)中放置一个UIView并改变IB检查器中它的类为MyCustomWidget来集成。

🖼`NibOwnerLoadable`自定义视图集成到另一个Storyboard的示例
  • 在下图中,所有蓝色方块视图都在Interface Builder中设置了自定义类MyCustomWidget
  • 选择这些自定义视图之一时,您可以直接访问该自定义视图公开的所有@IBOutlet,这允许在需要时将它们连接到Storyboard中的其他视图。
  • 选择这些自定义视图时,您还可以访问所有@IBInspectable属性。例如,在下面的截图中,您可以看到右侧面板上的“矩形颜色”和“文本”可检查属性,您可以直接从集成自定义小部件的Storyboard更改它们。

NibOwnerLoadable integrated in a Storyboard

3a. 自动加载`NibOwnerLoadable`视图的内容

如果您使用了`NibOwnerLoadable`并将您的自定义视图设置为XIB的“文件所有者”,则应覆盖`init?(coder:)以加载其关联的XIB作为子视图并自动添加约束

final class MyCustomWidget: UIView, NibOwnerLoadable {
  
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    self.loadNibContent()
  }
}

`self.loadNibContent()`是由`NibOwnerLoadable`混合提供的方法。它基本上从关联的`MyCustomWidget.xib`加载内容,然后将该XIB中的所有根视图添加为`MyCustomWidget`的子视图,并用适当的布局约束使它们与`MyCustomWidget`容器视图大小一致。

重写init?(coder:)并调用self.loadNibContent(),这样可以让系统在将MyCustomWidget包含在其他XIB或Storyboard(因为init?(coder:)是iOS在XIB或Storyboard中创建实例所调用的init方法)时,自动加载该内容。

💡注意:也可以类似地重写init(frame:),以便在需要时能够通过代码手动创建该视图实例。

3b. 实例化一个NibLoadable视图

如果您使用了NibLoadable并将自定义视图设置为您XIB的根视图(完全不使用文件的所有者),它们并不是设计用于在其他Storyboard或XIB中使用的,如NibOwnerLoadable,因为它们无法自动加载其内容。

相反,您可以通过代码实例化这些NibLoadable视图,这就像在您的自定义类上调用loadFromNib()一样简单。

let view1 = NibBasedRootView.loadFromNib() // Create one instance
let view2 = NibBasedRootView.loadFromNib() // Create another one
let view3 = NibBasedRootView.loadFromNib() // and another one

从Storyboard中获取类型安全的ViewController

Reusable还允许您标记您的UIViewController类为StoryboardBasedStoryboardSceneBased,以便以类型安全的方式轻松从关联的Storyboard中实例化它们。

1. 声明您的UIViewController以符合StoryboardBasedStoryboardSceneBased

在Swift源文件中声明您的自定义UIViewController

  • 如果您使用的*.storyboard文件与ViewController的类名相同,并且其场景为Storyboard的“初始场景”,则使用StoryboardBased协议。
    • 这通常在对每个ViewController使用一个Storyboard的情况下非常理想,例如。
  • 如果Storyboard中的场景具有与ViewController类名相同的sceneIdentifier,但*.storyboard文件名不必须与ViewController的类名匹配,则使用StoryboardSceneBased协议。
    • 这通常在大Storyboard的次要场景中非常理想
    • 然后您需要实现sceneStoryboard类型属性来表明它所属的Storyboard。
📑一个 ViewController 是其Storyboard初始 ViewController 的示例

在这个示例中,CustomVC被设计为名为CustomVC.storyboard的Storyboard的初始ViewController

final class CustomVC: UIViewController, StoryboardBased { /* and that's it! */ }
📑不同命名的Storyboard中的ViewController场景示例

在这个示例中,SecondaryVC是在名为CustomVC.storyboard的Storyboard中设计的(与类本身的名称不同),并且不是初始ViewController,而是将其“场景标识符”设置为"SecondaryVC"(与类名称相同)

如果遵守StoryboardSceneBased,您仍然需要实现static var sceneStoryboard: UIStoryboard { get }来指定设计此场景的Storyboard。通常可以使用一个let类型常量来实施此属性

final class SecondaryVC: UIViewController, StoryboardSceneBased {
  static let sceneStoryboard = UIStoryboard(name: "CustomVC", bundle: nil)
  /* and that's it! */
}

2. 实例化您的UIViewControllers

只需在您的自定义类上调用instantiate()方法。这将自动知道从哪个Storyboard加载它以及使用哪个场景(初始的或非初始的)来实例化它。

func presentSecondary() {
  let vc = SecondaryVC.instantiate() // Init from the "SecondaryVC" scene of CustomVC.storyboard
  self.present(vc, animated: true) {}
}

额外提示

使您的子类成为final

我建议您将自定义的UITableViewCellUICollectionViewCellUIViewUIViewController子类标记为final。这是因为

  • 在大多数情况下,您计划实例化的自定义单元格和VC不是打算要被子类化的。
  • 更重要的是,它大大有助于编译器,并为您提供了大的优化
  • 在符合具有Self要求的协议时可能会有所需要,比如这个库(ReusableStoryboardBased等)使用的协议。

在某些情况下,您可能不需要使您的类final,但总的来说,这是一个好习惯,并且在这个库的情况下,通常您的自定义UIViewController或无论什么都不会被子类化。

  • 要么它们被打算使用并直接实例化,并且永远不会被子类化,因此在这里final是有意义的
  • 如果您的自定义UIViewControllerUITableViewCell等被打算子类化并成为您应用中许多类的父类,那么将协议一致性(例如StoryboardBasedReusable等)添加到子类(并且将它们标记为final),比将其添加到父类、抽象类更有意义。

自定义 reuseIdentifier、nib 等非传统用途

该包中的协议,如 ReusableNibLoadableNibOwnerLoadableStoryboardBasedNibReusable 等,通常被称为 混入,它基本上是一个 Swift 协议,为所有方法提供了默认实现。

主要优点是 无需添加任何代码:只需遵循 ReusableNibOwnerLoadable 或这些协议中的任何一个,就可以开始使用而不需要编写额外的代码。

当然,那些提供的实现仅仅是 默认实现。这意味着如果你需要 还可以提供自己的实现,以防万一某些单元格没有遵循使用相同名称的典型配置,即类、reuseIdentifier 和 XIB 文件的相同名称。

final class VeryCustomNibBasedCell: UITableViewCell, NibReusable {
  // This cell use a non-standard configuration: its reuseIdentifier and XIB file
  // have a different name as the class itself. So we need to provide a custom implementation or `NibReusable`
  static var reuseIdentifier: String { return "VeryCustomReuseIdentifier" }
  static var nib: UINib { return UINib(nibName: "VeryCustomUI", bundle: nil) } // Use VeryCustomUI.xib
  
  // Then continue with the rest of your normal cell code 
}

这同样适用于该包中的所有协议,它们始终提供默认实现,但你可以在需要的情况下使用自己的实现。

但美妙之处在于,90% 的情况下,默认实现将符合典型约定,默认实现正是你想要的!

类型安全性和 fatalError

Reusable 允许你操作类型安全的 API 并避免拼写错误。但在某些配置错误的情况下,可能会出错,例如如果你忘记设置单元格的 reuseIdentifier 在其 XIB 中,或者你宣布 FooViewControllerStoryboardBased 但忘记在该 Storyboard 中的 FooViewController 场景设置初始 ViewController 标志等。

在这种情况下,因为这些都是开发错误,应该在开发过程中尽早捕捉,所以 Reusable 将调用 fatalError 使用尽可能详细的错误消息(而不是崩溃并带有关于某些强制转换或强制展开的晦涩难懂的消息)以帮助你正确配置。

例如,如果 Reusable 无法获取单元格,它将中断并以以下消息退出

「无法获取标识符为 \(cellType.reuseIdentifier) 且类型匹配 \(cellType.self) 的单元格。检查您的 XIB/Storyboard 中是否已正确设置 reuseIdentifier,并按要求预先注册单元格。」

希望这些明确的失败消息将帮助您了解哪些配置出错,并帮助您修复它!


示例项目

此仓库中包含了一个位于 Example/ 文件夹中的示例项目。您可以根据需要尝试它。

它展示了如何为

  • UITableViewCellUICollectionViewCell 子类使用 Reusable,
  • 这些单元格的 UI 模板要么只通过普通代码提供,要么由 XIB 提供,或者直接在 Storyboard 中原型化。
  • UICollectionViewSupplementaryViews(分区标题)
  • 在 XIB 中设计的自定义 UIViewNibOwnerLoadable

关于 Reusable 的讨论和文章

Reusable 的概念已在多篇文章和演讲中介绍过。

许可证

本代码在 MIT 许可下分发。有关更多信息,请参阅 LICENSE 文件。