版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2019.01.19 星期六 |
前言
IGListKit
這個框架可能很多人沒有聽過,它其實就是一個數(shù)據(jù)驅(qū)動的UICollectionView
框架,用于構建快速靈活的列表。它由
1. IGListKit框架詳細解析(一) —— 基本概覽(一)
開始
首先看下寫作環(huán)境
Swift 4.2, iOS 12, Xcode 10
每個應用程序都以相同的方式啟動:幾個屏幕,一些按鈕,也許一兩個列表。 但隨著時間的推移和應用程序的增長,功能開始逐漸涌入。在最后期限和產(chǎn)品經(jīng)理的壓力下,您的清潔數(shù)據(jù)源開始崩潰。 過了一會兒,你留下了大量的視圖控制器廢墟來維持。 幸運的是,有一個問題的解決方案!
在使用UICollectionView
時,Instagram
創(chuàng)建了IGListKit,使功能蠕變和大規(guī)模視圖控制器成為過去。 通過使用IGListKit創(chuàng)建列表,您可以構建具有分離組件,快速更新和支持任何類型數(shù)據(jù)的應用程序。
在本教程中,您將使用IGListKit重構一個基本的UICollectionView
,然后擴展應用程序!
您是美國宇航局頂級軟件工程師之一,也是最新載人火星任務的工作人員。 該團隊已經(jīng)構建了Marslink
應用程序的第一個版本。
打開已有工程Marslink.xcworkspace
,然后構建并運行該應用程序。
到目前為止,該應用程序只顯示了一份宇航員日記條目列表。
您的任務是在工作人員需要時為此應用添加新功能。通過打開ClassicFeedViewController.swift
并瀏覽一下,熟悉項目。
如果你曾經(jīng)使用過UICollectionView
,你看到的看起來非常標準:
-
ClassicFeedViewController
是一個UIViewController
子類,它在擴展中實現(xiàn)UICollectionViewDataSource
。 -
viewDidLoad()
創(chuàng)建一個UICollectionView
,注冊單元格,設置數(shù)據(jù)源并將其添加到視圖層次結構中。 -
loader.entries
數(shù)組提供section
的數(shù)量,每個section
只有兩個單元格(一個用于日期,一個用于文本)。 -
Date
單元格包含日期文本的Sol date和文本Journal
單元格。 -
collectionView(_:layout:sizeForItemAt :)
返回日期單元格的固定大小,并計算實際條目的文本大小。
一切似乎工作得很好,但項目主管提出了一些緊急的產(chǎn)品更新請求:
一名宇航員剛剛被困在火星上。我們需要您添加天氣模塊和實時聊天。你有48小時。
來自JPL的工程師可以使用其中一些系統(tǒng),但他們需要您的幫助才能將它們添加到應用程序中。
如果將宇航員送回家的壓力不足,NASA的首席設計師只是向您提出要求,即應用程序中每個子系統(tǒng)的更新都必須進行動畫處理,這意味著沒有reloadData()
。
您應該如何將這些新模塊集成到現(xiàn)有應用程序中并使所有過渡動畫?
Introducing IGListKit
雖然UICollectionView是一個非常強大的工具,但強大的功能帶來了巨大的責任。 保持數(shù)據(jù)源和視圖同步至關重要,但如果斷開連接通常會導致崩潰。
IGListKit是由Instagram
團隊構建的數(shù)據(jù)驅(qū)動的UICollectionView
框架。 使用此框架,您可以提供要在UICollectionView
中顯示的對象數(shù)組。 對于每種類型的對象,適配器adapter
都會創(chuàng)建一個稱為節(jié)控制器section controller
的東西,它具有創(chuàng)建單元格的所有細節(jié)。
IGListKit
會自動對您的對象進行區(qū)分,并在UICollectionView
上執(zhí)行動畫批量更新以進行更改。 這樣您就不必自己編寫批量更新,從而避免在此處here的警告中列出的問題。
Adding IGListKit to a UICollectionView
IGListKit
完成了識別集合中的更改以及使用動畫更新相應行的所有艱苦工作。 它的結構也可以輕松處理具有不同數(shù)據(jù)和UI的多個部分。 考慮到這一點,它是新一批處理要求的完美解決方案 - 因此是時候開始實施它了!
在Marslink.xcworkspace
仍然打開的情況下,右鍵單擊ViewControllers
組并選擇New File
。 添加一個新的Cocoa Touch Class
,它將UIViewController
的子類名為FeedViewController
,并確保將語言設置為Swift
。
打開AppDelegate.swift
并找到application(_:didFinishLaunchingWithOptions:)
。 找到將ClassicFeedViewController()
推送到導航控制器的行,并將其替換為:
nav.pushViewController(FeedViewController(), animated: false)
FeedViewController
現(xiàn)在是根視圖控制器。 您將保留ClassicFeedViewController.swift
作為參考,但FeedViewController
是您將實現(xiàn)新的IGListKit
驅(qū)動的collection view
的地方。
構建并運行并確保在屏幕上顯示一個新的空視圖控制器。
1. Adding the Journal Loader
打開FeedViewController.swift
并將以下屬性添加到FeedViewController
的頂部:
let loader = JournalEntryLoader()
JournalEntryLoader
是一個將硬編碼日記條目加載到entries
數(shù)組中的類。
將以下內(nèi)容添加到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
使用常規(guī)的UICollectionView
并在其上添加自己的功能,稍后您將看到。 - 2) 從零大小的
rect
開始,因為尚未創(chuàng)建視圖。 它像ClassicFeedViewController
一樣使用UICollectionViewFlowLayout
。 - 3) 將背景顏色設置為
NASA
認可的黑色。
將以下內(nèi)容添加到viewDidLoad()
的底部:
view.addSubview(collectionView)
這會將新的collectionView
添加到控制器的視圖中。
在viewDidLoad()
下面,添加以下內(nèi)容:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.frame = view.bounds
}
這將重寫viewDidLayoutSubviews()
,將collectionView
的frame
設置為與視圖bounds
相同。
3. ListAdapter and Data Source
使用UICollectionView
,您需要某種采用UICollectionViewDataSource
的數(shù)據(jù)源。 它的工作是返回section and row
數(shù)以及單個單元格。
在IGListKit
中,您使用ListAdapter
來控制集合視圖。 您仍然需要一個符合協(xié)議ListAdapterDataSource
的數(shù)據(jù)源,但不是返回計數(shù)和單元格,而是提供數(shù)組和節(jié)控制器(section controllers)
(稍后將詳細介紹)。
對于初學者,在FeedViewController.swift
中,在FeedViewController
的頂部添加以下內(nèi)容:
lazy var adapter: ListAdapter = {
return ListAdapter(
updater: ListAdapterUpdater(),
viewController: self,
workingRangeSize: 0)
}()
這將為ListAdapter
創(chuàng)建一個初始化變量。 初始化程序需要三個參數(shù):
- 1)
updater
是符合ListUpdatingDelegate的對象,它處理row and section
更新。ListAdapterUpdater
是一個適合您使用的默認實現(xiàn)。 - 2)
viewController
是一個容納適配器的UIViewController
。IGListKit
稍后使用此視圖控制器導航到其他視圖控制器。 - 3)
workingRangeSize
是working range的大小,允許您為可見框外部的部分準備內(nèi)容。
注意:工作范圍
Working ranges
是本教程未涵蓋的更高級主題。 然而,IGListKit repo中有大量文檔甚至是一個示例應用程序!
將以下內(nèi)容添加到viewDidLoad()
的底部:
adapter.collectionView = collectionView
adapter.dataSource = self
這將collectionView
連接到適配器adapter
。 它還將self設置為適配器的dataSource
- 導致編譯器錯誤,因為您尚未符合ListAdapterDataSource
。
通過擴展FeedViewController
以采用ListAdapterDataSource
來解決此問題。 將以下內(nèi)容添加到文件的底部:
// 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
大量使用所需的協(xié)議方法。 即使你可能最終得到空方法,或者返回nil的方法,你也不必擔心默默地丟失方法或者對抗動態(tài)運行時。 它使得使用IGListKit
非常困難。
FeedViewController
現(xiàn)在遵循ListAdapterDataSource
并實現(xiàn)其三個必需的方法:
- 1)
objects(for :)
返回應顯示在集合視圖中的數(shù)據(jù)對象數(shù)組。 您在此處提供loader.entries
,因為它包含日記帳分錄。 - 2) 對于每個數(shù)據(jù)對象,
listAdapter(_:sectionControllerFor :)
必須返回一個節(jié)控制器section controller
的新實例。 現(xiàn)在你要返回一個普通的ListSectionController
來讓編譯器不報錯。 稍后,您將修改此項以返回自定義日記記錄section controller
。 - 3)
emptyView(for :)
返回一個視圖,當列表為空時顯示。 美國宇航局有點緊張,所以他們沒有預算這個功能。
4. Creating Your First Section Controller
section controller
是一種抽象,在給定數(shù)據(jù)對象的情況下,它在集合視圖的一section
中配置和控制單元。 此概念類似于用于配置視圖的視圖模型view-model
:數(shù)據(jù)對象是視圖模型,單元格是視圖。 section controller
充當兩者之間的粘合劑。
在IGListKit
中,您可以為不同類型的數(shù)據(jù)和行為創(chuàng)建新的section controller
。 JPL
工程師已經(jīng)構建了一個JournalEntry
模型,因此您需要創(chuàng)建一個可以處理它的節(jié)控制器。
右鍵單擊SectionControllers
組并選擇New File
。 創(chuàng)建一個名為JournalSectionController
的新Cocoa Touch
類,它是ListSectionController
的子類。
Xcode
不會自動導入第三方框架,因此在JournalSectionController.swift
中,在頂部添加一行:
import IGListKit
將以下屬性添加到JournalSectionController
的頂部:
var entry: JournalEntry!
let solFormatter = SolFormatter()
JournalEntry
是您在實現(xiàn)數(shù)據(jù)源時將使用的模型類。 SolFormatter
類提供將日期轉換為Sol
格式的方法。 你很快就會需要兩個。
同樣在JournalSectionController
中,通過添加以下內(nèi)容來重寫init()
:
override init() {
super.init()
inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
}
如果沒有這個,sections
之間的單元格將彼此相鄰。 這會在JournalSectionController
對象的底部添加15點填充。
您的節(jié)控制器需要重寫ListSectionController
中的四個方法,以提供適配器使用的實際數(shù)據(jù)。
將以下擴展添加到文件的底部:
// 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()
之外,所有方法都是存根實現(xiàn),它只是為日期和文本對返回2。 如果您回顧ClassicFeedViewController.swift
,您會注意到在collectionView(_:numberOfItemsInSection :)
中每個部分也返回2個項目。 這基本上是一回事!
在didUpdate(to :)
中,添加以下內(nèi)容:
entry = object as? JournalEntry
IGListKit
調(diào)用didUpdate(to :)
將對象傳遞給節(jié)控制器(section controller.)
。 請注意,在任何單元協(xié)議方法之前始終調(diào)用此方法。 在這里,您將傳入的對象保存在entry
中。
注意:對象在段控制器的生命周期內(nèi)可以多次更改。 只有當您開始解鎖IGListKit的更高級功能(例如custom model diffing)時才會發(fā)生這種情況。 您不必擔心本教程中的差異。
現(xiàn)在您有了一些數(shù)據(jù),您可以開始配置您的單元格。 用以下代碼替換cellForItem(at :)
的占位符實現(xiàn):
// 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
中給定索引的單元格時調(diào)用cellForItem(at :)
。 以下是代碼的工作原理:
- 1) 如果索引是第一個,請使用
JournalEntryDateCell
單元格,否則使用JournalEntryCell
單元格。Journal entries
始終顯示日期后跟文本。 - 2) 使用單元類,
section controller(self)
和索引將單元從重用池中出列。 - 3) 根據(jù)單元格類型,使用您之前在
didUpdate(to object:)
中設置的JournalEntry
進行配置。
接下來,使用以下內(nèi)容替換sizeForItem(at :)
的占位符實現(xiàn):
// 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
是一個上下文對象,其中包含有關使用節(jié)控制器的適配器,集合視圖和視圖控制器的信息。在這里你可以得到容器的寬度。 - 3) 如果是第一個索引(日期單元格),則返回與容器一樣寬的大小和30個高點。否則,使用單元格幫助程序方法計算單元格的動態(tài)文本大小。
如果您之前使用過UICollectionView
,這種將不同類型的單元格出列,配置和返回大小的模式應該都會讓您感到熟悉。同樣,您可以參考ClassicFeedViewController
并看到很多此代碼幾乎完全相同。
現(xiàn)在,您有一個section controller
,它接收一個JournalEntry
對象并返回并調(diào)整兩個單元格的大小。是時候?qū)⑺鼈冋显谝黄鹆恕?/p>
回到FeedViewController.swift
,用以下內(nèi)容替換listAdapter(_:sectionControllerFor :)
的內(nèi)容:
return JournalSectionController()
只要IGListKit
調(diào)用此方法,它就會返回新的journal section controller
。
構建并運行應用程序。 您應該看到日記帳分錄列表:
后記
本篇主要簡單介紹了基于IGListKit框架的更好的UICollectionViews簡單示例,感興趣的給個贊或者關注~~~