一个 Swift mixin,用于以类型安全的方式使用 UITableViewCells
、UICollectionViewCells
和 UIViewControllers
,而无需操作其 String
类型的 reuseIdentifiers
。 该库还支持通过 XIB 加载任意 UIView
,只需简单调用 loadFromNib()
即可。
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 是 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 是一个去中心化的依赖管理器,用于将预构建的框架添加到您的 Cocoa 应用程序。
要使用 Carthage 将 Reusable 集成到您的 Xcode 项目中,请在您的 Cartfile
中指定它
github "AliSoftware/Reusable"
然后运行 carthage update --use-xcframeworks
CocoaPods 是一个依赖管理器,用于自动将框架集成到您的 Swift 和 Objective-C Cocoa 项目中。
要使用 Cocoapods 将 Reusable 集成到您的 Xcode 项目中,请在您的 Podfile
中指定它
pod 'Reusable'
该库旨在使创建、出列和实例化可重用视图变得非常容易,无论在何处使用此模式:从显而易见的 UITableViewCell
和 UICollectionViewCell
到自定义 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(一个协议,为其所有方法提供默认实现),在 我的博客文章 中有详细解释。
目录
✍️ 以下示例和解释使用
UITableView
和UITableViewCell
,但完全相同的示例和解释适用于UICollectionView
和UICollectionViewCell
。
Reusable
协议(这将使用 registerClass(…)
注册单元格)XIB
文件作为其内容,请使用 NibReusable
类型别名(= Reusable & NibLoadable
)(这将使用 registerNib(…)
注册单元格)final class CustomCell: UITableViewCell, Reusable { /* And that's it! */ }
✍️ 注意
- 对于嵌入在 Storyboard 的 tableView 中的单元格,这两个协议中的任何一个都可以工作(因为您无论如何都不需要手动注册单元格,因为注册由 storyboard 自动处理)
- 如果您创建一个基于 XIB 的单元格,请不要忘记在 Interface Builder 中将其重用标识符字段设置为与单元格类本身名称相同的字符串。
- 💡
NibReusable
是一个类型别名,因此您仍然可以使用两个协议一致性Reusable, NibLoadable
而不是NibReusable
。
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 }
}
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 }
}
// 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
}
// 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
}
除非您已在 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)
}
}
要出列单元格(通常在您的 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
}
}
}
现在您拥有的是漂亮的代码和类型安全的单元格,具有编译时类型检查,并且不再是基于字符串的 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 }
Reusable
还允许您创建在 Interface Builder 中设计的可重用自定义视图,以便在其他 XIB 或 Storyboard 中或通过代码重复使用它们。 这允许您将这些视图视为自定义 UI 小部件,可以在应用程序中的多个位置使用。
在声明您的自定义视图类的 swift 源代码中
NibLoadable
协议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 加载其自身的内容,而无需编写额外的代码来手动加载自定义视图的内容。
例如,如果您将您的类命名为 MyCustomWidget
并使其为 NibOwnerLoadable
MyCustomWidget
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 上放置一个 UIView
,并在 IB 的检查器中将其类更改为 MyCustomWidget
,将该小部件集成到 Storyboard 场景(或任何其他 XIB)中。
NibOwnerLoadable
自定义视图的示例MyCustomWidget
的自定义类。MyCustomWidget
公开的所有 @IBOutlet
,这允许您将它们连接到 Storyboard 的其他视图(如果需要)@IBInspectable
属性。 例如,在下面的截图中,您可以在右侧面板上看到“Rect color”和“Text”的可检查属性,您可以直接从集成您的自定义小部件的 Storyboard 中更改它们。如果您使用 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:)
,以便如果需要,也可以通过代码手动创建该视图的实例。
如果您使用 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
…
Reusable
还允许您将 UIViewController
类标记为 StoryboardBased
或 StoryboardSceneBased
,以便以类型安全的方式轻松地从其关联的 Storyboard 实例化它们。
在声明您的自定义 UIViewController
类的 swift 源代码中
*.storyboard
文件的名称与 ViewController 的类的名称相同,并且其场景是 storyboard 的“初始场景”,请使用 StoryboardBased
协议。sceneIdentifier
,但 *.storyboard
文件名不一定与 ViewController 的类名匹配,请使用 StoryboardSceneBased
协议。sceneStoryboard
类型属性以指示它所属的 storyboard。在此示例中,CustomVC
被设计为名为 CustomVC.storyboard
的 Storyboard 的初始 ViewController
final class CustomVC: UIViewController, StoryboardBased { /* and that's it! */ }
在这个例子中,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! */
}
只需在你的自定义类上调用 instantiate()
。这将自动知道要从哪个 Storyboard 加载它,以及使用哪个场景(初始或非初始)来实例化它。
func presentSecondary() {
let vc = SecondaryVC.instantiate() // Init from the "SecondaryVC" scene of CustomVC.storyboard
self.present(vc, animated: true) {}
}
我建议你将你的自定义 UITableViewCell
、UICollectionViewCell
、UIView
和 UIViewController
子类标记为 final
。这是因为:
Self
要求的 protocols
时,可能需要这样做,就像此 pod 使用的协议(Reusable
、StoryboardBased
、…)。在某些情况下,你可以避免使你的类成为 final
,但总的来说,这是一个好习惯,而且就此 pod 而言,通常你的自定义 UIViewController
或任何东西都不会被继承。
final
在这里是有意义的。UIViewController
、UITableViewCell
等… 旨在被继承并且是你应用程序中许多类的父类,那么将协议一致性(StoryboardBased
、Reusable
、…)添加到子类(并将它们标记为 final
)比在父类(抽象类)上添加协议更有意义。此 pod 中的协议,如 Reusable
、NibLoadable
、NibOwnerLoadable
、StoryboardBased
、NibReusable
… 通常被称为 Mixins,基本上是一个 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
}
对于此 pod 的所有协议也是如此,它们总是提供默认实现,如果需要一些自定义情况,仍然可以替换为自己的实现。
但美妙之处在于,在 90% 的情况下,默认实现将匹配典型的约定,并且默认实现将完全是你想要的!
Reusable
允许你操作类型安全的 API,并避免你出现拼写错误。但是,如果配置错误,例如,如果你忘记在单元格的 XIB
中设置 reuseIdentifier
,或者你声明一个 FooViewController
为 StoryboardBased
,但忘记在该 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
如何用于:
UITableViewCell
和 UICollectionViewCell
子类,UICollectionView
的 SupplementaryViews
(section Headers)UIView
(NibOwnerLoadable
)Reusable 背后的概念已在各种文章和演讲中介绍过
此代码在 MIT 许可证下分发。有关更多信息,请参阅 LICENSE
文件。