可复用

Reusable

一个 Swift mixin,用于以类型安全的方式使用 UITableViewCellsUICollectionViewCellsUIViewControllers,而无需操作其 String 类型的 reuseIdentifiers。 该库还支持通过 XIB 加载任意 UIView,只需简单调用 loadFromNib() 即可。

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 编译,只有在使用 Carthage 进行集成时才需要 4.0.2+

可以使用以下选项之一将 Reusable 集成到您的 Xcode 项目中

Swift Package Manager (SPM) 安装说明

Swift Package Manager 是 Apple 的去中心化依赖管理器,用于将库集成到您的 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"

然后运行 carthage update --use-xcframeworks

CocoaPods 安装说明

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

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

pod 'Reusable'

介绍

该库旨在使创建、出列和实例化可重用视图变得非常容易,无论在何处使用此模式:从显而易见的 UITableViewCellUICollectionViewCell 到自定义 UIViews,甚至支持 Storyboard 中的 UIViewControllers
所有这些只需将您的类标记为符合协议,而无需添加任何代码,并创建一个类型安全的 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. 声明您的单元格符合 ReusableNibReusable

final class CustomCell: UITableViewCell, Reusable { /* And that's it! */ }

✍️ 注意

📑 基于代码的自定义 tableView 单元格示例
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 单元格示例
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 单元格示例
// 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 单元格示例
// 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。

为此,只需调用

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,以及要返回的确切类型,从而节省您的类型转换。

📑 使用 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
    }
  }
}

现在您拥有的是漂亮的代码和类型安全的单元格,具有编译时类型检查,并且不再是基于字符串的 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 源代码中

// 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

🖼📑 配置为 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 上放置一个 UIView,并在 IB 的检查器中将其类更改为 MyCustomWidget,将该小部件集成到 Storyboard 场景(或任何其他 XIB)中。

🖼 一旦集成到另一个 Storyboard 中,NibOwnerLoadable 自定义视图的示例

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 mixin 提供的方法。 它基本上从关联的 MyCustomWidget.xib 加载内容,然后将该 XIB 中的所有根视图添加为您的 MyCustomWidget 的子视图,并使用适当的布局约束使其与您的 MyCustomWidget 容器视图具有相同的大小。

因此,重写 init?(coder:) 并调用 self.loadNibContent() 允许系统在另一个 XIB 或 Storyboard 中包含该 MyCustomWidget 时自动加载该内容(因为 init?(coder:) 是 iOS 调用以在 XIB 或 Storyboard 中创建这些实例的 init

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

3b. 实例化 NibLoadable 视图

如果您使用 NibLoadable 并使您的自定义视图成为 XIB 的根视图(根本不使用文件所有者),则这些视图不设计为像 NibOwnerLoadable 那样在其他 Storyboard 或 XIB 中使用,因为它们将无法自动加载其内容。

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

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

来自 Storyboard 的类型安全的 ViewControllers

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

1. 声明您的 UIViewController 符合 StoryboardBasedStoryboardSceneBased

在声明您的自定义 UIViewController 类的 swift 源代码中

📑 ViewController 是其 Storyboard 的初始 ViewController 的示例

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

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

在这个例子中,SecondaryVC 是在一个名为 CustomVC.storyboard 的 Storyboard 中设计的(因此名称与类本身不同),并且不是初始 ViewController,而是将其"Scene Identifier"(场景标识符)设置为值 "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。这是因为:

在某些情况下,你可以避免使你的类成为 final,但总的来说,这是一个好习惯,而且就此 pod 而言,通常你的自定义 UIViewController 或任何东西都不会被继承。

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

此 pod 中的协议,如 ReusableNibLoadableNibOwnerLoadableStoryboardBasedNibReusable… 通常被称为 Mixins,基本上是一个 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 
}

对于此 pod 的所有协议也是如此,它们总是提供默认实现,如果需要一些自定义情况,仍然可以替换为自己的实现。

但美妙之处在于,在 90% 的情况下,默认实现将匹配典型的约定,并且默认实现将完全是你想要的!

类型安全和 fatalError

Reusable 允许你操作类型安全的 API,并避免你出现拼写错误。但是,如果配置错误,例如,如果你忘记在单元格的 XIB 中设置 reuseIdentifier,或者你声明一个 FooViewControllerStoryboardBased,但忘记在该 Storyboard 中的 FooViewController 场景上设置初始 ViewController 标志等等,事情仍然可能会出错。

在这种情况下,因为这些是开发人员错误,应该在开发过程中尽早发现,所以 Reusable 将调用 fatalError并提供尽可能具有描述性的错误消息(而不是因为一些强制转换或强制解包之类的模糊消息而崩溃),以帮助你正确配置它。

例如,如果 Reusable 无法出列一个单元格,它会抛出一个如下消息:

« Failed to dequeue a cell with identifier \(cellType.reuseIdentifier) matching type \(cellType.self). Check that the reuseIdentifier is set properly in your XIB/Storyboard and that you registered the cell beforehand. » (无法出列一个具有标识符 \(cellType.reuseIdentifier) 且类型与 \(cellType.self) 匹配的单元格。检查 reuseIdentifier 是否在你的 XIB/Storyboard 中正确设置,以及你是否事先注册了该单元格。)

希望这些明确的失败消息能让你明白哪些地方配置错误,并帮助你修复它!


示例项目

此存储库在 Example/ 文件夹中附带了一个示例项目。请随意尝试它。

它演示了 Reusable 如何用于:

关于 Reusable 的演讲和文章

Reusable 背后的概念已在各种文章和演讲中介绍过

许可证

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