第2章:使用 UICollectionView 顯示內(nèi)容

注:
本文翻譯自 《iOS UICollectionView The Complete Guide 2nd Edition》
使用的翻譯工具:https://www.deepl.com/translator

現(xiàn)在,你已經(jīng)了解了如何在遵循 Model-View-Control (MVC) 設(shè)計(jì)模式的前提下膘怕,在 iOS 應(yīng)用中使用集合視圖涩赢。是時(shí)候嘗試新東西了:代碼控淡。本章一開始很簡(jiǎn)單蒜哀,展示了如何使用 storyboards.xibs 來(lái)設(shè)置集合視圖兜畸,然后告訴你如何在代碼中設(shè)置它們旨怠。集合視圖擴(kuò)展了它的父類 UIScrollView,所以本章簡(jiǎn)單的繞了一圈敢会,展示如何使用 UIScrollViewDelegate 來(lái)發(fā)揮你的優(yōu)勢(shì)曾沈。在結(jié)束關(guān)于性能的案例研究之前,你將開始使用單元格重用來(lái)定制實(shí)際的內(nèi)容以顯示給你的用戶鸥昏。

使用代碼和 Storyboards 進(jìn)行設(shè)置

傳統(tǒng)意義上塞俱,.xib 文件用于為 OS X 和 iOS 應(yīng)用設(shè)置并布局 UI 代碼。這些文件是你的界面的 "凍結(jié) "版本互广,會(huì)在運(yùn)行時(shí)解凍敛腌。.xibs 文件的好處是卧土,它們很容易用來(lái)創(chuàng)建基本的接口惫皱;通常每個(gè).xib 都有一個(gè) UIViewController 的實(shí)例。

Storyboards 首次被引入是在 2011年的 iOS 5 中尤莺,它使開發(fā)者能夠直觀地布局視圖控制器之間的交互旅敷。開發(fā)者不僅可以可視化視圖控制器之間的連接,還可以定義整個(gè)應(yīng)用如何從一個(gè)視圖控制器過渡到另一個(gè)視圖控制器颤霎。Storyboards 的關(guān)鍵之處在于它們的效率媳谁;一個(gè)巨大的 .xib 文件,必須完全加載到內(nèi)存中友酱,會(huì)延遲你的應(yīng)用程序啟動(dòng)的時(shí)間晴音。Storyboards 可以有效地只延遲加載(Lazy Loading)必要的視圖控制器。

當(dāng)然缔杉,任何可以在 .xib 文件或 storyboard 中做的事情都可以完全使用手寫代碼來(lái)實(shí)現(xiàn)锤躁。如果你正在將集合視圖集成到你現(xiàn)有的應(yīng)用程序中,該應(yīng)用程序使用 .xib 文件或 storyboard或详,繼續(xù)使用它們可能會(huì)很方便系羞。然而,由于集合視圖需要使用代碼來(lái)進(jìn)行布局霸琴,因此完全避免使用 .xib 和 storyboard 往往更容易椒振。盡管如此,本章還是闡述了如何使用 storyboard 設(shè)置上一章的集合視圖梧乘,然后再演示完全使用代碼進(jìn)行設(shè)置澎迎。

使用 "Single View template" 模板創(chuàng)建一個(gè)新的 Xcode 項(xiàng)目。確保 "Use Storyboards" 被選中选调。打開 Main.storyboard 文件夹供,刪除已經(jīng)存在的視圖控制器。從右側(cè)窗格的對(duì)象庫(kù)中拖動(dòng)一個(gè)集合視圖控制器到空畫布上学歧,如圖2.1 所示罩引。

(略)

你現(xiàn)在就可以運(yùn)行這個(gè)應(yīng)用,它可以正常工作枝笨,但會(huì)很無(wú)聊袁铐。storyboard 文件中已經(jīng)默認(rèn)設(shè)置了集合視圖的委托和數(shù)據(jù)源插座連接揭蜒,指向你的集合視圖控制器。下一步是自定義該視圖控制器的實(shí)際功能剔桨。這部分很簡(jiǎn)單屉更,因?yàn)槟阒恍枰獜?fù)制第1章 "理解 iOS 中的 Model-View-Controller "中的現(xiàn)有代碼。

打開你的視圖控制器的頭文件洒缀,改變它繼承自哪個(gè)類(將 UIViewController 改為 UICollectionViewController)瑰谜。然后把上一章的實(shí)現(xiàn)文件完整地復(fù)制過來(lái)。最后树绩,重要的一步是告訴你的 storyboard 應(yīng)該使用哪個(gè)視圖控制器萨脑。點(diǎn)擊 storyboard 中的集合視圖控制器,打開 "Identity Inspector"饺饭。在 Class 的地方渤早,你會(huì)看到默認(rèn)的占位符是 UICollectionViewController。無(wú)聊! 用你的視圖控制器的名字來(lái)代替--在我的例子中瘫俊,它是 AFViewController鹊杖。

這一步至關(guān)重要,它是 storyboard 如何知道在布局集合視圖時(shí)要執(zhí)行什么代碼的原因扛芽。運(yùn)行你的應(yīng)用程序骂蓖,你會(huì)看到和第1章一樣的輸出。

使用 storyboard 或 .xibs 文件時(shí)川尖,你可以在不編寫任何代碼的情況下更改集合視圖的視覺顯示樣式登下。在 storyboards 中選擇集合視圖,然后打開 "Attributes Inspector"空厌。在這里庐船,你可以將集合視圖的滾動(dòng)方向從默認(rèn)的垂直方向改為水平方向。你還可以更改集合視圖屬于其父類 UIScrollView 的屬性嘲更。將滾動(dòng)指示器 "樣式 "改為白色筐钟,使?jié)L動(dòng)指示器在黑色背景下可見。

打開 "Size Inspector"赋朦,你可以改變集合視圖布局的屬性篓冲,如圖2.2所示(集合視圖將這些屬性抽象到它們的布局對(duì)象中,更多內(nèi)容請(qǐng)閱讀第3章 "內(nèi)容的上下文")宠哄。在這里壹将,你可以改變單元格的大小,默認(rèn)情況下是 50*50毛嫉。將寬度降為 20诽俯,并保持高度設(shè)置為 50。頁(yè)眉和頁(yè)腳大小還不能用承粤,因?yàn)槟氵€沒有使用頁(yè)眉或頁(yè)腳暴区。

圖2.2

你可以在 "Size Inspector" 中的 "Min Spacing " 部分更改集合視圖中單元格之間的距離闯团。這只是最小距離;默認(rèn)布局(稱為 "流")確保單元格之間的距離最小仙粱。通過 "Size Inspector" 中的 "Section Insets" 區(qū)域房交,你可以指定整個(gè) Section 段周圍的距離。(請(qǐng)記住伐割,到目前為止候味,你只有一個(gè) Section)。我們將在第 3 章中更仔細(xì)地了解 "section insets" 屬性隔心,所以現(xiàn)在不用擔(dān)心具體細(xì)節(jié)白群。我個(gè)人最討厭內(nèi)容周圍的邊距太小,所以把 "section insets" 各個(gè)方向上的值設(shè)置為 10济炎。

上圖示例中川抡,我們?cè)O(shè)置了該集合視圖的 section 的邊緣插入量為 {10,10,10,10}辐真,默認(rèn)值為 {0,0,0,0}须尚。

運(yùn)行應(yīng)用程序,看看集合視圖中的視覺差異侍咱。它應(yīng)該類似于圖 2.3耐床。

圖 2.3

一點(diǎn)都不賴。不要擔(dān)心系統(tǒng)狀態(tài)欄在我們的內(nèi)容頁(yè)面上是可見的楔脯,這是 iOS 7 的默認(rèn)值撩轰。我們稍后會(huì)通過將集合視圖控制器放在導(dǎo)航控制器里面來(lái)解決這個(gè)問題。圖 2.3 的問題是昧廷,只有集合視圖布局的部分屬性可以通過 storyboards 或 .xib 文件訪問堪嫂。此外,如果你在代碼中覆蓋了你在 storyboard 中設(shè)置的屬性木柬,或者你忘記了你在 storyboards 中設(shè)置了一些東西皆串,這可能會(huì)導(dǎo)致調(diào)試上的困難。出于這個(gè)原因眉枕,我強(qiáng)烈建議對(duì)集合視圖使用純代碼的方法恶复。

你也可以通過只使用代碼的方式重新創(chuàng)建你的界面。使用空應(yīng)用程序模板創(chuàng)建一個(gè)新的 Xcode 項(xiàng)目速挑。(對(duì)于從未從空模板創(chuàng)建過應(yīng)用程序的人來(lái)說谤牡,這可能是一個(gè)很大的步驟)。使用 File姥宝、New翅萤、File 或 ?N 創(chuàng)建一個(gè)新文件。選擇 Objective-C 類腊满,并將其命名為 AFViewController 之類套么。在 Subclass 的字段中流纹,輸入UICollectionViewController。確保不要選擇 User Interface With XIB违诗。

打開 AppDelegate.m 文件漱凝,并添加一個(gè) #import 語(yǔ)句來(lái)導(dǎo)入新視圖控制器的頭文件。將實(shí)現(xiàn)改為清單2.1中的代碼诸迟。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    UICollectionViewFlowLayout *collectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
    collectionViewLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    collectionViewLayout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);  
    collectionViewLayout.itemSize = CGSizeMake(20, 50); 
    self.window.rootViewController = [[AFViewController alloc] initWithCollectionViewLayout:collectionViewLayout];
    self.window.backgroundColor = [UIColor whiteColor]; 
    [self.window makeKeyAndVisible];

    return YES;
}

下一步茸炒,打開視圖控制器的實(shí)現(xiàn)文件,并在 viewDidLoad 方法中添加下面代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.collectionView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
}

(其他設(shè)置與上一個(gè)示例相同)

構(gòu)建并運(yùn)行應(yīng)用程序阵苇,你會(huì)發(fā)現(xiàn)你使用 storyboards 定制的所有東西都已經(jīng)用代碼復(fù)制了壁公。擊掌!

在你深入了解集合視圖和布局內(nèi)容之前,下面的部分將帶你快速轉(zhuǎn)移討論 UIScrollView绅项。

UIScrollView:簡(jiǎn)要概述

UICollectionViewUIScrollView 的子類紊册,和 UITableView 很類似。與UICollectionView 的繼承關(guān)系類似快耿,UICollectionViewDelegate 協(xié)議也遵守 UIScrollViewDelegate 協(xié)議囊陡。在實(shí)際操作中,這意味著如果一個(gè)對(duì)象是集合視圖的委托者掀亥,它就會(huì)收到回調(diào)通知撞反,觸發(fā) UICollectionViewDelegate 事件以及 UIScrollViewDelegate 事件。

注:也就是說搪花,如果一個(gè)對(duì)象是 UICollectionView 實(shí)例對(duì)象的委托對(duì)象遏片,那么這個(gè)委托對(duì)象既遵守 UICollectionViewDelegate 協(xié)議中的方法,同時(shí)也自動(dòng)遵守 UIScrollViewDelegate 協(xié)議中的方法撮竿。因?yàn)?UICollectionViewDelegate 協(xié)議繼承自 UIScrollViewDelegate 協(xié)議吮便,它是 UIScrollViewDelegate 協(xié)議的子協(xié)議。

UIScrollView 是 UIKit 中的一個(gè)多功能類幢踏,從 iOS 2.0 的時(shí)候就已經(jīng)存在了髓需。它為開發(fā)人員提供了一種友好的方式來(lái)滾動(dòng)內(nèi)容,無(wú)論是電子郵件列表惑折、應(yīng)用程序的網(wǎng)格授账,還是一張照片。如果你可以在任何給定的應(yīng)用程序中滾動(dòng)一些東西惨驶,那么該應(yīng)用程序有可能使用了 UIScrollView 滾動(dòng)視圖白热。

滾動(dòng)視圖給用戶一種熟悉的感覺,讓任何使用滾動(dòng)視圖的應(yīng)用看起來(lái)更像是屬于 iOS 系統(tǒng)粗卜,而不像其開發(fā)者自己寫的滾動(dòng)視圖屋确。滾動(dòng)視圖以極少的工作為開發(fā)者提供了很大的權(quán)力,開發(fā)者需要做的就是設(shè)置滾動(dòng)視圖并為其添加子視圖。此外攻臀,你還可以依靠蘋果已經(jīng)為你做的工作焕数,比如模擬物理學(xué)和減速∨傩ィ看看一個(gè)例子堡赔,在這個(gè)例子中,用戶可以滾動(dòng)查看比屏幕上能同時(shí)容納的更多內(nèi)容设联。

使用 "Single View template" 模板創(chuàng)建一個(gè)新的 Xcode 項(xiàng)目善已。將一張大圖片復(fù)制到項(xiàng)目中,打開主視圖控制器的實(shí)現(xiàn)文件离例。用清單2.3中的實(shí)現(xiàn)替換 viewDidLoad换团。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIImage *image = [UIImage imageNamed:@"cat.jpg"];
    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
    imageView.frame = CGRectMake(0, 0, image.size.width, image.size.height);
    
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    scrollView.contentSize = image.size;
    scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  
    [scrollView addSubview:imageView];
    [self.view addSubview:scrollView];
}

運(yùn)行應(yīng)用程序,你會(huì)看到類似于圖 2.4 的顯示效果宫蛆;由于圖片太大了艘包,一次無(wú)法在屏幕上完整顯示,但用戶可以圍繞圖像滾動(dòng)以查看全部?jī)?nèi)容耀盗。(請(qǐng)注意滾動(dòng)指示器想虎。) 使這一切發(fā)揮作用的魔法是 contentSize 屬性。這是一個(gè) CGSize 值袍冷,表示可滾動(dòng)區(qū)域的尺寸大辛状住(以點(diǎn)為單位)。它的默認(rèn)值為零胡诗,而且使用任何滾動(dòng)視圖時(shí)必須設(shè)置這個(gè)屬性的值,即使內(nèi)容尺寸小于滾動(dòng)視圖自身的尺寸淌友。

圖 2.4

當(dāng)滾動(dòng)視圖知道它所顯示的內(nèi)容的尺寸大小時(shí)煌恢,它就會(huì)滾動(dòng)。內(nèi)容尺寸(contentSize)可以隨時(shí)改變震庭。

圖2.5 展示了內(nèi)容尺寸的概念瑰抵。照片左上角的亮色矩形區(qū)域,定義了應(yīng)用程序第一次啟動(dòng)時(shí)圖像的可見部分器联,這就是滾動(dòng)視圖的尺寸(the size of the scroll view )二汛,用虛線表示。實(shí)線代表滾動(dòng)視圖的內(nèi)容大胁ν亍(the content size of the scroll view)肴颊。

圖 2.5

當(dāng)用戶滾動(dòng) scrollView 時(shí),用戶可見的內(nèi)容區(qū)域會(huì)發(fā)生變化渣磷。內(nèi)容視圖在滾動(dòng)視圖中的位置稱為內(nèi)容偏移婿着,由 contentOffset 屬性表示,是一個(gè) CGPoint 值。該屬性由可見區(qū)域的原點(diǎn)(左上角)到內(nèi)容原點(diǎn)的距離定義竟宋。圖2.6 用白色箭頭演示了內(nèi)容偏移提完。內(nèi)容大小保持不變,但內(nèi)容偏移量會(huì)改變丘侠,以響應(yīng)用戶的交互徒欣。

圖 2.6

內(nèi)容偏移可以通過編程方式改變,contentOffset 屬性是可讀可寫的蜗字。更有趣的是帚称,你可以使用 setContentOffset:animated: 方法以動(dòng)畫方式對(duì)內(nèi)容偏移的變化進(jìn)行處理。這將 "移動(dòng) "滾動(dòng)視圖秽澳,就像用戶自己移動(dòng)它一樣闯睹。內(nèi)容偏移也可以用 scrollRectToVisible:animated: 方法來(lái)改變,但這更多的是用于縮放而不是簡(jiǎn)單的滾動(dòng)担神。

關(guān)于 scrollView楼吃,我想說的最后一件事是 contentInset 屬性。這是一個(gè) UIEdgeInset 值妄讯,表示滾動(dòng)視圖內(nèi)容周圍應(yīng)該 "填充" 的區(qū)域孩锡。將contentInset 屬性設(shè)置為 UIEdgeInsetsMake(10, 10, 10, 10),將在 scrollView 的內(nèi)容周圍創(chuàng)建一個(gè)10pt 的邊距亥贸。邊緣插入值也可以是負(fù)值躬窜;這將代表滾動(dòng)視圖內(nèi)容周圍不能被用戶看到的區(qū)域(除非她滾動(dòng)過滾動(dòng)視圖的邊緣)。試著玩玩 contentInset炕置,看看它是如何工作的荣挨。

contentInset 屬性是一個(gè)廣泛使用的屬性,經(jīng)常被用于 UITableView 和自定義下拉刷新控件中朴摊。如果你在視圖控制器的頂部有一個(gè)導(dǎo)航欄默垄,并且 wantsFullScreenLayout 設(shè)置為 YES,那么它也很有用甚纲。邊緣插入量的上邊界的值等于狀態(tài)欄和導(dǎo)航欄的高度口锭。

這就是 UIScrollView 的三個(gè)主要組件:contentSizecontentOffsetcontentInset介杆。

  • contentSize 用來(lái)標(biāo)識(shí) UIScrollView 的可滾動(dòng)范圍鹃操;
  • contentOffset 用來(lái)設(shè)置 UIScrollView 的視圖原點(diǎn)與當(dāng)前可視區(qū)域左上角的距離;
  • contentInset 用于設(shè)置邊緣插入量春哨,或者說荆隘,額外的視圖內(nèi)邊距;

現(xiàn)在悲靴,在本章繼續(xù)討論更多的集合視圖之前臭胜,是時(shí)候?qū)L動(dòng)視圖委托(UIScrollViewDelegate)進(jìn)行快速討論了莫其。

UIScrollViewDelegate 中有三組方法:響應(yīng)拖動(dòng)和滾動(dòng)的方法,響應(yīng)縮放的方法耸三,以及響應(yīng)由代碼顯式啟動(dòng)的滾動(dòng)動(dòng)畫的方法(見表2.1)乱陡。你將只處理第一組和最后一組,因?yàn)榧弦晥D不使用 UIScrollView 的縮放功能仪壮。

表 2.1 有用的 UIScrollViewDelegate 方法

方法名 描述
scrollViewDidScroll: 當(dāng) scrollView 的內(nèi)容偏移(contentOffset)發(fā)生變化時(shí)憨颠,就會(huì)被調(diào)用,可以是代碼觸發(fā)的變化积锅,也可以是響應(yīng)用戶交互觸發(fā)的變化爽彤。可用于自定義的下拉刷新控件中缚陷。
scrollViewWillBeginDragging: 當(dāng) scrollView 即將被用戶拖動(dòng)時(shí)調(diào)用适篙。可能的用途是禁止?jié)L動(dòng)視圖的更新(暫停一些復(fù)雜操作)箫爷,因?yàn)檫@可能會(huì)影響滾動(dòng)的流暢性嚷节。
scrollViewWillEndDragging: withVelocity: targetContentOffset: 當(dāng)用戶在拖動(dòng)后從 scrollView 上抬起手指時(shí),就會(huì)被調(diào)用虎锚。第二個(gè)參數(shù)表示結(jié)束拖動(dòng)時(shí)的滾動(dòng)速度硫痰,以點(diǎn)/秒為單位,表示當(dāng)用戶抬起手指時(shí)窜护,滾動(dòng)視圖的速度效斑。第三個(gè)參數(shù)是一個(gè) CGPoint 類型的指針類型,代表 scrollView 將滾動(dòng)到的位置柱徙。修改該 CGPoint 值就會(huì)改變滾動(dòng)視圖的滾動(dòng)位置缓屠。可能的用途是計(jì)算當(dāng)滾動(dòng)動(dòng)畫結(jié)束時(shí)什么內(nèi)容將是可見的坐搔,并從應(yīng)用程序編程接口(API)中預(yù)取藏研。
scrollViewDidEndDragging: willDecelerate: 當(dāng)用戶在 scrollView 上拖動(dòng)后抬起手指時(shí),就會(huì)被調(diào)用概行。第二個(gè)參數(shù)表示 scrollView 是否以動(dòng)畫形式停止減速,或者當(dāng)用戶抬起手指時(shí)是否已經(jīng)停止弧岳。只要第二個(gè)參數(shù)是 NO凳忙,可能的用途包括重啟在scrollViewWillBeginDragging: 中停止的任何暫停的計(jì)算。(注:這個(gè) decelerate 參數(shù)表示滾動(dòng)視圖是緩慢減速的還是戛然而止立即減速的)
scrollViewShouldScrollToTop: 當(dāng)操作系統(tǒng)需要確定當(dāng)用戶點(diǎn)擊系統(tǒng)狀態(tài)欄時(shí)禽炬,是否應(yīng)該將滾動(dòng)視圖以動(dòng)畫方式自動(dòng)滾動(dòng)到頂部時(shí)涧卵,就會(huì)調(diào)用該方法。每次只有一個(gè)可見的滾動(dòng)視圖應(yīng)該從這個(gè)方法返回 YES腹尖。
scrollViewDidScrollToTop: 在滾動(dòng)視圖因用戶點(diǎn)擊狀態(tài)欄而滾動(dòng)到頂部后調(diào)用柳恐。
scrollViewWillBeginDecelerating: 當(dāng)滾動(dòng)視圖即將開始減速動(dòng)畫時(shí)調(diào)用。
scrollViewDidEndDecelerating: 在滾動(dòng)視圖的減速動(dòng)畫完成后調(diào)用±稚瑁可能的用途包括重新啟動(dòng)在 scrollViewWillBeginDragging: 方法中暫停的復(fù)雜計(jì)算讼庇。
scrollViewDidEndScrollingAnimation: 當(dāng)滾動(dòng)視圖的內(nèi)容偏移量(contentOffset)以動(dòng)畫方式變化完成后調(diào)用。只有當(dāng)內(nèi)容偏移量是以編程方式改變并且啟用了顯式動(dòng)畫時(shí)近尚,才會(huì)在委托者上調(diào)用該方法蠕啄。

在本書以后更進(jìn)階的章節(jié)和一些案例研究中,你會(huì)用到一些滾動(dòng)視圖的委托方法戈锻。它們是解決許多問題的有用工具,你應(yīng)該了解它們。

UICollectionViewCell 的重用:如何以及為何重用

UICollectionView 使用一種節(jié)省內(nèi)存的方案來(lái)配置各個(gè)單元格的顯示涵紊。正如蘋果公司的一位軟件工程師所說的那樣揣苏,"創(chuàng)建并分配內(nèi)存十分昂貴"。他的意思是拒迅,如果你做了很多為新的變量創(chuàng)建并分配內(nèi)存的操作骚秦,就非常消耗系統(tǒng)內(nèi)存。UICollectionView 的做法非常聰明:它重用不再顯示的單元格坪它。

Note

對(duì)于熟悉 UITableView 的人來(lái)說骤竹,這應(yīng)該聽起來(lái)很熟悉。在 iOS 6 中往毡,蘋果將 UITableView 最好的部分做成了 UICollectionView蒙揣。許多東西看起來(lái)很熟悉,但你可能會(huì)對(duì)很多新東西感到驚訝开瞭。

這和列表中 UITableViewCell 的重用原理類似懒震。

UICollectionView 依靠它的 dataSource 告訴它要顯示多少個(gè)單元格,并在向用戶展示之前對(duì)每個(gè)單元格進(jìn)行配置嗤详。在滾動(dòng)時(shí)个扰,這需要非常快的速度葱色,這就是為什么單元格需要重用的原因递宅。下面解釋一下具體發(fā)生了什么。

對(duì)于不同的單元格顯示類型苍狰,你應(yīng)該使用不同的單元格重用標(biāo)識(shí)符办龄。重用標(biāo)識(shí)符是一個(gè) NSString 類型的字符串,你通常將其存儲(chǔ)為一個(gè)靜態(tài)變量淋昭。在具有該重用標(biāo)識(shí)符的任何單元格能夠顯示之前俐填,它需要在集合視圖中注冊(cè)。這與 UITableView 有很大的不同翔忽。你通常會(huì)在 viewDidLoad 中注冊(cè)單元格英融,而不會(huì)在以后重新注冊(cè)它們盏檐。

當(dāng)注冊(cè)一個(gè)單元格時(shí),你可以提供一個(gè) UINib 實(shí)例或一個(gè) Class 類驶悟。我更喜歡 Class 類而不是 nib胡野,因?yàn)樗茏屛覍?duì)布局和性能有更多的控制。

使用 registerClass:forCellWithReuseIdentifier:registerNib:forCellWithReuseIdentifier: 方法注冊(cè)單元格撩银。自此给涕,每當(dāng)調(diào)用 dequeueReusableCellWithReuseIdentifier:forIndexPath: 方法時(shí)。系統(tǒng)會(huì)保證你有一個(gè)與你的重用標(biāo)識(shí)符相對(duì)應(yīng)的已分配和初始化好的單元格(見圖2.7)额获。

圖 2.7

這與 UITableView 稍有不同够庙,UITableView 在歷史上要求開發(fā)人員在嘗試去序列化一個(gè)單元格時(shí)首先檢查返回值是否為 nil(盡管現(xiàn)在它也支持這種先注冊(cè)再使用的新方法了)。在集合視圖中抄邀,系統(tǒng)會(huì)保證為你返回一個(gè)有效可使用的單元格耘眨。

如果你的集合視圖只有 20 個(gè)單元格同時(shí)在屏幕上可見,那么你的集合視圖只分配了 20 個(gè)單元格境肾;當(dāng)一個(gè)單元格滾動(dòng)到屏幕外時(shí)剔难,它將被添加到重用隊(duì)列中,以便再次重用奥喻。這種技術(shù)可以讓應(yīng)用程序在滾動(dòng)瀏覽具有數(shù)百或數(shù)千個(gè)單元格的集合視圖時(shí)偶宫,保持極低的內(nèi)存占用和極高的幀率。

本書中的大多數(shù)例子环鲤,以及集合視圖的大多數(shù)實(shí)際用途纯趋,都只顯示一種類型的單元格,因此只有一個(gè)重用標(biāo)識(shí)符冷离。如果你要顯示不止一種類型的單元格吵冒,那么擁有不止一種類型的標(biāo)識(shí)符是完全合理的。

向用戶展示內(nèi)容

好吧西剥!你已經(jīng)完成了 MVC 的一章和 UICollectionView 的半章基礎(chǔ)知識(shí)”云埽現(xiàn)在是時(shí)候看一些代碼了。

你將創(chuàng)建一個(gè)基本的應(yīng)用程序瞭空,向用戶顯示一些自定義內(nèi)容揪阿。這將是一個(gè) iPad 應(yīng)用,所以你可以使用非常大的單元格咆畏。一開始你要做的是建立一個(gè)基本的集合視圖图甜,讓用戶可以通過加號(hào)按鈕添加新的單元格,單元格會(huì)顯示它們被添加的時(shí)間鳖眼。這只是為后面的內(nèi)容做熱身。

使用 Empty Application 模板創(chuàng)建一個(gè)新的 Xcode 項(xiàng)目嚼摩。創(chuàng)建一個(gè)新的文件钦讳,一個(gè)父類為 UICollectionViewController 的 Objective-C 類矿瘦,并給它一個(gè)合適的名字。在你的應(yīng)用程序委托的實(shí)現(xiàn)文件中愿卒,#import 視圖控制器的頭缚去,并創(chuàng)建一個(gè)視圖控制器的實(shí)例,作為導(dǎo)航控制器的根視圖控制器琼开,即窗口的根視圖控制器易结。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor];
    
    UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
    AFViewController *viewController = [[AFViewController alloc] initWithCollectionViewLayout:flowLayout];
    
    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
    navigationController.navigationBar.barStyle = UIBarStyleBlack;
    self.window.rootViewController = navigationController;
    
    [self.window makeKeyAndVisible];
    return YES;
}

你會(huì)使用到 UINavigationController,因?yàn)樗赓M(fèi)提供了很多好東西柜候。在本例中搞动,你得到了一個(gè)很酷的導(dǎo)航欄,你可以在上面加入按鈕渣刷。這個(gè) applicationDidFinishLaunchingWithOptions: 的實(shí)現(xiàn)比本章前面的例子更輕量級(jí)鹦肿;這次你將更接近遵循一些 "最佳實(shí)踐"。app delegate 只是為視圖控制器創(chuàng)建了基本的東西辅柴,它還進(jìn)一步定制了自己箩溃。

創(chuàng)建一個(gè)新的 Objective-C 類,它繼承 UICollectionViewCell碌嘀。你還不打算給它添加任何代碼涣旨。你只需要在視圖控制器的實(shí)現(xiàn)文件中#import 導(dǎo)入即可。

打開視圖控制器的實(shí)現(xiàn)文件股冗,用一些指示性的值創(chuàng)建一個(gè)靜態(tài)的NSString 實(shí)例霹陡;你將用它作為你的重用標(biāo)識(shí)符。添加兩個(gè)實(shí)例變量魁瞪。一個(gè)是代表模型的 NSMutableArray穆律,另一個(gè)是 NSDateFormatter,你將用它來(lái)格式化內(nèi)容給用戶导俘。

#import "AFCollectionViewCell.h"

@interface AFViewController ()

@end

static NSString *CellIdentifier = @"Cell Identifier";

@implementation AFViewController
{
    // This is our model
    NSMutableArray *datesArray;
    NSDateFormatter *dateFormatter;
}

接下來(lái)峦耘,在 viewDidLoad 方法中創(chuàng)建并初始化一個(gè)空模型(你的 datesArray)和一個(gè)日期格式化對(duì)象的實(shí)例。同時(shí)將你的布局和集合視圖配置成漂亮的樣子旅薄,通過重用標(biāo)識(shí)符注冊(cè)你的 UICollectionViewCell 子類辅髓,并為你的導(dǎo)航欄添加一個(gè)按鈕。

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // 實(shí)例化模型
    datesArray = [NSMutableArray array];
    dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:[NSDateFormatter dateFormatFromTemplate:@"h:mm:ss a" options:0 locale:[NSLocale currentLocale]]];
    
    // 初始化集合視圖布局
    UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
    flowLayout.minimumInteritemSpacing = 40.0f;
    flowLayout.minimumLineSpacing = 40.0f;
    flowLayout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
    flowLayout.itemSize = CGSizeMake(200, 200);
    
    // 配置集合視圖
    [self.collectionView registerClass:[AFCollectionViewCell class] forCellWithReuseIdentifier:CellIdentifier];
    self.collectionView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
    
    // 配置導(dǎo)航欄按鈕
    UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(userTappedAddButton:)];
    self.navigationItem.rightBarButtonItem = addButton;
    self.navigationItem.title = @"Our Time Machine";
}

真棒少梁,你現(xiàn)在就可以運(yùn)行這個(gè)應(yīng)用程序洛口,但你看到的只是一個(gè)空屏幕,上面有一個(gè)加號(hào)按鈕和一個(gè)標(biāo)題凯沪。所以第焰,在編寫你的集合視圖單元子類之前,先完成視圖控制器的代碼妨马。你需要實(shí)現(xiàn)你的UICollectionViewDataSource 方法挺举。

#pragma mark - UICollectionViewDataSource Methods

-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return datesArray.count;
}

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    AFCollectionViewCell *cell = (AFCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
    
    cell.text = [dateFormatter stringFromDate:datesArray[indexPath.row]];
    
    return cell;
}

現(xiàn)在杀赢,這將引發(fā)一個(gè)編譯器錯(cuò)誤。不過不要擔(dān)心湘纵。在你寫完剩下的代碼后脂崔,它將會(huì)工作。你需要一個(gè)方法來(lái)響應(yīng)你的 add 按鈕梧喷。創(chuàng)建兩個(gè)方法:一個(gè)用于設(shè)置你在 viewDidLoad 中給導(dǎo)航欄按鈕的選擇器名稱砌左,另一個(gè)是你可以在代碼中的任何地方調(diào)用的方法,以便向 datesArray 中添加新的日期铺敌。

#pragma mark - User Interface Interaction Methods

-(void)userTappedAddButton:(id)sender {
    [self addNewDate];
}

#pragma mark - Private, Custom methods

-(void)addNewDate {
    // performBatchUpdates: 批量更新集合視圖
    [self.collectionView performBatchUpdates:^{
        //create a new date object and update our model
        NSDate *newDate = [NSDate date];
        [datesArray insertObject:newDate atIndex:0];
        
        //update our collection view
        [self.collectionView insertItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:0]]];
    } completion:nil];
}

你在 UICollectionView 上調(diào)用 performBatchUpdates:completion: 執(zhí)行批量更新汇歹。這可以讓你免費(fèi)獲得動(dòng)畫(由你的布局類定義;更多內(nèi)容在第3章)适刀。

現(xiàn)在你要做的就是編寫你的 UICollectionViewCell 子類秤朗。轉(zhuǎn)到你之前創(chuàng)建的頭文件。你要給它一個(gè)單一的 NSString 屬性笔喉。

@interface AFCollectionViewCell : UICollectionViewCell

@property (nonatomic, copy) NSString *text;

@end

現(xiàn)在你的編譯器將停止抱怨取视,但如果你運(yùn)行應(yīng)用程序,將不會(huì)發(fā)生任何真正有趣的事情常挚。打開單元格的實(shí)現(xiàn)文件作谭,添加一個(gè) UILabel 實(shí)例變量。用以下代碼實(shí)現(xiàn)覆蓋 initWithFrame: 方法奄毡。

@implementation AFCollectionViewCell
{
    UILabel *textLabel;
}

#pragma mark - Initialization

- (id)initWithFrame:(CGRect)frame
{
    if (!(self = [super initWithFrame:frame])) return nil;
    
    self.backgroundColor = [UIColor whiteColor];
    
    textLabel = [[UILabel alloc] initWithFrame:self.bounds];
    textLabel.textAlignment = NSTextAlignmentCenter;
    textLabel.font = [UIFont boldSystemFontOfSize:20];
    [self.contentView addSubview:textLabel];
    
    return self;
}

接下來(lái)折欠,覆蓋文本屬性來(lái)更新標(biāo)簽。你還將覆蓋 UICollectionViewCell 的一個(gè)重要方法吼过,稱為prepareForReuse锐秦。

#pragma mark - Overriden UICollectionViewCell methods

-(void)prepareForReuse
{
    [super prepareForReuse];
    
    self.text = @"";
}

#pragma mark - Overriden properties

-(void)setText:(NSString *)text
{
    _text = [text copy];
    
    textLabel.text = self.text;
}

這里使用名為 text 屬性的字符串更新單元格的標(biāo)簽。在 prepareForReuse 方法中盗忱,你調(diào)用 super 關(guān)鍵字(非常重要=创病),然后將你的文本設(shè)置為空字符串趟佃。這一點(diǎn)真的很重要扇谣,你需要盡可能地將你的單元格重置到它的起始或中性狀態(tài)。否則闲昭,集合視圖的數(shù)據(jù)源可能會(huì)忘記重置部分?jǐn)?shù)據(jù)罐寨,你可能最終會(huì)得到一個(gè)不一致和混亂的用戶界面。

運(yùn)行應(yīng)用程序序矩,你會(huì)看到一個(gè)空屏幕鸯绿。點(diǎn)擊 "加號(hào) "按鈕,在收集視圖中添加一個(gè)新單元格。請(qǐng)注意楞慈,當(dāng)一個(gè)新單元格被添加到集合視圖的頂部時(shí)幔烛,你會(huì)得到一個(gè)動(dòng)畫(見圖 2.8)。很好囊蓝!該應(yīng)用還自適應(yīng)屏幕旋轉(zhuǎn)。

圖 2.8

我不想讓這個(gè)教程聽起來(lái)像個(gè)關(guān)于 MVC 的破紀(jì)錄令蛉,但重要的是要注意聚霜,單元格并不知道它在顯示什么;它傳遞了一個(gè)字符串珠叔,而這個(gè)字符串恰好包含了一個(gè)與模型對(duì)應(yīng)的日期蝎宇。重要的是,你并沒有向它傳遞 NSDate 對(duì)象本身祷安。

現(xiàn)在姥芥,你有了一個(gè)基本的集合視圖示例,再仔細(xì)看看 UICollectionView類本身汇鞭。單元格有兩個(gè)重要的布爾屬性:選中高亮凉唐。高亮狀態(tài)完全取決于用戶的交互;當(dāng)用戶的手指按住一個(gè)單元格時(shí)霍骄,它就會(huì)自動(dòng)變成高亮狀態(tài)台囱。單元格的選中則不那么短暫;當(dāng)用戶抬起手指時(shí)读整,單元格就會(huì)被選中(如果集合視圖支持選擇)簿训。單元格會(huì)一直被選中,直到你寫的一些代碼將其取消米间,或者直到用戶再次點(diǎn)擊它們强品。當(dāng)被點(diǎn)擊成為選中或取消選中時(shí),單元格會(huì)暫時(shí)高亮屈糊。這些屬性的設(shè)置器可以(并且經(jīng)常)從動(dòng)畫塊中調(diào)用的榛。在覆蓋它們的實(shí)現(xiàn)時(shí)要注意,你所做的改變很可能會(huì)被隱式動(dòng)畫化另玖。

選中和高亮可能會(huì)讓人感到困惑困曙。不過不用擔(dān)心,因?yàn)橄乱粋€(gè)例子將對(duì)其進(jìn)行更多的探討谦去。同時(shí)慷丽,圖 2.9 應(yīng)該會(huì)有所幫助。

圖 2.9

在上一次練習(xí)中的自定義子類中鳄哭,你將 UILabel 子視圖添加到 self.contentView 中要糊,而不是 self 中。一般來(lái)說妆丘,你不應(yīng)該直接將子視圖添加到集合視圖單元中锄俄。這就是為什么你應(yīng)該總是將它們添加到其 contentView 中局劲。

UICollectionViewCell 有三個(gè)子視圖,在圖 2.10 中表示奶赠。后面的黑色矩形是集合視圖單元格本身鱼填,前面的綠色視圖是 contentView,你可以在那里添加子視圖毅戈。中間的兩個(gè)視圖是 selectedBackgroundViewbackgroundView苹丸。這兩個(gè)視圖都是可選的,可以在任何時(shí)候設(shè)置苇经。backgroundView 如果設(shè)置了赘理,就會(huì)永久存在。

圖 2.10

現(xiàn)在你對(duì) UICollectionViewCell 中的視圖層次結(jié)構(gòu)有了更好的理解扇单,你可以繼續(xù)看另一個(gè)例子商模,它有助于說明這些屬性、contentView 和圖像的用途蜘澜。

您將創(chuàng)建一個(gè)應(yīng)用程序施流,在 10 個(gè)不同的 section 區(qū)域中重復(fù)顯示 12 張圖片,每個(gè)部分都將有自己的背景顏色兼都,除非它被選中嫂沉,演示如何使用 selectedBackgroundView。你使用 12 張圖片是因?yàn)樗鼈冋每梢栽谝粋€(gè) section 中充滿屏幕顯示扮碧。

基于 Empty 模板創(chuàng)建一個(gè)新的 Xcode 項(xiàng)目趟章。創(chuàng)建一個(gè)UICollectionViewController 的子類和一個(gè) UICollectionViewCell 的子類,就像上次一樣慎王。在應(yīng)用程序委托中設(shè)置一個(gè)視圖控制器的實(shí)例作為窗口的根視圖控制器--這次不需要使用導(dǎo)航控制器蚓土。

使用兩個(gè)數(shù)組來(lái)存儲(chǔ)你的模型:一個(gè)用于圖像,一個(gè)用于存儲(chǔ)背景色赖淤。你將調(diào)整上一個(gè)例子中的單元格大小蜀漆、單元格之間的間距和行間距。此外咱旱,你將在集合視圖上啟用多重選擇功能确丢;這將使用戶能夠同時(shí)選擇多個(gè)單元格,也使用戶能夠通過點(diǎn)擊它們來(lái)取消選擇單元格吐限。以下示例代碼中鲜侥, viewDidLoad 方法中的所有內(nèi)容應(yīng)該看起來(lái)很熟悉。我創(chuàng)建了一系列 JPEG 格式的圖片诸典,命名為 0.jpg 到 11.jpg描函,共12張。

static NSString *CellIdentifier = @"Cell Identifier";

@implementation AFViewController
{
    // models
    NSArray *imageArray;
    NSArray *colorArray;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // 初始化模型
    NSMutableArray *mutableImageArray = [NSMutableArray arrayWithCapacity:12];
    for (NSInteger i = 0; i < 12; i++)
    {
        NSString *imageName = [NSString stringWithFormat:@"%ld.jpg", (long)i];
        [mutableImageArray addObject:[UIImage imageNamed:imageName]];
    }
    imageArray = [NSArray arrayWithArray:mutableImageArray];
    
    NSMutableArray *mutableColorArray = [NSMutableArray arrayWithCapacity:10];
    for (NSInteger i = 0; i < 10; i++)
    {
        CGFloat redValue = (arc4random() % 255) / 255.0f;
        CGFloat blueValue = (arc4random() % 255) / 255.0f;
        CGFloat greenValue = (arc4random() % 255) / 255.0f;
        
        [mutableColorArray addObject:[UIColor colorWithRed:redValue green:greenValue blue:blueValue alpha:1.0f]];
    }
    colorArray = [NSArray arrayWithArray:mutableColorArray];
    
    // 初始化集合視圖布局對(duì)象
    UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
    flowLayout.minimumInteritemSpacing = 20.0f;
    flowLayout.minimumLineSpacing = 20.0f;
    flowLayout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
    flowLayout.itemSize = CGSizeMake(220, 220);
    
    // 設(shè)置集合視圖
    [self.collectionView registerClass:[AFCollectionViewCell class] forCellWithReuseIdentifier:CellIdentifier];
    self.collectionView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
    self.collectionView.allowsMultipleSelection = YES;
    self.collectionView.canCancelContentTouches = NO;
    self.collectionView.delaysContentTouches = NO;  
}

因?yàn)槟阋@示多個(gè) section 組,所以你需要實(shí)現(xiàn)一個(gè)新的舀寓、可選的UICollectionViewDelegate 方法胆数,稱為numberOfSectionsInCollectionView:。以下示例代碼中所示的collectionView:cellForItemAtIndexPath: 實(shí)現(xiàn)也看起來(lái)與之前很熟悉互墓。

#pragma mark - UICollectionViewDataSource Methods

-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    return colorArray.count;
}

-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return imageArray.count;
}

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    AFCollectionViewCell *cell = (AFCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
    
    cell.image = imageArray[indexPath.item];
    cell.backgroundColor = colorArray[indexPath.section];
    
    return cell;
}

打開集合視圖單元格子類必尼,在該類中添加一個(gè) UIImageView 實(shí)例變量。同時(shí)添加一個(gè)名為 imageUIImage 屬性轰豆。寫一個(gè)新的初始化器來(lái)實(shí)例化實(shí)例變量胰伍。

@implementation AFCollectionViewCell
{
    UIImageView *imageView;
}

- (id)initWithFrame:(CGRect)frame
{
    if (!(self = [super initWithFrame:frame])) return nil;
    
    self.backgroundColor = [UIColor whiteColor];
    
    imageView = [[UIImageView alloc] initWithFrame:CGRectInset(self.bounds, 10, 10)];
    [self.contentView addSubview:imageView];
    
    UIView *selectedBackgroundView = [[UIView alloc] initWithFrame:CGRectZero];
    selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:1.0f alpha:0.8f];
    self.selectedBackgroundView = selectedBackgroundView;
    
    return self;
}

你在圖像視圖的框架中移動(dòng)了 10 個(gè)點(diǎn),在圖像周圍創(chuàng)建一個(gè)邊框酸休。覆蓋 setImage: 方法來(lái)設(shè)置 imageView 的圖像。你還要重寫prepareForReuse 方法祷杈,并包含一個(gè) setHighlighted 的實(shí)現(xiàn)斑司。還請(qǐng)注意,你已經(jīng)將選定的 BackgroundView設(shè)置為一個(gè)純白色的視圖但汞。當(dāng)單元格被選中時(shí)宿刮,這個(gè)白色視圖將被放置在單元格的前面(以及任何背景視圖的前面,在本例中沒有)私蕾。

#pragma mark - Overriden UICollectionViewCell methods

-(void)prepareForReuse {
    [super prepareForReuse];
    
    self.backgroundColor = [UIColor whiteColor];
    self.image = nil; // also resets imageView’s image
}

-(void)setHighlighted:(BOOL)highlighted {
    [super setHighlighted:highlighted];
    
    if (self.highlighted) {
        imageView.alpha = 0.8f;
    } else {
        imageView.alpha = 1.0f;
    }
}

#pragma mark - Overridden Properties

-(void)setImage:(UIImage *)image {
    _image = image;
    
    imageView.image = image;
}

記住在覆蓋屬性的實(shí)現(xiàn)方法時(shí)僵缺,總是調(diào)用 super 關(guān)鍵字(除非你故意不想調(diào)用,并且有一個(gè)非常好的理由)踩叭。在實(shí)現(xiàn)中磕潮,當(dāng)圖像被高亮?xí)r,你將圖像視圖的 alpha 降低到 80%容贝。運(yùn)行應(yīng)用程序自脯。

玩轉(zhuǎn)應(yīng)用程序。點(diǎn)擊單元格斤富,使它們被選中膏潮,然后再點(diǎn)擊它們。請(qǐng)注意满力,如果你點(diǎn)擊并拖動(dòng)焕参,集合視圖會(huì)取消你的點(diǎn)擊并滾動(dòng)。這是因?yàn)?UIScrollView 屬性 canCancelContentTouches 被設(shè)置為 YES油额。另外叠纷,請(qǐng)注意集合視圖如何延遲高亮顯示單元格,直到你按住觸摸幾十分之一秒悔耘。這是因?yàn)?UIScrollView 屬性delaysContentTouches 被設(shè)置為 YES讲岁。在 viewDidLoad 的實(shí)現(xiàn)中,可以玩玩這兩個(gè)方法來(lái)試驗(yàn)它們?nèi)绾斡绊懠弦晥D的用戶體驗(yàn)(事實(shí)上,所有滾動(dòng)視圖都是如此缓艳,因?yàn)檫@些都是默認(rèn)值)校摩。

注意關(guān)于 UICollectionViewCellselectedBackgroundViewbackgroundView 屬性的幾件事。首先阶淘,它們將被拉伸以適應(yīng)它們被分配的任何單元格衙吩。這就是為什么在這個(gè)例子中,你能夠用一個(gè) CGRectZero 的邊框來(lái)初始化選定的背景視圖溪窒。接下來(lái)坤塞,一些屬性,如 alpha 屬性澈蚌,將被集合視圖重置為默認(rèn)值(在 alpha的示例中摹芙,為 1.0f)。在排除單元格背景的顯示問題時(shí)要注意這些問題宛瞄。

如果你想證明 selectedBackgroundView 被放置在視圖層次結(jié)構(gòu)中浮禾,您可以將其設(shè)置為稍微透明的顏色。將 selectedBackgroundView 的背景色改為類似 [UIColor colorWithWhite:1.0f alpha:0.8f] 的顏色》莺梗現(xiàn)在你將能夠通過 selectedBackgroundView 看到它的上層視圖盈电,即集合視圖本身。

在本章結(jié)束關(guān)于性能的案例研究之前杯活,我想做一個(gè)快速的轉(zhuǎn)移匆帚,重新審視一下 storyboard 和 .xib 文件。現(xiàn)在你已經(jīng)了解了集合視圖單元的工作原理旁钧,并且你可以創(chuàng)建子類來(lái)定制它們的外觀吸重,看看如何使用故事板和 .xibs 來(lái)處理前面的練習(xí)。

使用 .xibs 與代碼最相似均践,所以先從這個(gè)開始晤锹。打開上一次練習(xí)中的 Xcode 項(xiàng)目(如果你沒有使用源碼控制,先復(fù)制它)彤委,然后添加一個(gè)新文件鞭铆。

在新建文件對(duì)話框的左側(cè)窗格下,選擇 User Interface焦影,然后雙擊 "Empty" 以創(chuàng)建一個(gè)新的车遂、空的 .xib。給它起一個(gè)與你的集合視圖單元格子類相同的名字斯辰。打開該 .xib 文件舶担,在對(duì)象庫(kù)中,找到 "Collection View Cell" 并將其拖到空畫布上彬呻。

選擇新的單元格衣陶,打開"Size Inspector"柄瑰,將尺寸設(shè)置為 220 寬和 220高。反正這些都會(huì)被集合視圖重新配置剪况,所以這里設(shè)置只是為了幫助我們直觀地了解單元格的樣子教沾。打開 "Attributes Inspector",將背景色設(shè)為白色译断。打開 "Identity Inspector"授翻,將 “Custom Class” 類型,即集合視圖單元格的類型設(shè)置為你的子類孙咪。

在子類中堪唐,你需要?jiǎng)h除很多代碼。initWithFrame: 初始化器將不再被調(diào)用翎蹈。創(chuàng)建一個(gè)名為 awakeFromNib 的新方法淮菠。當(dāng)一個(gè)類的實(shí)例從 nib 中 "解凍 "時(shí),就會(huì)調(diào)用這個(gè)方法荤堪。在這個(gè)方法中兜材,你放置了你的自定義selectedBackgroundView 初始化〕蚜Γ看到有些事情無(wú)論如何都需要用代碼來(lái)完成嗎?

.xib 文件中糠爬,將 UIImageView 對(duì)象拖到單元格上寇荧。設(shè)置 springs 和 struts(或 Autolayout 約束),使圖像視圖四面嵌入 10 點(diǎn)执隧。將實(shí)例變量移動(dòng)到頭文件中揩抡,并將其前綴為關(guān)鍵字 IBOutlet,以便 .xib 可以看到它镀琉。命令單擊并從集合視圖單元格拖動(dòng)到其內(nèi)部的圖像視圖获诈;從出現(xiàn)的菜單中選擇圖像視圖出口助被。

最后,你需要告訴集合視圖使用這個(gè) nib,而不是自己初始化集合視圖單元格子類本身的副本于购。在 viewDidLoad中,改變集合視圖的設(shè)置椅亚,如以下示例代碼所示恢恼。

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // ...略去其他的初始化工作
    
    [self.collectionView registerNib:[UINib nibWithNibName:@"AFCollectionViewCell" bundle:nil] forCellWithReuseIdentifier:CellIdentifier];
}

運(yùn)行應(yīng)用程序,看看它的行為是否和代碼一樣弓熏。注意恋谭,即使你使用 UINib,你仍然被迫使用子類實(shí)現(xiàn)文件挽鞠。

最后疚颊,你要使用故事板來(lái)產(chǎn)生同樣的效果狈孔。清空 applicationDidFinishLaunchingWithOptions: 實(shí)現(xiàn),只返回 YES材义。刪除 .xib 文件均抽。將 viewDidLoad 實(shí)現(xiàn)縮減為以下示例代碼的樣子。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化模型,創(chuàng)建并加載圖片數(shù)據(jù)
    NSMutableArray *mutableImageArray = [NSMutableArray arrayWithCapacity:12];
    for (NSInteger i = 0; i < 12; i++) {
        NSString *imageName = [NSString stringWithFormat:@"%ld.jpg",(long)i];
        [mutableImageArray addObject:[UIImage imageNamed:imageName]];
    }
    self.imageArray = [NSArray arrayWithArray:mutableImageArray];
    
    // 初始化模型母截,創(chuàng)建并加載顏色數(shù)據(jù)到忽,作為一組 cell 的背景顏色
    NSMutableArray *mutableColorArray = [NSMutableArray arrayWithCapacity:10];
    for (NSInteger i = 0; i < 10; i++) {
        CGFloat redValue = (arc4random() % 255) / 255.0f;
        CGFloat blueValue = (arc4random() % 255) / 255.0f;
        CGFloat greenValue = (arc4random() % 255) / 255.0f;
        
        [mutableColorArray addObject:[UIColor colorWithRed:redValue green:greenValue blue:blueValue alpha:1.0f]];
    }
    self.colorArray = [NSArray arrayWithArray:mutableColorArray];
    
    // 配置集合視圖布局
    UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
    flowLayout.minimumInteritemSpacing = 20.0f;
    flowLayout.minimumLineSpacing = 20.0f;
    flowLayout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
    flowLayout.itemSize = CGSizeMake(220, 220);
    
    // 配置集合視圖
    self.collectionView.allowsMultipleSelection = YES;
}

你的 viewDidLoad 現(xiàn)在只設(shè)置了模型,并在集合視圖上設(shè)置了一個(gè)不能用故事板的屬性檢查器設(shè)置的屬性清寇。

創(chuàng)建一個(gè)名為 MainStoryboard 的新故事板文件喘漏。打開 Xcode 項(xiàng)目設(shè)置并將 MainStoryboard 設(shè)置為主故事板。(你沒看錯(cuò)华烟,伙計(jì)們翩迈。)將一個(gè)UICollectionViewController 拖到空的故事板上,并將其自定義類設(shè)置為你的代碼所在的類盔夜。展開集合視圖的視圖層次結(jié)構(gòu)负饲,直到你到達(dá)集合視圖單元。將其自定義類設(shè)置為你的 UICollectionViewCell 子類喂链,并在屬性檢查器中將其重用標(biāo)識(shí)符設(shè)置為單元格標(biāo)識(shí)符返十。打開 "尺寸 "檢查器,將其設(shè)置為寬220×高220椭微。

添加一個(gè)圖像視圖作為單元格的子視圖洞坑;命令點(diǎn)擊并從單元格拖動(dòng)到圖像視圖,設(shè)置單元格的圖像視圖出口蝇率。將圖像視圖設(shè)置為200寬200高迟杂,并設(shè)置 springs 和 struts(或 Autolayout 約束),使其尺寸與單元格一起變化本慕。

最后排拷,單擊視圖層次結(jié)構(gòu)中的集合視圖流布局對(duì)象。將 " Min Spacing "和 "Section Insets"設(shè)置為你之前在代碼中使用的值(見圖2.11)锅尘。運(yùn)行應(yīng)用程序监氢。

使用 storyboards 或 .xib 文件的好處是,你可以直觀地布局你的界面鉴象。當(dāng)你與設(shè)計(jì)師一起工作時(shí)忙菠,或者當(dāng)你第一次學(xué)習(xí) Cocoa Touch 中的視圖層次結(jié)構(gòu)時(shí),這將會(huì)有很大的幫助纺弊。然而牛欢,故事板和 .xib 文件除了它們的視覺性質(zhì)外,并沒有提供很多引人注目的優(yōu)勢(shì)淆游。故事板和集合視圖有兩個(gè)問題:集合視圖單元(它的重用標(biāo)識(shí)符)和代碼之間的緊密耦合傍睹,以及當(dāng)你在運(yùn)行時(shí)在代碼中修改故事板的設(shè)置時(shí)隔盛,一些棘手的調(diào)試。你不能依賴編譯時(shí)視覺上看到的東西拾稳,因?yàn)闊o(wú)論如何吮炕,它很可能會(huì)在運(yùn)行時(shí)被代碼改變。

從這一點(diǎn)出發(fā)访得,我不會(huì)再關(guān)注 .xib 文件或故事板龙亲。你已經(jīng)看到了它們是如何工作的,所以如果你正在將它們整合到一個(gè)使用它們的現(xiàn)有項(xiàng)目中悍抑,你將能夠應(yīng)用本書中的技術(shù)鳄炉。即使你仍然習(xí)慣于用代碼而不是視覺方式來(lái)布置界面,我也鼓勵(lì)你使用 .xib文件而不是故事板搜骡。記住使用自定義的 UICollectionViewCell 子類來(lái)保持你的代碼松散耦合拂盯;你的視圖控制器不應(yīng)該知道單元格的視圖層次結(jié)構(gòu)的內(nèi)部情況。

現(xiàn)在你已經(jīng)很好地理解了 UICollectionViewCell 以及如何向用戶顯示內(nèi)容记靡,我們來(lái)看看性能谈竿。

案例研究:評(píng)估 UICollectionView 的性能

當(dāng)你需要評(píng)估一款 iOS 應(yīng)用的性能時(shí),你必須在真機(jī)設(shè)備上進(jìn)行測(cè)量摸吠。使用真實(shí)而具體的設(shè)備很重要空凸,但最重要的是不要依賴模擬器。雖然它對(duì)很多事情都很有用寸痢,比如 NSZombies劫恒,但模擬器環(huán)境擁有一整臺(tái) PC 的性能作為動(dòng)力,然而大多數(shù)用戶的 iPhone 并不是這樣的轿腺。

選擇一個(gè)測(cè)試設(shè)備可能有點(diǎn)棘手。很明顯丛楚,像 iPhone 5 這樣真正的新東西不會(huì)是測(cè)試你的應(yīng)用在緊張時(shí)的性能的理想選擇族壳。然而,也不要依賴使用最老或最慢的硬件趣些。雖然 iPhone 3GS 只有一個(gè)核心仿荆,但它的實(shí)際表現(xiàn)可以比 iPhone 4 好得多,雖然 iPhone 4 有更多的隨機(jī)訪問內(nèi)存(RAM)和多核中央處理單元(CPU)坏平,但必須向其 Retina 屏幕推出四倍的像素拢操。

除了 iPhone,你還必須考慮 iPhone touch舶替。如果你正在編寫一款挑戰(zhàn)設(shè)備極限的應(yīng)用令境,你應(yīng)該在所有硬件/軟件組合上進(jìn)行測(cè)試。然而顾瞪,許多 iOS 開發(fā)人員只是單人操作舔庶,鞭策一些很酷的應(yīng)用程序抛蚁,他們沒有數(shù)千美元用于測(cè)試硬件(或理解哪些愿意沉迷于蘋果產(chǎn)品的很重要的人)。如果你沒有老舊的 iPhone 可供隨時(shí)使用惕橙,iPod touch 很好用瞧甩,而且價(jià)格便宜。

在集合視圖和其他滾動(dòng)視圖中弥鹦,性能最重要的評(píng)估參數(shù)是感知滾動(dòng)響應(yīng)肚逸。請(qǐng)注意,我說的是感知的響應(yīng)彬坏。你可以通過測(cè)量屏幕刷新率來(lái)衡量朦促。理想的情況下,這個(gè)速度應(yīng)該是每秒 60 幀(fps)苍鲜,也就是本機(jī)刷新率思灰。這意味著在每次調(diào)用主運(yùn)行循環(huán)(Main Run Loop)時(shí),你的應(yīng)用程序只有16 毫秒的時(shí)間來(lái)執(zhí)行相關(guān)任務(wù)混滔。好吧洒疚,這么多時(shí)間并不算非常充裕。這個(gè)案例研究將強(qiáng)調(diào)低效代碼嚴(yán)重影響性能的地方坯屿,并告訴你如何重構(gòu)你的代碼以保持精簡(jiǎn)油湖。同樣的代碼打開性能問題示例項(xiàng)目。解決方案也在那里领跛,前綴為 "Solved")乏德。

在進(jìn)入實(shí)際的剖析之前,這里還有一個(gè)提示吠昭。當(dāng)你在測(cè)量你的應(yīng)用程序的性能時(shí)喊括,CPU 的性能會(huì)受到 Instruments 的影響(有點(diǎn)像觀察者效應(yīng))。為了避免這種情況矢棚,請(qǐng)打開 Instruments 中的 Preferences郑什,并選中 Always Use Deferred Mode (一律使用延遲模式)復(fù)選框。這將在設(shè)備上本地收集數(shù)據(jù)蒲肋,直到運(yùn)行完成后才將數(shù)據(jù)發(fā)送到電腦上蘑拯。

當(dāng)你擁有了設(shè)備,并通過努力在 Apple 設(shè)備上面運(yùn)行你的應(yīng)用程序后兜粘,將它連接到你的電腦上申窘。確保你的設(shè)備是從 Scheme-下拉菜單中選擇的。在 Xcode 中孔轴,打開 Product 菜單剃法,選擇 Profile(Command-I)。這將用 Release 構(gòu)建設(shè)置(如編譯器優(yōu)化)構(gòu)建你的應(yīng)用程序路鹰,并打開Instruments 模板選擇器(見圖 2.12)玄窝。

圖 2.12

記住牵寺,根據(jù)你使用模擬器還是實(shí)際設(shè)備,你會(huì)得到不同的模板恩脂。選擇 “Core Animation” 模板帽氓。這將為您提供屏幕刷新率以及 CPU 使用率,這將告訴您 CPU 在哪里花費(fèi)了大部分時(shí)間執(zhí)行代碼俩块。點(diǎn)擊 Profile 并滾動(dòng)應(yīng)用程序黎休。使用滾動(dòng)并注意到響應(yīng)速度有多糟糕。當(dāng)你意識(shí)到這真的是非常非常糟糕的代碼時(shí)玉凯,點(diǎn)擊停止按鈕势腮,在儀器中查看結(jié)果(見圖2.13)。

圖 2.13

糟糕的屏幕刷新率! 峰值只有 37fps漫仆,太可怕了捎拯。選擇 "Time Profiler(時(shí)間剖析器)",打開 "Extended Detail(展開細(xì)節(jié))"窗格(見圖2.14)

圖2.14

你可以看到盲厌,在主線程上署照,從網(wǎng)上下載圖片的時(shí)間是最多的。這絕不是一個(gè)好主意! 此外吗浩,你沒有在任何地方緩存下載的數(shù)據(jù)建芙。在你的視圖控制器中添加一個(gè) NSCache 實(shí)例來(lái)保存你緩存的數(shù)據(jù)結(jié)果。這個(gè)類是一個(gè)方便的小鍵/值存儲(chǔ)懂扼,當(dāng)內(nèi)存變低時(shí)禁荸,它會(huì)自動(dòng)釋放內(nèi)存。在 loadView中初始化它(見清單2.18)阀湿。

-(void)configureCell:(AFCollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath withURLString:(NSString *)urlString
{
    // 嘗試從緩存中調(diào)出一個(gè) NSData 的緩存實(shí)例赶熟。
    id data = [photoDataCache objectForKey:urlString];
    
    if (data) {
        // 如果 objectForKey:是非 nil,也就是說我們之前下載了圖片陷嘴,這個(gè)分支就會(huì)執(zhí)行钧大。
        if ([data isKindOfClass:[NSNull class]]) {
            // 這表明該實(shí)例是 NSNull,所以我們不應(yīng)該使用它罩旋。
        } else {
            // 我們可以成功解壓我們的 JPEG 數(shù)據(jù)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                [data af_decompressedImageFromJPEGDataWithCallback:^(UIImage *decompressedImage) {
                    [cell setImage:decompressedImage];
                }];
            });
        }
    } else {
        // 在后臺(tái)隊(duì)列中下載圖片
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSData *data = [self downloadImageDataWithURLString:urlString];
            
            //Now that we have the data, dispatch back to the main queue
            //to use it. UIImage is part of UIKit and can *only* be accessed on
            //the main thread
            dispatch_async(dispatch_get_main_queue(), ^{
                
                UIImage *image = [UIImage imageWithData:data];
                
                if (image) {
                    // 這個(gè)作為參數(shù)傳入的單元格實(shí)例現(xiàn)在可能已經(jīng)被重用了。
                    // 調(diào)用 reloadItemsAtIndexPaths: 代替眶诈。
                    [photoDataCache setObject:data forKey:urlString];
                    [photoCollectionView reloadItemsAtIndexPaths:@[indexPath]];
                } else {
                    // 這表明 JPEG 解壓失敗涨醋。在我們的緩存中設(shè)置NSNull
                    [photoDataCache setObject:[NSNull null] forKey:urlString];
                }
            });
        });
    }
}

這段代碼非常直觀。注意你在方法簽名中添加了一個(gè)新的參數(shù)逝撬,一個(gè)索引路徑浴骂。這是用來(lái)在該索引路徑處重載項(xiàng)目的;直接引用單元格是不安全的宪潮,因?yàn)樗赡芤呀?jīng)被重用了溯警。比如說趣苏,如果單元格已經(jīng)被刪除,直接重載項(xiàng)目就會(huì)遇到問題梯轻。這個(gè)簡(jiǎn)單的例子適合本章的需要食磕。如果你要做更復(fù)雜的事情,我建議依靠獲取結(jié)果控制器來(lái)更新集合視圖喳挑。

注:

有更好的方法來(lái)緩存照片彬伦,比如 Core Data。也有更好的方法從互聯(lián)網(wǎng)上下載數(shù)據(jù)伊诵,但這個(gè)案例研究的重點(diǎn)是研究收集視圖性能的問題单绑,而不是一般的軟件架構(gòu)。

用修改后的代碼重新運(yùn)行剖析器曹宴。你可以看到搂橙,性能有了顯著的提高。這很好笛坦,但仔細(xì)看看你是否還能進(jìn)一步改進(jìn)区转。如果你看一下擴(kuò)展細(xì)節(jié)窗格,占用時(shí)間最多的方法還是 configureCell:atIndexPath:withURLString:弯屈∥现模看來(lái) imageWithData: 占用了大量的 CPU 時(shí)間。

你可以采取一些方法來(lái)處理這個(gè)問題资厉。你可以緩存解壓后的 JPEG 文件厅缺,這是一個(gè)好主意,但它有其缺點(diǎn)宴偿。最大的問題是湘捎,它消耗了大量的內(nèi)存。你的圖像是145×145 像素窄刘,有 3 個(gè)通道窥妇,一個(gè)通道 8 位。這意味著每張圖片解壓后娩践,要占用1451453=63KB活翩。這聽起來(lái)并不是很多,但應(yīng)用程序是在內(nèi)存受限的設(shè)備上運(yùn)行的翻伺,如果它使用了太多的內(nèi)存材泄,操作系統(tǒng)會(huì)殺死應(yīng)用程序。

而是在后臺(tái)隊(duì)列上解壓 JPEG 數(shù)據(jù)吨岭。"哈拉宗!"你說,"UIImage 是 UIKit 框架的一部分,叫我在后臺(tái)隊(duì)列上使用它是癡人說夢(mèng)旦事!" 你沒的說錯(cuò)魁巩,但存在一個(gè)替代方案。UIImage 是一個(gè)方便的類姐浮,但在告訴我們它是否已經(jīng)解壓了圖像方面是相當(dāng)不透明的谷遂。例如,使用 Core Graphics 框架來(lái)解壓圖像(見清單2.19)单料。

typedef void (^JPEGWasDecompressedCallback)(UIImage *decompressedImage);

// Just a utility class to round numbers up
int roundUp(int numToRound, int multiple)
{
    if(multiple == 0)
    {
        return numToRound;
    }
    
    int remainder = numToRound % multiple;
    if (remainder == 0)
        return numToRound;
    return numToRound + multiple - remainder;
}

@implementation NSData (AFDecompression)

-(void)af_decompressedImageFromJPEGDataWithCallback:(JPEGWasDecompressedCallback)callback
{
    uint8_t character;
    [self getBytes:&character length:1];
    
    if (character != 0xFF)
    {
        //This is not a valid JPEG.
        
        callback(nil);
        
        return;
    }
    
    // get a data provider referencing the relevant file
    CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)self);
    
    // use the data provider to get a CGImage; release the data provider
    CGImageRef image = CGImageCreateWithJPEGDataProvider(dataProvider, NULL, NO, kCGRenderingIntentDefault);
    CGDataProviderRelease(dataProvider);
    
    // make a bitmap context of a suitable size to draw to, forcing decode
    size_t width = CGImageGetWidth(image);
    size_t height = CGImageGetHeight(image);
    size_t bytesPerRow = roundUp(width * 4, 16);
    size_t byteCount = roundUp(height * bytesPerRow, 16);
    
    void *imageBuffer = malloc(byteCount);
    
    if (width == 0 || height == 0)
    {
        dispatch_async(dispatch_get_main_queue(), ^{
            callback(nil);
        });
    }
    
    CGColorSpaceRef colourSpace = CGColorSpaceCreateDeviceRGB();
    
    CGContextRef imageContext =
    CGBitmapContextCreate(imageBuffer, width, height, 8, bytesPerRow, colourSpace,
                          kCGImageAlphaNone | kCGImageAlphaNoneSkipLast); //Depsite what the docs say these are not the same thing
    
    CGColorSpaceRelease(colourSpace);
    
    // draw the image to the context, release it
    CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), image);
    CGImageRelease(image);
    
    // now get an image ref from the context
    CGImageRef outputImage = CGBitmapContextCreateImage(imageContext);
    
    CGContextRelease(imageContext);
    free(imageBuffer);
    
    dispatch_async(dispatch_get_main_queue(), ^{
        UIImage *decompressedImage = [UIImage imageWithCGImage:outputImage];
        callback(decompressedImage);
        CGImageRelease(outputImage);
    });
}

這個(gè)范疇類是非常有用的埋凯。在后臺(tái)隊(duì)列上調(diào)用解壓方法,一切都會(huì)為你處理好扫尖。NSData 實(shí)例被解壓白对,如果它實(shí)際上是一個(gè) JPEG,就在該方法被調(diào)用的隊(duì)列上换怖。當(dāng)解壓完成后甩恼,它會(huì)調(diào)用一個(gè)回調(diào)塊,并負(fù)責(zé)清理 CGImageRef 內(nèi)存沉颂。

現(xiàn)在你可以在后臺(tái)隊(duì)列中安全地解壓 JPEG条摸,將其納入到你的代碼中,如清單 2.20 所示铸屉。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [data af_decompressedImageFromJPEGDataWithCallback:^(UIImage *decompressedImage) {
        [cell setImage:decompressedImage];
    }];
});

因?yàn)?JPEG 解壓只需要幾毫秒钉蒲,所以我在清單 2.20 中直接更新單元格。如果你要解壓的 JPEG 文件是兆字節(jié)大的彻坛,這對(duì)你來(lái)說是行不通的顷啼,但是在集合視圖中顯示這么大的圖像,一般來(lái)說是個(gè)壞主意昌屉。

如果你重新運(yùn)行 Instruments钙蒙,你會(huì)發(fā)現(xiàn),總體來(lái)說间驮,最昂貴的操作躬厌,是從 UICollectionViewCellinitWithFrame: 中分配空間。這真的很好竞帽。從軼事上看扛施,應(yīng)用程序運(yùn)行得更流暢了。

擊掌! 但是看看代碼庫(kù)中還有兩個(gè)地方可以改進(jìn)屹篓。

圖片是145×145 像素疙渣,但你的單元格是145×100 邏輯像素。UIImageView 是把圖片縮小到合適的位置抱虐。你可以改變 content mode,將它們居中饥脑,而不是讓它們不被縮放恳邀。

理想情況下懦冰,你的圖像大小和單元格大小應(yīng)該是一樣的,這樣操作系統(tǒng)就不需要調(diào)整任何大小谣沸,從而提高性能刷钢。然而,如果你使用的是第三方 API乳附,你將無(wú)法控制圖像大小内地。

我可以推薦的唯一一件事是打開單元格層的 masksToBounds 屬性來(lái)提高這個(gè)例子的性能;你將這個(gè)屬性與 cornerRadius 一起使用赋除。這對(duì) CPU 造成了壓力阱缓,因?yàn)樗赡軙?huì)產(chǎn)生離屏渲染,可能會(huì)導(dǎo)致很多問題举农。如果你搞不清楚為什么你的集合視圖速度很慢荆针,可以檢查一下。

如果集合視圖背景不透明颁糟,你可以在 contentViewUIImageView 子視圖中使用 PNG 來(lái)遮擋角落航背。這也是一個(gè)很好的方法,但是如果可以避免的話棱貌,不要使用可調(diào)整大小的圖片玖媚。如果你的所有單元格都是一樣大小,就使用不可調(diào)整大小的 UIImage 來(lái)遮擋角落婚脱,因?yàn)樗匿秩舅俣雀臁?/p>

第一個(gè)性能示例就到此為止今魔。看看下一個(gè)例子起惕,叫做 "性能問題實(shí)例二"涡贱,在相同的代碼中。構(gòu)建并運(yùn)行該應(yīng)用惹想,以了解它的工作原理问词。

這個(gè)應(yīng)用程序的用例如下。你受雇于一家剛剛獲得天使資金的新興創(chuàng)業(yè)公司嘀粱,他們正在建立一個(gè)貓咪的社交網(wǎng)絡(luò)(見圖 2.15)激挪。你要為他們未來(lái)的移動(dòng)應(yīng)用做一個(gè)相當(dāng)于 "Facebook墻 "的原型,這樣他們就可以搶到數(shù)百萬(wàn)的風(fēng)險(xiǎn)投資資金锋叨。

圖 2.15

該應(yīng)用程序在具有不同背景顏色的單元格中顯示注釋垄分。模型是在 setupModel 中設(shè)置的。只需忽略這個(gè)方法娃磺;它與本案例研究無(wú)關(guān)薄湿。另外,請(qǐng)注意 Xcode 如何讓你在 Objective-C 源代碼中使用 emoji。這有多酷豺瘤?

配置應(yīng)用程序吆倦,并使用與上一個(gè)例子相同的 Core Animation 模板。峰值幀率為 48fps坐求,這并不可怕蚕泽,但并不理想。當(dāng)你打開 "擴(kuò)展細(xì)節(jié) "窗格時(shí)桥嗤,你看到的應(yīng)該會(huì)引起一些警覺(見圖2.16)须妻。

圖2.16

最昂貴的操作性能是卸載 .xib 文件。那是怎么回事泛领?打開 AFCollectionViewCell.xib荒吏,沉浸在視圖層次結(jié)構(gòu)的純粹存在感的恐怖中。

很明顯师逸,圖 2.17 代表了一個(gè)教學(xué)實(shí)例司倚。你永遠(yuǎn)不會(huì)在一個(gè) nib 中擁有一個(gè)相當(dāng)糟糕的視圖層次結(jié)構(gòu)。即使你有一個(gè)相當(dāng)復(fù)雜的視圖層次結(jié)構(gòu)篓像,你真正要做的就是在彩色背景上顯示一些文本动知。你可以在 drawRect: 中更快地繪制這個(gè)視圖,以及相當(dāng)于所有這些無(wú)用的視圖员辩。你可以在你自己的單元格子類中應(yīng)用同樣的邏輯盒粮;如果你有一個(gè)復(fù)雜的視圖層次結(jié)構(gòu),需要花費(fèi)太長(zhǎng)的時(shí)間來(lái)繪制奠滑,就實(shí)現(xiàn) drawRect: 并拋棄視圖層次結(jié)構(gòu)丹皱。 drawRect: 也可以是一個(gè)性能緩慢的東西,然而宋税,在這樣一個(gè)簡(jiǎn)單的例子中使用它只是為了說明它是如何完成的摊崭。只有當(dāng)手動(dòng)繪制視圖的組件比渲染一個(gè)必然復(fù)雜的視圖層次結(jié)構(gòu)更快時(shí),你才應(yīng)該使用它杰赛。

刪除單元格頭文件中的.xib和兩個(gè)屬性呢簸。不在視圖控制器的 viewDidLoad 中注冊(cè)一個(gè) UINib,而是注冊(cè)一個(gè) Class乏屯。不為顏色使用單獨(dú)的背景視圖根时,而只是在 drawRect: 中繪制它。為單元格的文本創(chuàng)建一個(gè)新的字符串屬性辰晕。你將覆蓋 backgroundColorgettersetter 來(lái)進(jìn)行一些巧妙的繪制蛤迎。

static inline void addRoundedRectToPath(CGContextRef context, CGRect rect, float ovalWidth, float ovalHeight)
{
    float fw, fh;
    if (ovalWidth == 0 || ovalHeight == 0) {
        CGContextAddRect(context, rect);
        return;
    }
    CGContextSaveGState(context);
    CGContextTranslateCTM (context, CGRectGetMinX(rect), CGRectGetMinY(rect));
    CGContextScaleCTM (context, ovalWidth, ovalHeight);
    fw = CGRectGetWidth (rect) / ovalWidth;
    fh = CGRectGetHeight (rect) / ovalHeight;
    CGContextMoveToPoint(context, fw, fh/2);
    CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1);
    CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1);
    CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1);
    CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1);
    CGContextClosePath(context);
    CGContextRestoreGState(context);
}

@implementation AFCollectionViewCell
{
    UIColor *realBackgroundColor;
}

-(id)initWithFrame:(CGRect)frame
{
    if (!(self = [super initWithFrame:frame])) return nil;
    
    self.opaque = NO;
    self.backgroundColor = [UIColor clearColor];
    
    return self;
}

-(void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    CGContextSaveGState(context);
    
    [realBackgroundColor set];

    addRoundedRectToPath(context, self.bounds, 10, 10);
    CGContextClip(context);

    CGContextFillRect(context, self.bounds);
    
    CGContextRestoreGState(context);
    
    [[UIColor whiteColor] set];
    
    [self.text drawInRect:CGRectInset(self.bounds, 10, 10) withFont:[UIFont boldSystemFontOfSize:20] lineBreakMode:NSLineBreakByWordWrapping alignment:NSTextAlignmentCenter];
}

#pragma mark - Overridden Properties

-(void)setBackgroundColor:(UIColor *)backgroundColor
{
    [super setBackgroundColor:[UIColor clearColor]];
    
    realBackgroundColor = backgroundColor;
    
    [self setNeedsDisplay];
}

-(UIColor *)backgroundColor
{
    return realBackgroundColor;
}

-(void)setText:(NSString *)text
{
    _text = [text copy];
    
    [self setNeedsDisplay];
}

優(yōu)化原理:通過重寫 drawRect: 方法通過 Core Graphics 框架繪制視圖。

addRoundedRectToPath 的 C 方法很方便含友,可以很容易地修改成只對(duì)某些角進(jìn)行圓角處理替裆。這些方法通常應(yīng)該放在一個(gè)單獨(dú)的源文件中校辩,以便可以重復(fù)使用。請(qǐng)看清單2.22辆童。

-(void)configureCell:(AFCollectionViewCell *)cell withModel:(AFModel *)model
{
    cell.backgroundColor = [model.color colorWithAlphaComponent:0.6f];
    cell.text = model.comment;
}

清單 2.22是一個(gè)高效的實(shí)現(xiàn)召川。繪圖代碼很直接,使用單元格的代碼在MVC 架構(gòu)中也能很好地工作胸遇。重新編譯應(yīng)用程序。圖 2.18 在Instruments中第一次剖析運(yùn)行情況

圖 2.18

圖 2.18顯示汉形,當(dāng)你去掉一個(gè)單元格的時(shí)候纸镊,有一個(gè)叫做 performLongRunningTask 的方法被調(diào)用。它嚴(yán)重阻礙了幀刷新率概疆。你不應(yīng)該在主線程上執(zhí)行長(zhǎng)運(yùn)行任務(wù)逗威,而且如果你看代碼,它是由 prepareForReuse 調(diào)用的岔冀。開發(fā)者把為重用做準(zhǔn)備和已經(jīng)向用戶展示了單元格混為一談凯旭。這確實(shí)是不可接受的。將這個(gè)邏輯重構(gòu)到視圖控制器中(見清單2.23)使套。

有個(gè)小技巧罐呼。我在 performLongRunningTask 方法中設(shè)置了一個(gè)空的 for 循環(huán),故意造成性能問題侦高,但為了讓這個(gè)例子正常工作嫉柴,我不得不禁用編譯器優(yōu)化。LLVM 太聰明了奉呛,如果啟用了編譯器優(yōu)化计螺,它就會(huì)把空循環(huán)剝離出來(lái)。

#pragma mark - UICollectionViewDataSource & UICollectionViewDelegate Methods

-(void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self performLongRunningTask];
    });
}

-(void)performLongRunningTask
{
    /*
     Let's run some long-running task. Maybe it's some complicated view
     hierarchy math that could be simplified with Autolayout.
     */
    for (int i = 0; i < 5000000; i++);
}

現(xiàn)在瞧壮,調(diào)用長(zhǎng)期運(yùn)行的任務(wù)的代碼在適當(dāng)?shù)牡胤降锹蝿?wù)在后臺(tái)隊(duì)列上執(zhí)行。很好咆槽。重新提交應(yīng)用程序陈轿。幀率大約在 55fps左右,這是相當(dāng)不錯(cuò)的罗晕。代碼中最慢的部分是 drawRect:济欢,這會(huì)導(dǎo)致一些性能問題。如前所述小渊,只有當(dāng)你需要實(shí)現(xiàn)一個(gè)非常復(fù)雜的視圖層次結(jié)構(gòu)時(shí)法褥,使用 drawRect: 才是一個(gè)更好的選擇

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末酬屉,一起剝皮案震驚了整個(gè)濱河市半等,隨后出現(xiàn)的幾起案子揍愁,更是在濱河造成了極大的恐慌,老刑警劉巖杀饵,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莽囤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡切距,警方通過查閱死者的電腦和手機(jī)朽缎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谜悟,“玉大人话肖,你說我怎么就攤上這事∑闲遥” “怎么了最筒?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蔚叨。 經(jīng)常有香客問我床蜘,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮爹梁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘弹囚。我一直安慰自己,他們只是感情好领曼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布鸥鹉。 她就那樣靜靜地躺著,像睡著了一般庶骄。 火紅的嫁衣襯著肌膚如雪毁渗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天单刁,我揣著相機(jī)與錄音灸异,去河邊找鬼。 笑死羔飞,一個(gè)胖子當(dāng)著我的面吹牛肺樟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逻淌,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼么伯,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了卡儒?” 一聲冷哼從身側(cè)響起田柔,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤俐巴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后硬爆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體欣舵,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年缀磕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缘圈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡袜蚕,死狀恐怖准验,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情廷没,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布垂寥,位于F島的核電站颠黎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏滞项。R本人自食惡果不足惜狭归,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望文判。 院中可真熱鬧过椎,春花似錦、人聲如沸戏仓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)赏殃。三九已至敷待,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間仁热,已是汗流浹背榜揖。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抗蠢,地道東北人举哟。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像迅矛,于是被迫代替她去往敵國(guó)和親妨猩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353