版本记录
| 版本号 | 时间 |
|---|---|
| V1.0 | 2019.01.19 星期六 |
前言
IGListKit这个框架可能很多人没有听过,它其实就是一个数据驱动的UICollectionView框架,用于构建快速灵活的列表。它由
1. IGListKit框架详细解析(一) —— 基本概览(一)
开始
首先看下写作环境
Swift 4.2, iOS 12, Xcode 10
每个应用程序都以相同的方式启动:几个屏幕,一些按钮,也许一两个列表。 但随着时间的推移和应用程序的增长,功能开始逐渐涌入。在最后期限和产品经理的压力下,您的清洁数据源开始崩溃。 过了一会儿,你留下了大量的视图控制器废墟来维持。 幸运的是,有一个问题的解决方案!
在使用UICollectionView时,Instagram创建了IGListKit,使功能蠕变和大规模视图控制器成为过去。 通过使用IGListKit创建列表,您可以构建具有分离组件,快速更新和支持任何类型数据的应用程序。
在本教程中,您将使用IGListKit重构一个基本的UICollectionView,然后扩展应用程序!
您是美国宇航局顶级软件工程师之一,也是最新载人火星任务的工作人员。 该团队已经构建了Marslink应用程序的第一个版本。
打开已有工程Marslink.xcworkspace,然后构建并运行该应用程序。

到目前为止,该应用程序只显示了一份宇航员日记条目列表。
您的任务是在工作人员需要时为此应用添加新功能。通过打开ClassicFeedViewController.swift并浏览一下,熟悉项目。
如果你曾经使用过UICollectionView,你看到的看起来非常标准:
-
ClassicFeedViewController是一个UIViewController子类,它在扩展中实现UICollectionViewDataSource。 -
viewDidLoad()创建一个UICollectionView,注册单元格,设置数据源并将其添加到视图层次结构中。 -
loader.entries数组提供section的数量,每个section只有两个单元格(一个用于日期,一个用于文本)。 -
Date单元格包含日期文本的Sol date和文本Journal单元格。 -
collectionView(_:layout:sizeForItemAt :)返回日期单元格的固定大小,并计算实际条目的文本大小。
一切似乎工作得很好,但项目主管提出了一些紧急的产品更新请求:
一名宇航员刚刚被困在火星上。我们需要您添加天气模块和实时聊天。你有48小时。

来自JPL的工程师可以使用其中一些系统,但他们需要您的帮助才能将它们添加到应用程序中。
如果将宇航员送回家的压力不足,NASA的首席设计师只是向您提出要求,即应用程序中每个子系统的更新都必须进行动画处理,这意味着没有reloadData()。

您应该如何将这些新模块集成到现有应用程序中并使所有过渡动画?
Introducing IGListKit
虽然UICollectionView是一个非常强大的工具,但强大的功能带来了巨大的责任。 保持数据源和视图同步至关重要,但如果断开连接通常会导致崩溃。
IGListKit是由Instagram团队构建的数据驱动的UICollectionView框架。 使用此框架,您可以提供要在UICollectionView中显示的对象数组。 对于每种类型的对象,适配器adapter都会创建一个称为节控制器section controller的东西,它具有创建单元格的所有细节。

IGListKit会自动对您的对象进行区分,并在UICollectionView上执行动画批量更新以进行更改。 这样您就不必自己编写批量更新,从而避免在此处here的警告中列出的问题。
Adding IGListKit to a UICollectionView
IGListKit完成了识别集合中的更改以及使用动画更新相应行的所有艰苦工作。 它的结构也可以轻松处理具有不同数据和UI的多个部分。 考虑到这一点,它是新一批处理要求的完美解决方案 - 因此是时候开始实施它了!
在Marslink.xcworkspace仍然打开的情况下,右键单击ViewControllers组并选择New File。 添加一个新的Cocoa Touch Class,它将UIViewController的子类名为FeedViewController,并确保将语言设置为Swift。
打开AppDelegate.swift并找到application(_:didFinishLaunchingWithOptions:)。 找到将ClassicFeedViewController()推送到导航控制器的行,并将其替换为:
nav.pushViewController(FeedViewController(), animated: false)
FeedViewController现在是根视图控制器。 您将保留ClassicFeedViewController.swift作为参考,但FeedViewController是您将实现新的IGListKit驱动的collection view的地方。
构建并运行并确保在屏幕上显示一个新的空视图控制器。

1. Adding the Journal Loader
打开FeedViewController.swift并将以下属性添加到FeedViewController的顶部:
let loader = JournalEntryLoader()
JournalEntryLoader是一个将硬编码日记条目加载到entries数组中的类。
将以下内容添加到viewDidLoad()的底部:
loader.loadLatest()
loadLatest()是一个JournalEntryLoader方法,用于加载最新的日记帐分录。
2. Adding the Collection View
是时候开始向视图控制器添加一些IGListKit特定的控件了。 在此之前,您需要导入框架。 在FeedViewController.swift的顶部附近,添加一个新的import:
import IGListKit
注意:本教程中的项目使用
CocoaPods来管理依赖项。IGListKit是用Objective-C编写的,因此如果手动将其添加到项目中,则需要将#import插入到桥接头 bridging header中。
将初始化的collectionView常量添加到FeedViewController的顶部:
// 1
let collectionView: UICollectionView = {
// 2
let view = UICollectionView(
frame: .zero,
collectionViewLayout: UICollectionViewFlowLayout())
// 3
view.backgroundColor = .black
return view
}()
这是代码的作用:
- 1)
IGListKit使用常规的UICollectionView并在其上添加自己的功能,稍后您将看到。 - 2) 从零大小的
rect开始,因为尚未创建视图。 它像ClassicFeedViewController一样使用UICollectionViewFlowLayout。 - 3) 将背景颜色设置为
NASA认可的黑色。
将以下内容添加到viewDidLoad()的底部:
view.addSubview(collectionView)
这会将新的collectionView添加到控制器的视图中。
在viewDidLoad()下面,添加以下内容:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.frame = view.bounds
}
这将重写viewDidLayoutSubviews(),将collectionView的frame设置为与视图bounds相同。
3. ListAdapter and Data Source
使用UICollectionView,您需要某种采用UICollectionViewDataSource的数据源。 它的工作是返回section and row数以及单个单元格。
在IGListKit中,您使用ListAdapter来控制集合视图。 您仍然需要一个符合协议ListAdapterDataSource的数据源,但不是返回计数和单元格,而是提供数组和节控制器(section controllers)(稍后将详细介绍)。
对于初学者,在FeedViewController.swift中,在FeedViewController的顶部添加以下内容:
lazy var adapter: ListAdapter = {
return ListAdapter(
updater: ListAdapterUpdater(),
viewController: self,
workingRangeSize: 0)
}()
这将为ListAdapter创建一个初始化变量。 初始化程序需要三个参数:
- 1)
updater是符合ListUpdatingDelegate的对象,它处理row and section更新。ListAdapterUpdater是一个适合您使用的默认实现。 - 2)
viewController是一个容纳适配器的UIViewController。IGListKit稍后使用此视图控制器导航到其他视图控制器。 - 3)
workingRangeSize是working range的大小,允许您为可见框外部的部分准备内容。
注意:工作范围
Working ranges是本教程未涵盖的更高级主题。 然而,IGListKit repo中有大量文档甚至是一个示例应用程序!
将以下内容添加到viewDidLoad()的底部:
adapter.collectionView = collectionView
adapter.dataSource = self
这将collectionView连接到适配器adapter。 它还将self设置为适配器的dataSource - 导致编译器错误,因为您尚未符合ListAdapterDataSource。
通过扩展FeedViewController以采用ListAdapterDataSource来解决此问题。 将以下内容添加到文件的底部:
// MARK: - ListAdapterDataSource
extension FeedViewController: ListAdapterDataSource {
// 1
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return loader.entries
}
// 2
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any)
-> ListSectionController {
return ListSectionController()
}
// 3
func emptyView(for listAdapter: ListAdapter) -> UIView? {
return nil
}
}
注意:
IGListKit大量使用所需的协议方法。 即使你可能最终得到空方法,或者返回nil的方法,你也不必担心默默地丢失方法或者对抗动态运行时。 它使得使用IGListKit非常困难。
FeedViewController现在遵循ListAdapterDataSource并实现其三个必需的方法:
- 1)
objects(for :)返回应显示在集合视图中的数据对象数组。 您在此处提供loader.entries,因为它包含日记帐分录。 - 2) 对于每个数据对象,
listAdapter(_:sectionControllerFor :)必须返回一个节控制器section controller的新实例。 现在你要返回一个普通的ListSectionController来让编译器不报错。 稍后,您将修改此项以返回自定义日记记录section controller。 - 3)
emptyView(for :)返回一个视图,当列表为空时显示。 美国宇航局有点紧张,所以他们没有预算这个功能。
4. Creating Your First Section Controller
section controller是一种抽象,在给定数据对象的情况下,它在集合视图的一section中配置和控制单元。 此概念类似于用于配置视图的视图模型view-model:数据对象是视图模型,单元格是视图。 section controller充当两者之间的粘合剂。
在IGListKit中,您可以为不同类型的数据和行为创建新的section controller。 JPL工程师已经构建了一个JournalEntry模型,因此您需要创建一个可以处理它的节控制器。
右键单击SectionControllers组并选择New File。 创建一个名为JournalSectionController的新Cocoa Touch类,它是ListSectionController的子类。

Xcode不会自动导入第三方框架,因此在JournalSectionController.swift中,在顶部添加一行:
import IGListKit
将以下属性添加到JournalSectionController的顶部:
var entry: JournalEntry!
let solFormatter = SolFormatter()
JournalEntry是您在实现数据源时将使用的模型类。 SolFormatter类提供将日期转换为Sol格式的方法。 你很快就会需要两个。
同样在JournalSectionController中,通过添加以下内容来重写init():
override init() {
super.init()
inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
}
如果没有这个,sections之间的单元格将彼此相邻。 这会在JournalSectionController对象的底部添加15点填充。
您的节控制器需要重写ListSectionController中的四个方法,以提供适配器使用的实际数据。
将以下扩展添加到文件的底部:
// MARK: - Data Provider
extension JournalSectionController {
override func numberOfItems() -> Int {
return 2
}
override func sizeForItem(at index: Int) -> CGSize {
return .zero
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
return UICollectionViewCell()
}
override func didUpdate(to object: Any) {
}
}
除了numberOfItems()之外,所有方法都是存根实现,它只是为日期和文本对返回2。 如果您回顾ClassicFeedViewController.swift,您会注意到在collectionView(_:numberOfItemsInSection :)中每个部分也返回2个项目。 这基本上是一回事!
在didUpdate(to :)中,添加以下内容:
entry = object as? JournalEntry
IGListKit调用didUpdate(to :)将对象传递给节控制器(section controller.)。 请注意,在任何单元协议方法之前始终调用此方法。 在这里,您将传入的对象保存在entry中。
注意:对象在段控制器的生命周期内可以多次更改。 只有当您开始解锁IGListKit的更高级功能(例如custom model diffing)时才会发生这种情况。 您不必担心本教程中的差异。
现在您有了一些数据,您可以开始配置您的单元格。 用以下代码替换cellForItem(at :)的占位符实现:
// 1
let cellClass: AnyClass = index == 0 ? JournalEntryDateCell.self : JournalEntryCell.self
// 2
let cell = collectionContext!.dequeueReusableCell(of: cellClass, for: self, at: index)
// 3
if let cell = cell as? JournalEntryDateCell {
cell.label.text = "SOL \(solFormatter.sols(fromDate: entry.date))"
} else if let cell = cell as? JournalEntryCell {
cell.label.text = entry.text
}
return cell
IGListKit在需要section中给定索引的单元格时调用cellForItem(at :)。 以下是代码的工作原理:
- 1) 如果索引是第一个,请使用
JournalEntryDateCell单元格,否则使用JournalEntryCell单元格。Journal entries始终显示日期后跟文本。 - 2) 使用单元类,
section controller(self)和索引将单元从重用池中出列。 - 3) 根据单元格类型,使用您之前在
didUpdate(to object:)中设置的JournalEntry进行配置。
接下来,使用以下内容替换sizeForItem(at :)的占位符实现:
// 1
guard
let context = collectionContext,
let entry = entry
else {
return .zero
}
// 2
let width = context.containerSize.width
// 3
if index == 0 {
return CGSize(width: width, height: 30)
} else {
return JournalEntryCell.cellSize(width: width, text: entry.text)
}
这段代码的工作原理:
- 1)
collectionContext是一个weak变量,必须是nullable。虽然它永远不应该是nil,但最好采取预防措施,而Swift guard就是这么简单。 - 2)
ListCollectionContext是一个上下文对象,其中包含有关使用节控制器的适配器,集合视图和视图控制器的信息。在这里你可以得到容器的宽度。 - 3) 如果是第一个索引(日期单元格),则返回与容器一样宽的大小和30个高点。否则,使用单元格帮助程序方法计算单元格的动态文本大小。
如果您之前使用过UICollectionView,这种将不同类型的单元格出列,配置和返回大小的模式应该都会让您感到熟悉。同样,您可以参考ClassicFeedViewController并看到很多此代码几乎完全相同。
现在,您有一个section controller,它接收一个JournalEntry对象并返回并调整两个单元格的大小。是时候将它们整合在一起了。
回到FeedViewController.swift,用以下内容替换listAdapter(_:sectionControllerFor :)的内容:
return JournalSectionController()
只要IGListKit调用此方法,它就会返回新的journal section controller。
构建并运行应用程序。 您应该看到日记帐分录列表:

后记
本篇主要简单介绍了基于IGListKit框架的更好的UICollectionViews简单示例,感兴趣的给个赞或者关注~~~

