版本记录
版本号 | 时间 |
---|---|
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简单示例,感兴趣的给个赞或者关注~~~