WWDC2016 Session筆記 - iOS 10 UICollectionView新特性

前言

關於 iOS 10 UICollectionView的新特性,主要仍是體如今以下3個方面數據庫

  1. 順滑的滑動體驗 如今基本上人人都離不開手機,手機的app也天天都有人在用。一個app的好壞由它的用戶體驗決定。在能夠滑動的視圖裏面,必需要更加絲滑柔順才能得到用戶的青睞。這些UICollectionView的新特性可讓大家的app比原來更加順滑,並且這些特性只須要你加入少許的代碼便可達到目的。
  2. 針對self-sizing的改進 self-sizing的API在iOS8的時候被引進,iOS10中加入更多特性使cell更加容易去適配。
  3. Interactive reordering重排 這個功能在iOS9的時候介紹過了,蘋果在iOS 10的API裏面大大加強了這一功能。

目錄

  • 1.UICollectionViewCell順滑的滑動體驗
  • 2.UICollectionViewCell的Pre-Fetching預加載
  • 3.UITableViewCell的Pre-Fetching預加載
  • 4.針對self-sizing的改進
  • 5.Interactive Reordering
  • 6.UIRefreshControl

一. UICollectionViewCell順滑的滑動體驗

衆所周知,iOS設備已良好的用戶體驗贏得了廣大的用戶羣。iOS系統在用戶點擊屏幕會當即作出響應。並且很大一部分的操做是來自於用戶的滑動操做。因此滑動的順滑是使用戶沉浸在app中享受的必要條件。接下來咱們就談談iOS 10 中增長了那些新特性。api

咱們先來看一下以前 UICollectionView 的體驗,假設咱們每一個cell都是簡單的藍色,實際開發app中,cell會比這複雜不少。 咱們先生成100個cell。當用戶滑動不是很快的時候,還感受不出來卡頓,當用戶大幅度滑動,整個UICollectionView的卡頓就很明顯了。若是整個cell的DataSource又是從網絡加載的,那就更加卡頓了。效果以下圖。數組

若是這種app上架,用戶使用事後,極可能就直接給1星評價了。可是爲何會形成這種問題呢?咱們來分析一下,咱們模擬一下系統如何處理重用機制的,效果以下圖網絡

在上圖中,咱們能夠看出,當cell準備加載進屏幕的時候,整個cell都已經加載完成,等待在屏幕外面了。並且更重要的是,在屏幕外面等待加載的cell是整整一行!這一行的cell都已經加載完數據。這是UICollectionView在用戶大幅度滑動時卡頓的根本緣由。用專業的術語來講,掉幀。多線程

接下來咱們就來詳細的說說掉幀的問題。 app

當今的用戶是很挑剔的,用戶須要一個很順滑的體驗,只要有一點卡頓,極可能一言不合就卸載app了。要想用戶感受不到卡頓,那麼咱們的app必須幀率達到60幀/秒。用數學換算一下就是每幀16毫秒就必須刷新一次。異步

咱們用圖標來分析一下掉幀的問題。下面會出現2種不一樣的幀。 佈局

第一種狀況,下圖是當用戶輕微的上下小幅度滑動。這個時候每一個cell的加載壓力都不大,iOS針對這種狀況,已經作了很好的優化了,因此用戶感受不到任何卡頓。這種狀況是不會掉幀,用戶也但願能使用如此順滑的app。
性能

第二種狀況,當用戶大幅度滑動,每一個cell加載的壓力很大,也許須要網絡請求,也許須要讀取數據庫,並且每次都加載一行cell出來,這樣每一個cell的加載時間都增長了,加載一行的總時間也就大大增長了,以下圖所示。這樣,不只僅當前幀在加載cell,總的時間還會擠壓到下一幀的時間裏面去。這種狀況下,用戶就感受到了卡頓了。fetch

咱們換種方式在說明一下2種狀況下掉幀的狀況。咱們用下圖的標準來衡量一下上面2種狀況。下圖分爲2部分,上面紅色的區域,就是表示掉幀的區域,由於高於16ms。紅色和綠色區域的分界線就在16ms處。y軸咱們表示的是CPU在主線程中花費的時間。x軸表示的是在用戶滑動中發生的刷新事件。

針對上述掉幀的狀況,繪製出實驗數據,以下圖。值得咱們關注的是,曲線是很曲折的,很是的不平滑。當用戶大幅度滑動的時候,峯值超過了16ms,當用戶慢速滑動的時候,幀率又能保持在比較順滑的區域。處於綠色區域內的cell加載壓力都是很小的。這就是時而掉幀時而順滑的場景。這種場景下,用戶體驗是很糟糕的。

那怎麼解決這麼問題的呢?咱們來看下圖:

上圖中的曲線咱們看着就很平緩了,並且這種狀況也不會出現掉幀的狀況了,每一個滑動中的時間都能達到60幀了。這是怎樣作到的呢?由於把每一個cell的加載事件都平分了,每一個cell不會再出現很忙和很閒的兩個極端。這樣咱們就取消了以前的波峯和波谷。從而讓該曲線達到近乎水平的直線。

如何讓每一個cell都分攤加載任務的壓力?這就要談到新的cell的生命週期了。

先來看看老的 UICollectionViewCell的聲明週期。當用戶滑動屏幕,屏幕外有一個cell準備加載顯示進來。

這個時候咱們把這個cell從reuse隊列裏面拿出來,而後調用prepareForReuse方法。這個方法就給了cell時間,用來重置cell,重置狀態,刷新cell,加載新的數據。

再滑動,咱們就會調用cellForItemAtIndexPath方法了。這個方法裏面就是咱們開發者自定義的填充cell的方式了。這裏會填充data model,而後賦值給cell,再把cell返回給iOS系統。

當cell立刻要進入屏幕的時候,就會調用willDisplayCell的方法。這個方法給了咱們app最後一次機會,爲cell進入屏幕作最後的準備工做。執行完willDisplayCell以後,cell就進入屏幕了。

當cell徹底離開屏幕以後,就會調用didEndDisplayingCell方法。以上就是在iOS10以前的整個UICollectionViewCell的生命週期。

接下來咱們就來看看iOS 10的UICollectionViewCell生命週期是怎麼樣的。

這裏仍是和iOS9同樣的,當用戶滑動UICollectionView的時候,須要一個cell,咱們就從reuse隊列裏面拿出一個cell,並調用prepareForReuse方法。注意調用這個方法的時間,當cell尚未進入屏幕的時候,就已經提早調用這個方法了。注意對比和iOS 9的區別,iOS 9 是在cell上邊緣立刻進入屏幕的時候才調用方法,而這裏,cell整個生命週期都被提早了,提早到cell還在設備外面的時候。

這裏仍是和以前同樣,在cellForItemAtIndexPath中建立cell,填充數據,刷新狀態等等操做。注意,這裏生命週期也比iOS 9提早了。

用戶繼續滑動,這個時候就有不一樣了!

這個時候咱們並不去調用willDisplayCell方法了!這裏遵循的原則是,什麼時候去顯示,什麼時候再去調用willDisplayCell。

當cell要立刻就須要顯示的時候,咱們再調用willDisplayCell方法。

當整個cell要從UICollectionView的可見區域消失的時候,這個時候會調用didEndDisplayingCell方法。接下來發生的事情和iOS9同樣,cell會進入重用隊列中。

若是用戶想要顯示某個cell,在iOS 9 當中,cell只能從重用隊列裏面取出,再次走一遍生命週期。並調用cellForItemAtIndexPath去建立或者生成一個cell。

在iOS 10 當中,系統會把cell保持一段時間。在iOS中,若是用戶把cell滑出屏幕後,若是忽然又想回來,這個時候cell並不須要再走一段的生命週期了。只須要直接調用willDisplayCell就能夠了。cell就又會從新出如今屏幕中。這就是iOS 10 的整個UICollectionView的生命週期。

上面說的iOS 10裏面的場景一樣適用於多列的狀況。 這時咱們每次只加載一個cell,而不是每次加載一行的cell。當第一個cell準備好以後再叫第二個cell準備。當2個cell都準備好了以後,接着咱們再調用willDisplayCell給每一個cell,發送完這個消息以後,cell就會出如今屏幕上了。

這雖然看起來是一個很小的改動,可是這小小的改動就提高了不少的用戶體驗!

讓咱們來看看上述的改動對滑動的影響

滑動比iOS 9流程不少,這裏能夠看到整個過程都很平緩,不卡頓。

仍是和iOS 9同樣,咱們來模擬一下系統是如何加載cell的狀況。

咱們能夠很明顯的看到,iOS 系統是一個個的加載cell的,一個cell加載完以後再去加載下一個cell。這裏和iOS 9 的有很大的不一樣,iOS 9是加載整整一行的cell。

這是由於咱們用了新的 UICollectionViewCell的生命週期。整個app徹底沒有加一行代碼。如今iOS 10是絲滑的滑動體驗實在是太棒了!!

二. UICollectionViewCell的Pre-Fetching預加載

當咱們編譯iOS 10的app的時候,這個Pre-Fetching默認是enable的。固然,若是有一些緣由致使你必須用到iOS 10以前老的生命週期,你只須要給collectionView加入新的isPrefetchingEnabled屬性便可。若是你不想用到Pre-Fetching,那麼把這個屬性變成false便可。

collectionView.isPrefetchingEnabled = false複製代碼

爲了最佳實踐一下這個新特性。咱們先改變一下咱們加載cell的方式。咱們把很重的讀取數據的操做,全部內容的建立都放到cellForItemAtIndexPath方法裏面去完成。保證咱們在willDisplayCell 和 didEndDisplayCell這兩個方法裏面基本不作其餘事情。最後,須要注意的是cellForItemAtIndexPath生成的某些cell,可能永遠都不會被展現在屏幕上,有這樣一種狀況,當cell將要展現在屏幕上的時候,用戶忽然滑動離開了這個界面。

若是這個時候當你用iOS 10編譯出你的app,那麼很是順滑的用戶體驗就會自動的優化出來。

UICollectionView的流暢的滑動解決了,那麼在UICollectionViewCell在加載的時候所花費的時間,怎麼解決呢??

UICollectionViewCell加載的時間取決於DataModel。DataModel極可能回去加載圖片,來自於網絡或者來自於本地的數據庫。這些操做大多數都是異步的操做。爲了使data加載更快,iOS 10引入了新的API來解決這個問題。

UICollectionView有2個「小夥伴」,那就是data source和delegate。在iOS 10中,將會迎來第3個「小夥伴」。這個「小夥伴」叫prefetchDataSource。

protocol UICollectionViewDataSourcePrefetching {
    func collectionView(_ collectionView: UICollectionView,
                        prefetchItemsAt indexPaths: [NSIndexPath])
    optional func collectionView(_ collectionView: UICollectionView,
                                 cancelPrefetchingForItemsAt indexPaths: [NSIndexPath])
}
class UICollectionView : UIScrollView {
    weak var prefetchDataSource: UICollectionViewDataSourcePrefetching?
    var isPrefetchingEnabled: Bool
}複製代碼

這個協議裏面只有一個必需要實現的方法——ColletionView prefetchItemsAt indexPaths。這個方法會在prefetchDataSource裏面被調用,用來給你異步的預加載數據的。indexPaths數組是有序的,就是接下來item接收數據的順序,讓咱們model異步處理數據更加方便。

在這個協議裏面還有第二個方法CollectionView cancelPrefetcingForItemsAt indexPaths,不過這個方法是optional的。咱們能夠利用這個方法來處理在滑動中取消或者下降提早加載數據的優先級。

值得說明的是,新增長的這個「小夥伴」prefetchDataSource並不能代替原來的讀取數據的方法,這個預加載僅僅只是輔助加載數據,並不能 刪除原來咱們讀取數據的方法。

至此,咱們來看看從文章開始到如今,UICollectionView的性能提高了多少。咱們仍是用掉幀的方法來看看UICollectionView的性能。

上圖是iOS 9 UICollectionView的性能,很明顯的看見,波峯波谷很明顯,而且還掉了8幀,有明顯的卡頓現象。

上圖是iOS 10 UICollectionView的性能,咱們能夠很明顯的看到,通過iOS 10的優化,整個曲線很明顯平緩了一些,沒有極端的波峯掉幀現象。可是依舊存在少許的波峯快到16ms分界線了。

上圖是iOS 10 + Pre-Fetching API 以後的性能,已經優化的效果很明顯了!整條曲線基本都水平了。近乎完美。可是仍是能發現有個別波峯特別高。波峯特別高的地方就是那個cell加載壓力大,時間花的比較長致使的。接下來咱們繼續優化!

先來總結一下使用Pre-Fetching API須要注意的地方。

  1. 在咱們使用Pre-Fetching API的時候,咱們必定要保證整個預加載的過程都放在後臺線程中進行。合理使用GCD 和 NSOperationQueue處理好多線程。

  2. 請切記,Pre-Fetching API是一種自適應的技術。何爲自適應技術呢?當咱們滑動速度很慢的時候,在這種「安靜」的時期,Pre-Fetching API會默默的在後臺幫咱們預加載數據,可是一旦當咱們快速滑動,咱們須要頻繁的刷新,咱們不會去執行Pre-Fetching API。

  3. 最後,用cancelPrefetchingAPI去迎合用戶的滑動動做的變換,好比說用戶在快速滑動忽然發現了有趣的感興趣的事情,這個時候停下來滑動了,甚至快速反向滑動了,或者點擊了事件,進去看詳情了,這些時刻咱們都應該開啓cancelPrefetchingAPI。

綜上所述,Pre-Fetching API對於提升UICollectionView的性能提高是頗有幫助的,並且並不須要加入太多的代碼。加入少許的代碼就能夠得到巨大的性能提高!

三. UITableViewCell的Pre-Fetching預加載

在iOS 10中,UITableViewCell也跟着UICollectionView一塊兒獲得了性能的提高,同樣擁有了Pre-Fetching API。

protocol UITableViewDataSourcePrefetching {
    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [NSIndexPath])
    optional func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths:
                            [NSIndexPath])
}
class UITableView : UIScrollView {
    weak var prefetchDataSource: UITableViewDataSourcePrefetching?
}複製代碼

這裏和上面 UICollectionView同樣,會調用TableView prefetchRowsAt indexPaths方法。indexPaths仍是一個有序數字,順序就是列表上可見的順序。第二個可選的API仍是TableView cancelPrefetchingForRowsAt indexPaths,和以前提到的同樣,也是用來取消預加載的。性能的提高和UICollectionView同樣的,對UITableView的性能提高很大!

四. 針對self-sizing的改進

self-sizing API 第一次被引入是在iOS 8,然而如今在iOS 10中獲得了一些改進。

在UICollectionView 中有一個固定的類,叫UICollectionViewFlowLayout,iOS已經在這個類中徹底支持了self-sizing。爲了能開啓這一特性,須要咱們開發者爲一些不能爲0的CGSize的cell設置一下estimated item size。

layout.estimatedItemSize = CGSize(width:50,height:50)複製代碼

這會告訴UICollectionView咱們想要開啓動態計算內容的佈局。

至今,咱們能有3種方法來動態的佈局。

  1. 第一種方法是使用autolayout 當咱們合理的加上了constrain,當cell加載的時候,就會根據內容動態的加載佈局。

  2. 第二種方法,若是你不想使用autolayout的方法,想更加手動的控制它,那麼咱們就須要重寫sizeThatFits()方法。

  3. 第三種方法,終極的方法是重寫preferredLayoutAttributesFittingAttributes()方法。在這個方法裏面不只僅能夠提供size的信息,更能夠獲得alpha和transform的信息。

因此想指定cell的大小,就能夠用上面3個方法之一。

可是實際操做中,咱們能夠發現,有時候設置一個合適的estimated item size,對於咱們來講是很困難的事情。若是flow layout能夠用數學的方法動態的計算佈局,而不是根據咱們給的size去佈局,那會是件很酷的事情。

iOS 10中就引入了新的API來解決上述的問題。

layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize複製代碼

對於開發者,咱們須要作的事情,僅僅就是設置好flow layout ,而後給estimatedItemSize設定一個新的常數, 最後UICollectionViewFlowLayout 就會自動計算高度了。

系統會自動計算好全部的佈局,包括已經定下來的size的cell,而且還會動態的給出接下來cell的大小的預測。

接下來看2個例子就能夠很明顯看出iOS 10針對self-sizing的改進了。

上圖能夠看到,iOS 9 的佈局是針對單個cell計算的,當改變了單個的cell,其餘的cell依舊沒有變化,仍是須要從新計算。

這裏例子就能夠很明顯的看出差異了。當咱們改變了第一個cell的size之後,系統會自動計算出全部的cell的size,而且每一行,每個section的size都會被動態的計算出來,而且刷新界面!

以上就是iOS 10針對self-sizing的改進。

五. Interactive Reordering

談到從新排列,這是咱們就須要類比一下UITableView了,UICollectionView的從新排列就如同UITableView 把cell上下移動,只不過UITableView的重排是針對垂直方向的。

在iOS 9中,引入了UICollectionView的Interactive Reordering,在今年的iOS 10中,又加入了一些新的API。

在上圖中,咱們能夠看到,咱們即便任意拖動cell,整個界面也會從新排列,而且咱們改變了cell的大小,整個 UICollectionView 也會從新動態的佈局。

咱們先來看看iOS 9裏面的API

class UICollectionView : UIScrollView {
    func beginInteractiveMovementForItem(at indexPath: NSIndexPath) -> Bool
    func updateInteractiveMovementTargetPosition(_ targetPosition: CGPoint)
    func endInteractiveMovement()
    func cancelInteractiveMovement()
}複製代碼

要想開啓interactive movement,咱們就須要調用beginInteractiveMovementForItem()方法,其中indexPath表明了咱們將要移動走的cell。接着每次手勢的刷新,咱們都須要刷新cell的位置,去響應咱們手指的移動操做。這時咱們就須要調用updateInteractiveMovementTargetPosition()方法。咱們經過手勢來傳遞座標的變化。當咱們移動結束以後,就會調用endInteractiveMovement()方法。 UICollectionView 就會放下cell,處理完整個layout,此時你也能夠從新刷新model或者處理數據model。若是中間忽然手勢取消了,那麼這個時候就應該調用cancelInteractiveMovement()方法。若是咱們從新把cell移動一圈以後又放回原位,其實就是取消了移動,那這個時候就應該在cancelInteractiveMovement()方法裏面不用去刷新data source。

在iOS 10中,若是你使用UICollectionViewController,那麼這個重排對於你來講會更加的簡單。

class UICollectionViewController : UIViewController {
    var installsStandardGestureForInteractiveMovement: Bool
}複製代碼

你只須要把installsStandardGestureForInteractiveMovement這個屬性設置爲True便可。CollectionViewController會自動爲你加入手勢,而且自動爲你調用上面的方法。

以上就是去年iOS 9爲咱們增長的API。

今年的iOS 10新加入的API是在iOS 9的基礎上增長了翻頁的功能。
UICollectionView繼承自UIScrollView,因此只須要你作的是把isPagingEnabled屬性設置爲True,便可開啓分頁的功能。

collectionView.isPagingEnabled = true複製代碼

開啓分頁以前:

開啓分頁以後就長這樣子:

每次移動一次就會以頁爲單位的翻頁。

六.UIRefreshControl

UIRefreshControl如今能夠直接在CollectionView裏面使用,一樣的,也能夠直接在UITableView裏面使用,而且能夠脫離UITableViewController。由於如今RefreshControl成爲了ScrollView的一個屬性了。

UIRefreshControl的使用方法很簡單,就三步:

let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshControlDidFire(_:)),
                         for: .valueChanged)
collectionView.refreshControl = refreshControl複製代碼

先建立一個refreshControl,再關聯一個action事件,最後把這個新的refreshControl賦給想要的控件的對應的屬性便可。

總結

經過以上,咱們談到了如下的知識:

  1. UICollectionView cell pre-fetching預加載機制
  2. UICollectionView and UITableView prefetchDataSource 新增的API
  3. 針對self-sizing cells 的改進
  4. Interactive reordering

最後,談談我看了iOS 10 UICollectionView的優化的見解吧,原來有些地方用到AsyncDisplayKit優化UICollectionView速度的,如今能夠考慮不用第三方庫優化了,系統自帶的方法能夠解決通常性的卡頓的問題了。我感受iOS 10的UICollectionView纔像是一個完整版的,以前的系統優化的都不夠。我仍是很看好iOS 10的UICollectionView。

請你們多多指教。新浪微博@halfrost

相關文章
相關標籤/搜索