Reusable
一个Swift mixin,以强类型方式使用UITableViewCells
、UICollectionViewCells
和UIViewControllers
,无需操作它们的字符串类型reuseIdentifiers
。此库还支持使用简单的调用loadFromNib()
加载通过XIB加载的任意UIView
。
安装
要求:对于每个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
类型安全的
✍️ 下面的例子和解释将使用UITableView
和UITableViewCell
,但对UICollectionView
和UICollectionViewCell
也同样适用。
Reusable
或 NibReusable
1. 声明你的 cells 遵循 - 使用
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 theVariable
或let 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小部件。
NibLoadable
或NibOwnerLoadable
1. 声明您的视图以符合在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`的视图
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更改它们。
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:)
,以便在需要时能够通过代码手动创建该视图实例。
NibLoadable
视图
3b. 实例化一个如果您使用了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
类为StoryboardBased
或StoryboardSceneBased
,以便以类型安全的方式轻松从关联的Storyboard中实例化它们。
UIViewController
以符合StoryboardBased
或StoryboardSceneBased
1. 声明您的在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
使您的子类成为我建议您将自定义的UITableViewCell
、UICollectionViewCell
、UIView
和UIViewController
子类标记为final
。这是因为
- 在大多数情况下,您计划实例化的自定义单元格和VC不是打算要被子类化的。
- 更重要的是,它大大有助于编译器,并为您提供了大的优化
- 在符合具有
Self
要求的协议时可能会有所需要,比如这个库(Reusable
、StoryboardBased
等)使用的协议。
在某些情况下,您可能不需要使您的类final
,但总的来说,这是一个好习惯,并且在这个库的情况下,通常您的自定义UIViewController
或无论什么都不会被子类化。
- 要么它们被打算使用并直接实例化,并且永远不会被子类化,因此在这里
final
是有意义的 - 如果您的自定义
UIViewController
、UITableViewCell
等被打算子类化并成为您应用中许多类的父类,那么将协议一致性(例如StoryboardBased
、Reusable
等)添加到子类(并且将它们标记为final
),比将其添加到父类、抽象类更有意义。
自定义 reuseIdentifier、nib 等非传统用途
该包中的协议,如 Reusable
、NibLoadable
、NibOwnerLoadable
、StoryboardBased
、NibReusable
等,通常被称为 混入,它基本上是一个 Swift 协议,为所有方法提供了默认实现。
主要优点是 无需添加任何代码:只需遵循 Reusable
、NibOwnerLoadable
或这些协议中的任何一个,就可以开始使用而不需要编写额外的代码。
当然,那些提供的实现仅仅是 默认实现。这意味着如果你需要 还可以提供自己的实现,以防万一某些单元格没有遵循使用相同名称的典型配置,即类、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
中,或者你宣布 FooViewController
为 StoryboardBased
但忘记在该 Storyboard 中的 FooViewController
场景设置初始 ViewController 标志等。
在这种情况下,因为这些都是开发错误,应该在开发过程中尽早捕捉,所以 Reusable
将调用 fatalError
使用尽可能详细的错误消息(而不是崩溃并带有关于某些强制转换或强制展开的晦涩难懂的消息)以帮助你正确配置。
例如,如果 Reusable
无法获取单元格,它将中断并以以下消息退出
「无法获取标识符为 \(cellType.reuseIdentifier) 且类型匹配 \(cellType.self) 的单元格。检查您的 XIB/Storyboard 中是否已正确设置 reuseIdentifier,并按要求预先注册单元格。」
希望这些明确的失败消息将帮助您了解哪些配置出错,并帮助您修复它!
示例项目
此仓库中包含了一个位于 Example/
文件夹中的示例项目。您可以根据需要尝试它。
它展示了如何为
UITableViewCell
和UICollectionViewCell
子类使用Reusable
,- 这些单元格的 UI 模板要么只通过普通代码提供,要么由 XIB 提供,或者直接在 Storyboard 中原型化。
UICollectionView
的SupplementaryViews
(分区标题)- 在 XIB 中设计的自定义
UIView
(NibOwnerLoadable
)
关于 Reusable 的讨论和文章
Reusable 的概念已在多篇文章和演讲中介绍过。
- 在我的博客上关于使用泛型改进 TableView cells 文章:Using Generics to improve TableView cells
- FrenchKit'16 演讲:Mixins over Inheritance(视频)
- 同样的演讲还曾在 NSSpain'16(幻灯片)和 AppDevCon'17(幻灯片)上做过
- 在 iOS 开发中不再需要字符串类型的实例化
许可证
本代码在 MIT 许可下分发。有关更多信息,请参阅 LICENSE
文件。