IGListKit框架详细解析(二) —— 基于IGListKit框架的更好的UICollectionViews简单示例(一)

版本记录

版本号 时间
V1.0 2019.01.19 星期六

前言

IGListKit这个框架可能很多人没有听过,它其实就是一个数据驱动的UICollectionView框架,用于构建快速灵活的列表。它由Instagram开发,接下来这几篇我们就一起看一下这个框架。感兴趣的看上面几篇。
1. IGListKit框架详细解析(一) —— 基本概览(一)

开始

首先看下写作环境

Swift 4.2, iOS 12, Xcode 10

每个应用程序都以相同的方式启动:几个屏幕,一些按钮,也许一两个列表。 但随着时间的推移和应用程序的增长,功能开始逐渐涌入。在最后期限和产品经理的压力下,您的清洁数据源开始崩溃。 过了一会儿,你留下了大量的视图控制器废墟来维持。 幸运的是,有一个问题的解决方案!

在使用UICollectionView时,Instagram创建了IGListKit,使功能蠕变和大规模视图控制器成为过去。 通过使用IGListKit创建列表,您可以构建具有分离组件,快速更新和支持任何类型数据的应用程序。

在本教程中,您将使用IGListKit重构一个基本的UICollectionView,然后扩展应用程序!

您是美国宇航局顶级软件工程师之一,也是最新载人火星任务的工作人员。 该团队已经构建了Marslink应用程序的第一个版本。

打开已有工程Marslink.xcworkspace,然后构建并运行该应用程序。

IGListKit框架详细解析(二) —— 基于IGListKit框架的更好的UICollectionViews简单示例(一)_第1张图片

到目前为止,该应用程序只显示了一份宇航员日记条目列表。

您的任务是在工作人员需要时为此应用添加新功能。通过打开ClassicFeedViewController.swift并浏览一下,熟悉项目。

如果你曾经使用过UICollectionView,你看到的看起来非常标准:

  • ClassicFeedViewController是一个UIViewController子类,它在扩展中实现UICollectionViewDataSource
  • viewDidLoad()创建一个UICollectionView,注册单元格,设置数据源并将其添加到视图层次结构中。
  • loader.entries数组提供section的数量,每个section只有两个单元格(一个用于日期,一个用于文本)。
  • Date单元格包含日期文本的Sol date和文本Journal单元格。
  • collectionView(_:layout:sizeForItemAt :)返回日期单元格的固定大小,并计算实际条目的文本大小。

一切似乎工作得很好,但项目主管提出了一些紧急的产品更新请求:

一名宇航员刚刚被困在火星上。我们需要您添加天气模块和实时聊天。你有48小时。

IGListKit框架详细解析(二) —— 基于IGListKit框架的更好的UICollectionViews简单示例(一)_第2张图片

来自JPL的工程师可以使用其中一些系统,但他们需要您的帮助才能将它们添加到应用程序中。

如果将宇航员送回家的压力不足,NASA的首席设计师只是向您提出要求,即应用程序中每个子系统的更新都必须进行动画处理,这意味着没有reloadData()

IGListKit框架详细解析(二) —— 基于IGListKit框架的更好的UICollectionViews简单示例(一)_第3张图片

您应该如何将这些新模块集成到现有应用程序中并使所有过渡动画?


Introducing IGListKit

虽然UICollectionView是一个非常强大的工具,但强大的功能带来了巨大的责任。 保持数据源和视图同步至关重要,但如果断开连接通常会导致崩溃。

IGListKit是由Instagram团队构建的数据驱动的UICollectionView框架。 使用此框架,您可以提供要在UICollectionView中显示的对象数组。 对于每种类型的对象,适配器adapter都会创建一个称为节控制器section controller的东西,它具有创建单元格的所有细节。

IGListKit框架详细解析(二) —— 基于IGListKit框架的更好的UICollectionViews简单示例(一)_第4张图片

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的地方。

构建并运行并确保在屏幕上显示一个新的空视图控制器。

IGListKit框架详细解析(二) —— 基于IGListKit框架的更好的UICollectionViews简单示例(一)_第5张图片

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(),将collectionViewframe设置为与视图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是一个容纳适配器的UIViewControllerIGListKit稍后使用此视图控制器导航到其他视图控制器。
  • 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 controllerJPL工程师已经构建了一个JournalEntry模型,因此您需要创建一个可以处理它的节控制器。

右键单击SectionControllers组并选择New File。 创建一个名为JournalSectionController的新Cocoa Touch类,它是ListSectionController的子类。

IGListKit框架详细解析(二) —— 基于IGListKit框架的更好的UICollectionViews简单示例(一)_第6张图片

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框架详细解析(二) —— 基于IGListKit框架的更好的UICollectionViews简单示例(一)_第7张图片

后记

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

IGListKit框架详细解析(二) —— 基于IGListKit框架的更好的UICollectionViews简单示例(一)_第8张图片

你可能感兴趣的:(IGListKit框架详细解析(二) —— 基于IGListKit框架的更好的UICollectionViews简单示例(一))