版本記錄
版本號(hào) | 時(shí)間 |
---|---|
V1.0 | 2018.11.28 星期三 |
前言
iOS中有關(guān)視圖控件用戶能看到的都在UIKit框架里面,用戶交互也是通過UIKit進(jìn)行的凶赁。感興趣的參考上面幾篇文章。
1. UIKit框架(一) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(一)
2. UIKit框架(二) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復(fù)使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復(fù)使用的滑塊(二)
7. UIKit框架(七) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(一)
8. UIKit框架(八) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(二)
開始
首先看一下寫作環(huán)境
Swift 4.2, iOS 12, Xcode 10
作為開發(fā)人員,您應(yīng)始終努力提供出色的用戶體驗(yàn)哩罪。在顯示列表的應(yīng)用程序中你需要保證出色一種方法是確保滾動(dòng)順滑狞甚。在iOS 10中锁摔,Apple引入了UICollectionView
預(yù)取API和相應(yīng)的UITableView
預(yù)取API,允許您在Collection Views and Table Views
需要之前獲取數(shù)據(jù)哼审。
當(dāng)您遇到滾動(dòng)阻塞感很強(qiáng)的應(yīng)用程序時(shí)谐腰,這通常是由于長(zhǎng)時(shí)間運(yùn)行的進(jìn)程阻塞主UI線程更新。您希望保持主線程可以自由響應(yīng)觸摸事件等事情涩盾。如果您花費(fèi)很長(zhǎng)時(shí)間來獲取和顯示數(shù)據(jù)十气,用戶可以原諒您,但如果您的應(yīng)用沒有響應(yīng)他們的手勢(shì)春霍,他們就不會(huì)寬恕砸西。將繁重的工作轉(zhuǎn)移到后臺(tái)線程是構(gòu)建響應(yīng)式應(yīng)用程序的第一步。
在本教程中址儒,您將開始使用EmojiRater
芹枷,這是一個(gè)顯示表情符號(hào)集合的應(yīng)用程序。不幸的是莲趣,它的滾動(dòng)性能還有很多不足之處鸳慈。您將使用預(yù)取API來查找應(yīng)用可能很快顯示的單元格,并在后臺(tái)觸發(fā)相關(guān)數(shù)據(jù)提取喧伞。
打開并運(yùn)行示例應(yīng)用程序走芋,如下所示:
很難受,不是嗎潘鲫? 好消息是你可以解決這個(gè)問題翁逞。
關(guān)于應(yīng)用程序的一點(diǎn)。 該應(yīng)用程序顯示emojis的集合視圖溉仑,您可以向下或向下投票挖函。 要使用,請(qǐng)單擊其中一個(gè)單元格彼念,然后用力按直到您感覺到某些觸覺反饋挪圾。 應(yīng)出現(xiàn)評(píng)級(jí)選擇浅萧。 選擇一個(gè)并在更新的集合視圖中查看結(jié)果:
注意:如果您無法在模擬器中使用3D Touch,則首先需要具有
“Force Touch”
功能的觸控板的Mac
或MacBook
哲思。 然后洼畅,您可以轉(zhuǎn)到System Preferences ? Trackpad
并啟用Force Click and haptic feedback
。 如果您無法訪問此類設(shè)備或使用3D Touch
的iPhone棚赔,您仍然可以獲得本教程的基本知識(shí)帝簇。
看看Xcode中的項(xiàng)目。 這些是主要文件:
-
EmojiRating.swift
:表示表情符號(hào)的模型靠益。 -
DataStore.swift
:加載一個(gè)表情符號(hào)丧肴。 -
EmojiViewCell.swift
:顯示表情符號(hào)的集合視圖單元格。 -
RatingOverlayView.swift
:允許用戶對(duì)表情符號(hào)進(jìn)行評(píng)級(jí)的視圖胧后。 -
EmojiViewController.swift
:在集合視圖中顯示表情符號(hào)芋浮。
您將向DataStore
和EmojiViewController
添加功能以增強(qiáng)滾動(dòng)性能。
Understanding Choppy Scrolling - 了解斷續(xù)的滾動(dòng)
您可以通過確保您的應(yīng)用程序滿足每秒60幀(FPS)顯示約束來實(shí)現(xiàn)平滑滾動(dòng)壳快。 這意味著您的應(yīng)用程序需要能夠每秒刷新其UI 60次纸巷,因此每個(gè)幀大約需要16毫秒來呈現(xiàn)內(nèi)容。 系統(tǒng)會(huì)丟棄需要太長(zhǎng)時(shí)間才能顯示內(nèi)容的幀眶痰。
當(dāng)應(yīng)用程序跳過幀并移動(dòng)到下一幀時(shí)瘤旨,這會(huì)導(dǎo)致不穩(wěn)定的滾動(dòng)體驗(yàn)。 丟幀的可能原因是長(zhǎng)時(shí)間運(yùn)行阻塞主線程的操作竖伯。
Apple提供了一些方便的工具來幫助您存哲。 首先,您可以拆分長(zhǎng)時(shí)間運(yùn)行的操作并將它們移動(dòng)到后臺(tái)線程七婴。 這允許您在主線程上處理任何觸摸事件祟偷。 后臺(tái)操作完成后,您可以根據(jù)操作在主線程上進(jìn)行任何所需的UI更新打厘。
以下顯示了丟幀的情況:
將工作移至后臺(tái)后肩袍,事情如下所示:
您現(xiàn)在有兩個(gè)并發(fā)線程正在運(yùn)行以提高應(yīng)用程序的性能。
如果你可以在必須顯示之前開始獲取數(shù)據(jù)婚惫,那會(huì)不會(huì)更好? 這就是UITableView
和UICollectionView
預(yù)取API的用武之地魂爪。您將在本教程中使用集合視圖API先舷。
Loading Data Asynchronously - 異步加載數(shù)據(jù)
Apple提供了多種方法來為您的應(yīng)用添加并發(fā)性。 您可以使用Grand Central Dispatch (GCD)作為輕量級(jí)機(jī)制來同時(shí)執(zhí)行任務(wù)滓侍。 或者蒋川,您可以使用構(gòu)建在GCD之上的Operation。
Operation
會(huì)增加更多開銷撩笆,但可以輕松重用和取消操作捺球。 您將在本教程中使用Operation
缸浦,以便您可以取消之前開始加載不再需要的表情符號(hào)的操作。
現(xiàn)在是時(shí)候開始研究哪里可以最好地利用EmojiRater
中的并發(fā)性氮兵。
打開EmojiViewController.swift
并找到數(shù)據(jù)源方法collectionView(_:cellForItemAt :)
裂逐。 看下面的代碼:
if let emojiRating = dataStore.loadEmojiRating(at: indexPath.item) {
cell.updateAppearanceFor(emojiRating, animated: true)
}
這會(huì)在顯示之前從數(shù)據(jù)存儲(chǔ)中加載表情符號(hào)。 讓我們來看看它是如何實(shí)現(xiàn)的泣栈。
打開DataStore.swift
并查看加載方法:
public func loadEmojiRating(at index: Int) -> EmojiRating? {
if (0..<emojiRatings.count).contains(index) {
let randomDelayTime = Int.random(in: 500..<2000)
usleep(useconds_t(randomDelayTime * 1000))
return emojiRatings[index]
}
return .none
}
此代碼在隨機(jī)延遲(500ms到2,000ms)之后返回有效的表情符號(hào)卜高。 延遲是在不同條件下對(duì)網(wǎng)絡(luò)請(qǐng)求的仿真模擬。
問題發(fā)現(xiàn)了南片! 表情符號(hào)提取發(fā)生在主線程上掺涛,違反了16ms
閾值,觸發(fā)丟幀疼进。 你即將解決這個(gè)問題薪缆。
將以下代碼添加到DataStore.swift
的末尾:
class DataLoadOperation: Operation {
// 1
var emojiRating: EmojiRating?
var loadingCompleteHandler: ((EmojiRating) -> Void)?
private let _emojiRating: EmojiRating
// 2
init(_ emojiRating: EmojiRating) {
_emojiRating = emojiRating
}
// 3
override func main() {
// TBD: Work it!!
}
}
Operation
是一個(gè)抽象類,您必須使用它的子類才能實(shí)現(xiàn)要從主線程移出的工作伞广。
以下是代碼中一步一步發(fā)生的事情:
- 1) 創(chuàng)建對(duì)此操作中將使用的表情符號(hào)和完成處理程序的引用拣帽。
- 2) 創(chuàng)建一個(gè)指定的初始化程序,允許您傳入表情符號(hào)赔癌。
- 3) 重寫
main()
方法以執(zhí)行此操作的實(shí)際工作诞外。
現(xiàn)在,將以下代碼添加到main()
:
// 1
if isCancelled { return }
// 2
let randomDelayTime = Int.random(in: 500..<2000)
usleep(useconds_t(randomDelayTime * 1000))
// 3
if isCancelled { return }
// 4
emojiRating = _emojiRating
// 5
if let loadingCompleteHandler = loadingCompleteHandler {
DispatchQueue.main.async {
loadingCompleteHandler(self._emojiRating)
}
}
下面進(jìn)行細(xì)分
- 1) 在開始之前檢查取消灾票。 在嘗試長(zhǎng)期或密集的工作之前峡谊,
Operations
應(yīng)定期檢查是否已取消。 - 2) 模擬長(zhǎng)時(shí)間運(yùn)行的表情符號(hào)提取刊苍。 這段代碼應(yīng)該很熟悉既们。
- 3) 檢查操作是否已取消。
- 4) 分配表情符號(hào)以指示提取已完成正什。
- 5) 在主線程上調(diào)用完成處理程序啥纸,傳入表情符號(hào)。 然后婴氮,這應(yīng)該觸發(fā)UI更新以顯示表情符號(hào)斯棒。
將loadEmojiRating(at :)
替換為以下內(nèi)容:
public func loadEmojiRating(at index: Int) -> DataLoadOperation? {
if (0..<emojiRatings.count).contains(index) {
return DataLoadOperation(emojiRatings[index])
}
return .none
}
原始代碼有兩處更改:
- 1) 您創(chuàng)建一個(gè)
DataLoadOperation()
以在后臺(tái)獲取表情符號(hào)。 - 2) 此方法現(xiàn)在返回
DataLoadOperation
可選主经,而不是EmojiRating
可選荣暮。
您現(xiàn)在需要處理方法簽名更改并使用您的全新operation
。
打開EmojiViewController.swift
罩驻,并在collectionView(_:cellForItemAt :)
中刪除以下代碼:
if let emojiRating = dataStore.loadEmojiRating(at: indexPath.item) {
cell.updateAppearanceFor(emojiRating, animated: true)
}
您將不再啟動(dòng)此數(shù)據(jù)源方法的數(shù)據(jù)提取穗酥。 相反,您將在應(yīng)用程序即將顯示集合視圖單元格時(shí)調(diào)用的委托方法中執(zhí)行此操作。
在類頂部附近添加以下屬性:
let loadingQueue = OperationQueue()
var loadingOperations: [IndexPath: DataLoadOperation] = [:]
第一個(gè)屬性包含操作隊(duì)列砾跃。 loadingOperations
是一個(gè)跟蹤數(shù)據(jù)加載操作的數(shù)組骏啰,通過索引路徑將每個(gè)加載操作與其對(duì)應(yīng)的單元相關(guān)聯(lián)。
將以下代碼添加到文件末尾:
// MARK: - UICollectionViewDelegate
extension EmojiViewController {
override func collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath) {
guard let cell = cell as? EmojiViewCell else { return }
// 1
let updateCellClosure: (EmojiRating?) -> Void = { [weak self] emojiRating in
guard let self = self else {
return
}
cell.updateAppearanceFor(emojiRating, animated: true)
self.loadingOperations.removeValue(forKey: indexPath)
}
// 2
if let dataLoader = loadingOperations[indexPath] {
// 3
if let emojiRating = dataLoader.emojiRating {
cell.updateAppearanceFor(emojiRating, animated: false)
loadingOperations.removeValue(forKey: indexPath)
} else {
// 4
dataLoader.loadingCompleteHandler = updateCellClosure
}
} else {
// 5
if let dataLoader = dataStore.loadEmojiRating(at: indexPath.item) {
// 6
dataLoader.loadingCompleteHandler = updateCellClosure
// 7
loadingQueue.addOperation(dataLoader)
// 8
loadingOperations[indexPath] = dataLoader
}
}
}
}
這將為UICollectionViewDelegate
創(chuàng)建一個(gè)擴(kuò)展抽高,并實(shí)現(xiàn)collectionView(_:willDisplay:forItemAt :)
委托方法判耕。 下面進(jìn)行細(xì)說分解:
- 1) 創(chuàng)建一個(gè)閉包來處理加載數(shù)據(jù)后如何更新單元格。
- 2) 檢查單元是否有數(shù)據(jù)加載操作厨内。
- 3) 檢查數(shù)據(jù)加載操作是否已完成祈秕。 如果是這樣,請(qǐng)更新單元格的UI并從跟蹤陣列中刪除操作雏胃。
- 4) 如果尚未獲取表情符號(hào)请毛,則將閉包分配給數(shù)據(jù)加載完成處理程序。
- 5) 如果沒有數(shù)據(jù)加載操作瞭亮,請(qǐng)為相關(guān)表情符號(hào)創(chuàng)建一個(gè)新的操作方仿。
- 6) 將閉包添加到數(shù)據(jù)加載完成處理程序。
- 7) 將操作添加到操作隊(duì)列统翩。
- 8) 將數(shù)據(jù)加載器添加到操作跟蹤陣列仙蚜。
從集合視圖中刪除單元格時(shí),您需要確保進(jìn)行一些清理厂汗。
將以下方法添加到UICollectionViewDelegate
擴(kuò)展:
override func collectionView(_ collectionView: UICollectionView,
didEndDisplaying cell: UICollectionViewCell,
forItemAt indexPath: IndexPath) {
if let dataLoader = loadingOperations[indexPath] {
dataLoader.cancel()
loadingOperations.removeValue(forKey: indexPath)
}
}
此代碼檢查與cell關(guān)聯(lián)的現(xiàn)有數(shù)據(jù)加載操作委粉。如果存在,則取消下載并從跟蹤操作的陣列中刪除操作娶桦。
構(gòu)建并運(yùn)行應(yīng)用程序贾节。滾動(dòng)表情符號(hào)并注意應(yīng)用程序性能的改進(jìn)。
如果您可以樂觀地獲取數(shù)據(jù)以預(yù)期顯示集合視圖單元格衷畦,那就更好了栗涂。您將使用預(yù)取API來執(zhí)行此操作并為EmojiRater
提供額外的提升。
Enabling UICollectionView Prefetching - 啟用UICollectionView預(yù)取
UICollectionViewDataSourcePrefetching
協(xié)議為您提前發(fā)出警告祈争,即可能很快需要集合視圖的數(shù)據(jù)斤程。您可以使用此信息開始預(yù)取數(shù)據(jù),以便在單元格可見時(shí)菩混,數(shù)據(jù)可能已經(jīng)可用忿墅。這與你已經(jīng)完成的并發(fā)工作一起工作 - 關(guān)鍵的區(qū)別在于工作開始時(shí)。
下圖顯示了這種情況如何發(fā)揮作用沮峡。用戶在集合視圖上向上滾動(dòng)球匕。黃色單元格應(yīng)該很快進(jìn)入視圖 - 假設(shè)這發(fā)生在Frame 3
中,并且您目前處于Frame 1
帖烘。
采用prefetch
協(xié)議會(huì)通知應(yīng)用程序有關(guān)可能變?yōu)榭梢姷南乱粋€(gè)單元格。 如果沒有prefetch
觸發(fā)器橄杨,黃色單元的數(shù)據(jù)提取將在Frame 3
開始秘症,并且cell的數(shù)據(jù)在一段時(shí)間后變?yōu)榭梢姟?由于prefetch
照卦,單元格數(shù)據(jù)將在單元格可見時(shí)準(zhǔn)備就緒。
打開EmojiViewController.swift
并將以下代碼添加到文件末尾:
// MARK: - UICollectionViewDataSourcePrefetching
extension EmojiViewController: UICollectionViewDataSourcePrefetching {
func collectionView(_ collectionView: UICollectionView,
prefetchItemsAt indexPaths: [IndexPath]) {
print("Prefetch: \(indexPaths)")
}
}
EmojiViewController
現(xiàn)在采用UICollectionViewDataSourcePrefetching
并實(shí)現(xiàn)所需的委托方法乡摹。 該實(shí)現(xiàn)只是打印出很快就可以看到的索引路徑役耕。
在viewDidLoad()
中,在調(diào)用super.viewDidLoad()
之后添加以下內(nèi)容:
collectionView?.prefetchDataSource = self
這將EmojiViewController
設(shè)置為集合視圖的預(yù)取數(shù)據(jù)源聪廉。
構(gòu)建并運(yùn)行應(yīng)用程序瞬痘,在滾動(dòng)之前,檢查Xcode的控制臺(tái)板熊。 你應(yīng)該看到這樣的東西:
Prefetch: [[0, 10], [0, 11], [0, 12], [0, 13], [0, 14], [0, 15]]
這些對(duì)應(yīng)于尚未變得可見的cell框全。 現(xiàn)在,滾動(dòng)更多并像您一樣檢查控制臺(tái)日志干签。 您應(yīng)該看到基于不可見的索引路徑的日志消息津辩。 嘗試向上和向下滾動(dòng),直到您充分了解這一切是如何工作的容劳。
您可能想知道為什么這個(gè)委托方法只是為您提供索引路徑喘沿。 我們的想法是你應(yīng)該從這個(gè)方法開始你的數(shù)據(jù)加載過程,然后在collectionView(_:cellForItemAt :)
或collectionView(_:willDisplay:forItemAt :)
中處理結(jié)果竭贩。 請(qǐng)注意蚜印,當(dāng)立即需要單元格時(shí),不會(huì)調(diào)用委托方法留量。 因此窄赋,您應(yīng)該不依賴于在此方法中將數(shù)據(jù)加載到單元格中。
Prefetching Data Asynchronously - 異步預(yù)取數(shù)據(jù)
在EmojiViewController.swift
中肪获,通過使用以下內(nèi)容替換print()
語句來修改collectionView(_:prefetchItemsAt :)
:
for indexPath in indexPaths {
// 1
if let _ = loadingOperations[indexPath] {
continue
}
// 2
if let dataLoader = dataStore.loadEmojiRating(at: indexPath.item) {
// 3
loadingQueue.addOperation(dataLoader)
loadingOperations[indexPath] = dataLoader
}
}
代碼循環(huán)遍歷方法接收的索引路徑并執(zhí)行以下操作:
- 1) 檢查此cell是否存在現(xiàn)有的加載操作寝凌。 如果有的話,沒有更多的事要做孝赫。
- 2) 如果找不到加載操作较木,則創(chuàng)建數(shù)據(jù)加載操作。
- 3) 將操作添加到隊(duì)列并更新跟蹤數(shù)據(jù)加載操作的字典青柄。
傳遞到collectionView(_:prefetchItemsAt :)
的索引路徑按優(yōu)先級(jí)排序伐债,基于到集合視圖視圖的單元格幾何距離。 這允許您獲取最有可能需要的單元格致开。
回想一下峰锁,您之前在collectionView(_:willDisplay:forItemAt :)
中添加了代碼來處理加載操作的結(jié)果。 請(qǐng)查看以下方法的重點(diǎn):
override func collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// ...
let updateCellClosure: (EmojiRating?) -> Void = { [weak self] emojiRating in
guard let self = self else {
return
}
cell.updateAppearanceFor(emojiRating, animated: true)
self.loadingOperations.removeValue(forKey: indexPath)
}
if let dataLoader = loadingOperations[indexPath] {
if let emojiRating = dataLoader.emojiRating {
cell.updateAppearanceFor(emojiRating, animated: false)
loadingOperations.removeValue(forKey: indexPath)
} else {
dataLoader.loadingCompleteHandler = updateCellClosure
}
} else {
// ...
}
}
創(chuàng)建cell更新閉包后双戳,檢查跟蹤操作的數(shù)組虹蒋。 如果存在即將出現(xiàn)的單元格且表情符號(hào)可用,則更新單元格的UI。 請(qǐng)注意魄衅,傳遞給數(shù)據(jù)加載操作的閉包也會(huì)更新單元格的UI峭竣。
這就是所有內(nèi)容的關(guān)系,從預(yù)取觸發(fā)操作到正在更新的單元UI晃虫。
構(gòu)建并運(yùn)行應(yīng)用程序并滾動(dòng)表情符號(hào)皆撩。 滾動(dòng)到的Emojis應(yīng)該比以前更快地顯示。
你能發(fā)現(xiàn)一些可以改進(jìn)的東西嗎哲银?如果您滾動(dòng)得非晨竿蹋快,那么您的collection view
將開始獲取可能永遠(yuǎn)不會(huì)看到的表情符號(hào)荆责。 請(qǐng)繼續(xù)閱讀滥比。
Canceling a Prefetch - 取消預(yù)取
UICollectionViewDataSourcePrefetching
具有可選的委托方法,可讓您知道不再需要數(shù)據(jù)草巡。 這可能發(fā)生守呜,因?yàn)橛脩粢呀?jīng)開始非常快地滾動(dòng)并且可能不會(huì)看到中間單元。 您可以使用委托方法取消任何掛起的數(shù)據(jù)加載操作。
仍然在EmojiViewController.swift
中祥得,將以下方法添加到您的UICollectionViewDataSourcePrefetching
協(xié)議實(shí)現(xiàn):
func collectionView(_ collectionView: UICollectionView,
cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
if let dataLoader = loadingOperations[indexPath] {
dataLoader.cancel()
loadingOperations.removeValue(forKey: indexPath)
}
}
}
代碼循環(huán)遍歷索引路徑并查找附加到它們的任何加載操作。 然后它取消操作并將其從跟蹤操作的字典中刪除玛迄。
構(gòu)建并運(yùn)行應(yīng)用程序。 當(dāng)您快速滾動(dòng)時(shí)棚亩,可能已開始的操作應(yīng)該開始取消蓖议。 在視覺上,事情看起來不會(huì)有太大的不同讥蟆。
需要注意的一點(diǎn)是勒虾,由于cell重用,可能需要重新獲取一些先前可見的cell瘸彤。
后記
本篇主要講述了UICollectionView的數(shù)據(jù)異步預(yù)加載修然,感興趣的給個(gè)贊或者關(guān)注~~~