設計你的數據源和委托
任何集合視圖都必須有一個數據源對象。這個數據源對象是你的app顯示的內容。它可以是一個來自于你的app的數據模型的對象,或者是管理該集合視圖的控制器。數據源唯一的要求是它必須能夠提供集合視圖所需的信息,例如它有多少個items和當顯示這些items時要使用哪些視圖。
委托對象是一個可選的(但推薦)對象,用于管理與內容的呈現和交互相關的方面。雖然委托主要的職責是來管理cell的高亮和選擇,但也可以擴展來提供其他信息。例如,流式布局擴展了幾本委托行為來自定義布局度量,例如cells的大小和它們之間的間距。
數據源管理你的內容
數據源對象的職責是來管理你將要使用集合視圖來呈現的內容。數據源對象必須遵守UICollectionViewDataSource
協議,該協議定義了你必須支持的基本行為和方法。數據源的工作是來提供集合視圖以下問題的答案:
- 集合視圖包含多少個分區?
- 對于一個給定的分區,該分區包含多少個items?
- 對于給定的分區和item,應使用什么視圖來顯示相應的內容?
分區和items是集合視圖內容的基本組織原則。集合視圖通常至少有一個分區并且可能有更多的分區。每個分區,包含0個或更多的items。items代表你想要呈現的主要內容,而分區將這些items組織成邏輯組。例如,一個照片app可能會使用分區來代表一張相冊或同一天拍攝的一組照片。
集合視圖是指使用NSIndexPath對象所包含的數據。當嘗試定位一個item時,集合視圖通過布局對象提供索引路徑信息給它。對于items, 索引路徑包含一個分區編號和一個item編號。對于補充和裝飾視圖,索引路徑包含布局對象提供的所有值。附加到補充和裝飾視圖上的索引路徑的含義取決于你的app,盡管第一個索引對應于數據源中的特定分區。這些視圖的索引路徑更多的是標識而不是意義,確定當前正在考慮什么樣的視圖。因此,例如,如果你有補充視圖為分區創建頁眉和頁腳,這在流式布局中可以看到,通過索引路徑提供的相關信息是分區引用。
注意:盡管標準索引路徑支持多個級別,但集合視圖的cells只支持有”section”和”item”兩個級別深度的索引路徑,和UITableView類的索引路徑很相似。如果需要,補充視圖和裝飾視圖可以有更多復雜的索引路徑。其索引路徑大于1的元素被解釋為對應于路徑中第一個索引指定的分區。傳統上,只有第二個索引是必要的,但補充和裝飾視圖不限于兩個。在設計數據源時要牢記這一點。
無論你如何在你的數據對象上安排分區和items, 這些分區和items的可視化呈現仍然由布局對象確定。不同的布局對象可以非常不同地呈現分區和item數據,如圖2-1所示。在該圖中,流式布局對象垂直地排列各個分區,每個分區都與前一個分區相同。自定義布局對象可以將分區放置在一個非線形布局中,再次示范布局與實際數據的分離。
設計數據對象
有效的數據源使用分區和items來幫忙組織其底層數據對象。組織你的數據到分區和items中使其以后更容易實現你的數據源方法。并且由于你的數據源方法會頻繁地調用,你希望確保這些方法的實現能夠盡可能快地獲取數據。
一個簡單的解決方案(但不是唯一的解決方案)是為你的數據源使用一個嵌套數組,如圖2-2所示。在這個配置中,頂層數組包含一個或多個數組代表數據源的分區。每一個分區數組包含該分區內的items數據。在一個分區中查找item是一個獲取它的分區數組,然后再從該數組中獲取item的問題。這種類型的安排便于管理適當大小的items集合,并根據需要檢索單個item。
當設計你的數據結構時,你總是可以從一個簡單的數組開始,并根據需要移動到更高效的結構中。一般來說,你的數據對象不應該是性能瓶頸。集合視圖通常訪問數據源,只計算一共有多少個對象,并獲取當前屏幕上元素的視圖。如果布局對象僅依賴于數據對象的數據,則當數據源包含數千個對象時,性能可能會受到嚴重影響。
告訴集合視圖你的內容
集合視圖詢問你的數據源的問題是它包含多少個分區和每個分區包含多少個items。當下列動作發生時,集合視圖會詢問你的數據源來提供這些信息:
- 集合視圖首次顯示
- 給集合視圖賦值一個不同的數據源對象
- 指定調用集合視圖的
reloadData
方法 - 集合視圖使用
performBatchUpdates:completion:
或任何移動、增加、刪除的方法來執行一個block
你使用numberOfSectionsInCollectionView:
方法來提供分區數目,并使用collectionView:numberOfItemsInSection:
方法來提供每個分區中的items數目。你必須實現collectionView:numberOfItemsInSection:
方法,但是如果你的集合視圖僅僅只有一個分區,numberOfSectionsInCollectionView:
方法的實現是可選的。兩個方法都返回適當信息的整數值。
如果你如圖2-2所示那樣實現數據源,你的數據源方法的實現可能與列表2-1所示一樣簡單。在這些代碼中,_data變量是存儲分區的頂層數組的數據源的自定義成員變量。獲取該數組的計數將產生該分區的數目。獲取其子數組中的計數將產生分區中items的數目。當然,你自己的代碼應該做錯誤檢查,以確保返回的值是有效的。
//Listing 2-1 Providing the section and item counts
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView*)collectionView {
// _data is a class member variable that contains one array per section.
return [_data count];
}
- (NSInteger)collectionView:(UICollectionView*)collectionView numberOfItemsInSection:(NSInteger)section {
NSArray* sectionArray = [_data objectAtIndex:section];
return [sectionArray count];
}
配置cells和補充視圖
你的數據源另外一個重要的任務是提供集合視圖用來顯示你的內容的視圖。集合視圖不會跟蹤你的app的內容。它只需要你給出的視圖,并將當前的布局信息應用于它們。因此,視圖上顯示的所有內容都是你的責任。
在你的數據源上報它管理有多少個分區和items后,集合視圖要求布局對象來提供集合視圖內容的布局屬性。在某些時候,集合視圖要求布局對象提供特定矩形中的元素列表(通常是可見的矩形)。集合視圖使用該列表詢問你的數據源對應的cells和補充視圖。為了提供這些cells和補充視圖,你的代碼必須執行以下操作:
- 將你的模版cell和視圖嵌入到故事板文件中(或者,為每個支持的cell或視圖注冊一個類或nib文件)
- 在你的數據源中,當被詢問時出列并配置相應的cell或視圖
為了確保以最有效的方式使用cell和補充視圖,集合視圖承擔了為你創建這些對象的責任。每個集合視圖維護當前未使用的cells和補充視圖的內部隊列。代替你自己創建對象,簡單的詢問集合視圖來提供你想要的視圖。如果有一個在重用隊列中正在等待,集合視圖會準備好并將其快速地返回給你。如果沒有在等待,集合視圖會使用注冊的類或nib文件來創建一個新的并把它返回給你。因此,每次你取回一個cell或者視圖時,總是能得到一個現成的對象。
重用標識符使注冊多種類型的cells和多種類型的補充視圖成為可能。重用標識符是一個字符串,用于區分注冊cell和視圖類型。字符串的內容只與數據源對象有關。但當被請求一個視圖或者cell時,你可以使用提供的索引路徑來確定你想要的哪種類型的視圖或cell,然后通過合適的重用標識符號的獲取方法來獲取。
注冊你的cells和補充視圖
你可以用代碼或者在故事板文件中配置集合視圖的cells和視圖。
在故事板中配置cells和視圖。當在故事板中配置cells和補充視圖時,你可以拖拽item到集合視圖并配置它。這會在集合視圖和相應的cells或視圖之間創建一個關系。
- 對于cells, 在對象庫中拖拽一個
CollectionViewCell
然后把它放置在集合視圖中。將cell的自定義類和集合可重用視圖標識符設置為合適的值。 - 對于補充視圖,在對象庫中拖拽一個
CollectionReusableView
,然后把它放置在集合視圖中。將視圖的自定義類和集合可重用視圖標識符設置為合適的值。
用代碼配置cells。使用registerClass:forCellWithReuseIdentifier:
方法或registerNib:forCellWithReuseIdentifier:
方法來將你的cell與重用標識符關聯。你可以稱這些方法為父視圖控制器初始化過程的一部分。
用代碼配置補充視圖。使用registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
方法或registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
方法來將一種類型的視圖與重用標識符相關聯。你可以稱這些方法為父視圖控制器初始化過程的一部分。
雖然只使用一個重用標識符來注冊cells,但補充視圖需要一個被稱為類型字符串的附加標識符。每個布局對象負責定義它支持的補充視圖的種類。例如,UICollectionViewFlowLayout
類支持兩種類型的補充視圖:分區首視圖和分區尾視圖。為了標識視圖的這兩種類型,它定義了字符串常量UICollectionElementKindSectionHeader
和UICollectionElementKindSectionFooter
。在布局過程中,布局對象包含用于該視圖類型的其他布局屬性的類型字符串。然后,集合視圖將信息傳遞給數據源。然后,數據源使用類型字符串和可重用標識符來決定哪個視圖對象用來出列和返回。
注意:如果你實現你自己的自定義布局,你負責定義你的布局支持的補充視圖的種類。一個布局可以支持多個補充視圖,每個補充視圖都有它自己的類型字符串。關于自定義布局更多信息,請看Creating Custom Layouts.
注冊是一個一次性事件,必須在你意圖出列cells或視圖之前發生。在已經注冊之后,你可以根據需要出列任意多個cells或視圖,而無需重新注冊它們。不推薦在出列一個或多個items后改變注冊信息。最好是只注冊一次你的cells和視圖并完成它。
出列并配置cells和視圖
你的數據源對象負責在集合視圖詢問它們的時候來提供cells和補充視圖。UICollectionViewDataSource
協議為這個目的包含了兩個方法:collectionView:cellForItemAtIndexPath:
和 collectionView:viewForSupplementaryElementOfKind:atIndexPath:
。因為cells是集合視圖的必備元素,你的數據源對象必須實現collectionView:cellForItemAtIndexPath:
方法,但是collectionView:viewForSupplementaryElementOfKind:atIndexPath:
方法是可選的,這依賴于使用的布局類型。在這兩種情況下,你實現這些方法都遵循一個非常簡單的模式:
- 使用
dequeueReusableCellWithReuseIdentifier:forIndexPath:
或者dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:
方法出列一個cell或者合適類型的視圖。 - 使用特定索引路徑的數據來配置該視圖。
- 返回該視圖。
出列過程的目的是減輕你必須創建一個cell或視圖的責任。只要你以前注冊了一個cell或視圖,出列過程保證不會返回nil。如果在重用隊列中沒有cell或給定類型的視圖,則出列方法會使用你的故事板或使用你注冊的類或nib文件簡單的創建一個。
從出列過程返回的cell應處于原始狀態,并準備配置新數據。對于必須創建的cell或視圖,出列過程使用正常的進程創建并初始化它-也就是說,通過從故事板或nib文件中加載視圖,或者通過創建一個新的實例并使用initWithFrame:
方法進行初始化。相比之下,沒有從新創建的數據項,而是從一個重用隊列中獲取的條目可能已經包含了以前使用的數據。在這種情況下,出列方法會調用該條目的prepareForReuse
方法,讓它有機會返回原始狀態的條目。當實現自定義cell或視圖類時,可以覆蓋該方法來將屬性重置為默認值,并執行任何其他的清除。
當數據源出列視圖后,它將使用新數據來配置視圖。可以使用傳遞給數據源方法的索引路徑來定位合適的數據對象,然后將該對象的數據應用于視圖。在配置視圖之后,從方法中返回它并完成。列表2-2顯示一個簡單的例子關于如何配置一個cell。在出列該cell后,該方法使用cell的位置信息來設置cell的自定義標簽,然后返回這個cell。
//Listing 2-2 Configuring a custom cell
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCustomCell* newCell = [self.collectionView dequeueReusableCellWithReuseIdentifier:MyCellID
forIndexPath:indexPath];
newCell.cellLabel.text = [NSString stringWithFormat:@"Section:%d, Item:%d", indexPath.section, indexPath.item];
return newCell;
}
注意:當從數據源返回視圖時,總是返回一個有效的視圖。即使由于某些原因,無法顯示所請求的視圖而返回nil,會造成斷言,并且應用程序會終止,因為布局對象期望通過這個方法返回有效的視圖。
插入,刪除和移動分區和items
要插入,刪除或刪除一個單獨的分區或item,請按照下列步驟操作:
- 在數據源對象上更新數據。
- 調用集合視圖的合適方法來插入或刪除該分區或item。
在通知集合視圖的任何更改之前,更新數據源是非常重要的。集合視圖方法會假設你的數據源包含當前正確的數據。如果沒有,則集合視圖可能會從你的數據源接受到錯誤的items集合,或者詢問不存在的items并導致應用程序崩潰。
當你以編程方式添加,刪除或移動單個item時,集合視圖的方法會自動創建動畫來反應更改。如果你想將多個更改一起動畫,你必須完成所有的插入,刪除,或移動在一個block內調用,并通過block的performBatchUpdates:completion:
方法。批量更新過程隨后會同時對所有更改進行動畫處理,你可以自由地混合調用來插入,刪除或移動items在同一個block中。
列表2-3顯示了如何執行批處理更新來刪除當前選定的items的簡單示例。該block在執行performBatchUpdates:completion:
方法中首先調用自定義方法來更新數據源。然后告訴集合視圖來刪除這些items。你提供的更新block和完成block會同步執行。
//Listing 2-3 Deleting the selected items
[self.collectionView performBatchUpdates:^{
NSArray* itemPaths = [self.collectionView indexPathsForSelectedItems];
// Delete the items from the data source.
[self deleteItemsFromDataSourceAtIndexPaths:itemPaths];
// Now delete the items from the collection view.
[self.collectionView deleteItemsAtIndexPaths:itemPaths];
} completion:nil];
管理視覺狀態的選中和高亮
集合視圖默認支持單項選擇,并且可以配置為可以支持多項選擇或完全禁用選擇。集合視圖檢測其邊界內的輕擊,并高亮或選擇相應的cell。在大多數情況下,集合視圖僅修改cell的屬性來表明其是選中的或高亮的;它不會改變cell的視覺外觀,有一個例外。如果cell的selectedBackgroundView
屬性包含一個有效的視圖,集合視圖會在cell是高亮或選中時顯示該視圖。
列表2-4顯示了可以在自定義集合視圖cell中實現的代碼,以便于突出高亮和選中狀態的更改外觀。當cell第一次加載的時候和在cell不是高亮或不是選中的時候,cell的backgroundView
屬性總是一個默認視圖。一旦cell是高亮或選中時selectedBackgroundView
屬性會替換默認背景視圖。在這種情況下,當被選中或高亮時cell的背景色會從紅色改變為白色。
//Listing 2-4 Setting the background views to indicate changed states
UIView* backgroundView = [[UIView alloc] initWithFrame:self.bounds];
backgroundView.backgroundColor = [UIColor redColor];
self.backgroundView = backgroundView;
UIView* selectedBGView = [[UIView alloc] initWithFrame:self.bounds];
selectedBGView.backgroundColor = [UIColor whiteColor];
self.selectedBackgroundView = selectedBGView;
集合視圖的代理使用以下方法提供集合視圖,以便于高亮和選中:
collectionView:shouldSelectItemAtIndexPath:
collectionView:shouldDeselectItemAtIndexPath:
collectionView:didSelectItemAtIndexPath:
collectionView:didDeselectItemAtIndexPath:
collectionView:shouldHighlightItemAtIndexPath:
collectionView:didHighlightItemAtIndexPath:
collectionView:didUnhighlightItemAtIndexPath:
這些方法提供給你許多機會來調整高亮/選中集合視圖所需的精確規范行為。
例如,如果你喜歡自己來繪制一個cell的選中狀態,你可以把selectedBackgroundView
屬性設置為nil,用你的代理對象應用任何視覺變化給該cell。你可以在collectionView:didSelectItemAtIndexPath:
方法中應用視覺變化,并在collectionView:didDeselectItemAtIndexPath:
方法中移除它們。
如果你喜歡自己來繪制高亮狀態,你可以重寫collectionView:didHighlightItemAtIndexPath:
和collectionView:didUnhighlightItemAtIndexPath:
代理方法,并使用它們來運用你的高亮。如果你在selectedBackgroundView
屬性中也指定了一個視圖,你應該對cell的內容視圖進行更改,以確保更改是可見的。列表2-5顯示了使用內容視圖背景色更改高亮顯示的簡單方法。
//Listing 2-5 Applying a temporary highlight to a cell
- (void)collectionView:(UICollectionView *)colView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell* cell = [colView cellForItemAtIndexPath:indexPath];
cell.contentView.backgroundColor = [UIColor blueColor];
}
- (void)collectionView:(UICollectionView *)colView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell* cell = [colView cellForItemAtIndexPath:indexPath];
cell.contentView.backgroundColor = nil;
}
cell的高亮狀態和選中狀態之間有細微但重要的區別。高亮狀態是一個過渡狀態,當用戶手指仍在觸摸設備時,你可以將可見高亮應用于該cell。這種狀態當集合視圖正在跟蹤cell上的觸摸事件時設置為YES,當觸摸事件停止時,高亮狀態返回值為NO。相比之下,選中狀態只在一系列觸摸事件結束之后才改變-具體地,當這些事件指示用戶嘗試選擇cell時。
圖2-3闡釋當用戶觸摸一個未選擇的cell時產生的一系列步驟。初始按下事件導致集合視圖將cell的高亮狀態改變為YES,即使做這些并不會自動改變cell的外觀。如果最終的抬起事件發生在cell上,高亮狀態返回NO并且集合視圖將選中狀態改變為YES。當用戶改變選中狀態,集合視圖在cell的selectedBackgroundView
屬性上顯示該視圖,但這是集合視圖對cell唯一的視覺變化。其他視覺上的變化必須通過你的代理對象來完成。
無論用戶是選擇還是取消選擇cell,cell的選中狀態總是由最后一件事來改變。
cell的點擊首先始終會改變cell的高亮狀態。只有在點擊系列事件之后和應用于任何高亮被移除之后,才會改變cell的選中狀態。當設計你的cell時,你應該確保你的高亮和選中狀態的視覺外觀不會發生意外沖突。
給cell顯示編輯菜單
當用戶在cell上執行長按手勢,集合視圖意圖為cell顯示一個編輯菜單。這個編輯菜單可以用來剪切,復制和粘貼cells在集合視圖中。在顯示編輯菜單之前,必須滿足幾個條件:
- 代理必須實現與處理操作相關的三個方法:
collectionView:shouldShowMenuForItemAtIndexPath:
collectionView:canPerformAction:forItemAtIndexPath:withSender:
collectionView:performAction:forItemAtIndexPath:withSender:
-
collectionView:shouldShowMenuForItemAtIndexPath:
方法必須為指定的cell返回YES -
collectionView:canPerformAction:forItemAtIndexPath:withSender:
方法必須為至少一個所需的操作返回YES。集合視圖支持以下操作:
cut:
copy:
paste:
如果這些條件都滿足了,并且用戶從菜單中選擇了一個操作,集合視圖會調用代理的collectionView:performAction:forItemAtIndexPath:withSender:
方法來在指定的cell上執行操作。
列表2-6顯示如何防止一個菜單項出現。在這個例子中,collectionView:canPerformAction:forItemAtIndexPath:withSender:
方法防止剪切菜單從編輯菜單中出現。它啟用復制和粘貼項目,以便用戶可以插入內容。
//Listing 2-6 Selectively disabling actions in the Edit menu
- (BOOL)collectionView:(UICollectionView *)collectionView
canPerformAction:(SEL)action
forItemAtIndexPath:(NSIndexPath *)indexPath
withSender:(id)sender {
// Support only copying and pasting of cells.
if ([NSStringFromSelector(action) isEqualToString:@"copy:"]
|| [NSStringFromSelector(action) isEqualToString:@"paste:"])
return YES;
// Prevent all other actions.
return NO;
}
有關更多使用剪貼板命令的詳細信息,請看Text Programming Guide for iOS.
布局之間的轉換
布局之間的轉換的最簡單的方式是使用setCollectionViewLayout:animated:
方法。然而,如果你需要對轉換進行控制或希望其可以交互,使用UICollectionViewTransitionLayout
對象。
UICollectionViewTransitionLayout
類是一種特殊類型的布局,當集合視圖的布局對象轉換為一個新布局是得以初始化。使用轉換布局對象,你可以使對象遵循非線性路徑,使用不同的計時算法,或根據傳入的觸摸事件移動。標準類會給一個新布局提供線性轉換,但是像UICollectionViewLayout
類一樣,UICollectionViewTransitionLayout
類可以被子類化以創建任何所需的效果。在這樣做時,你需要實現與創建自定義布局時相同的方法,并允許你的實現適應于用戶的輸入,最常用于手勢識別器。關于創建自定義布局對象的詳細信息,請看Creating Custom Layouts.
UICollectionViewLayout
類提供了幾個方法來跟蹤布局間的轉換,UICollectionViewTransitionLayout
對象通過transitionProgress屬性來跟蹤轉換的完成。一旦轉換發生,你的代碼定期更新此屬性以指示轉換的完成百分比。例如,使用UICollectionViewTransitionLayout
類與諸如手勢識別器之類的對象,你可以使用它們在布局之間轉換,允許您創建交互式轉換。同樣,如果您實現自定義轉換布局對象UICollectionViewTransitionLayout
類提供了兩個來跟蹤與布局相關的值的方法:updateValue:forAnimatedKey:
和valueForAnimatedKey:
方法。這些方法跟蹤可以在轉換期間設置和更改的特殊浮點值,以便與布局重要信息進行通信。例如,如果你使用捏合手勢在布局之間進行轉換,則可以使用這些方法告知轉換布局對象在視圖需要彼此偏移的位置。
在你的app中包括UICollectionViewTransitionLayout
對象的步驟如下:
- 使用
initWithCurrentLayout:nextLayout:
方法創建標準類或你自己自定義類的實例。 - 通過定期修改
transitionProgress
屬性來傳達轉換的進度。不要忘記在改變了轉換的進度之后使用集合視圖的invalidateLayout
方法來使布局廢止。 - 在你的集合視圖的代理中實現
collectionView:transitionLayoutForOldLayout:newLayout:
方法,并返回你的轉換布局對象。 - (可選)使用
updateValue:forAnimatedKey:
方法修改布局值,以指示與布局對象相關的更改值。這種情況下的穩定值為0。