原文地址:http://www.lxweimin.com/p/db55bd5f5aeb
這篇文章是我學習了大神的文章之后,幾乎抄了一份下來,所以建議通過上面地址去看原文。
首先學習幾個英文單詞
- abstract: 抽象的
- concrete: 具體的 (反義詞??)
- reusable: 可重用的
- attributes: 屬性
- collection: 集合、收集
- drag and drop: 拖和放
- supplementary: 補充的
- decoration: 裝飾、修飾、裝飾品
- vertical: 垂直的
- layout: 布局
- reference: 參考
UICollectionView
是我們常說的集合視圖,它在iOS6中引入,是iOS開發者中最受歡迎的UI元素之一。其布局靈活、可變,可用于顯示有序數據項集,最常見的用途是以類似于網格的形式呈現item,除此之外還可以通過子類化UICollectionViewLayout
類,精準地控制可視化元素布局,并動態改變布局。因此可以實現網格、堆棧、圓形、動態變化等形式布局,以及其他任何你想像出的布局。
UICollectionView
將數據源和用于呈現數據的視覺元素進行了嚴格的分離。下圖顯示了UICollectionView
與相關對象的關系:
其中data source
提供呈現數據的視圖對象,collection view layout
提供視圖布局信息,而collection view
負責將數據和布局信息合并后呈現到屏幕上。需要注意的是,在創建UICollectionView
時,必須傳遞一個UICollectionViewLayout
對象,這里的UICollectionViewLayout
是一個抽象基類abstract base class
,不能直接使用,必須使用其子類。例如,在創建網格布局時一般使用UICollectionViewFlowLayout
這個具體類concrete class
。
下面表格列出了UIKit
中與集合視圖相關的類,并按照各自扮演的角色進行分類:
1. UICollectionView和UICollectionViewController
UICollectionView
派生自UIScrollView
,定義集合視圖內容區域,將dataSource
的數據與layout
提供的布局信息合并后呈現到屏幕上。UICollectionViewController
為集合視圖提供了控制器級別支持,UICollectionViewController
的使用是可選的。
2. UICollectionViewDataSource和UICollectionViewDelegate協議
dataSource
為集合視圖提供數據,是UICollectionView
中最重要、必須提供的對象。要實現dataSource
中的方法,必須創建一個遵守UICollectionViewDataSource
協議的對象。通過UICollectionView
的delegate
對象可以監聽集合視圖狀態、自定義視圖。例如,使用delegate
跟蹤item
是否高亮、選中。與數據源對象不同,代理對象不是必須實現。
3. UICollectionReusableView和UICollectionViewCell
UICollectionView
中顯示的所有視圖都必須是UICollectionReusableView
類的實例,該類支持回收機制(循環使用視圖,而非創建新的視圖),以便提高性能,特別是在滑動屏幕時。UICollectionViewCell
用來顯示主要數據,也是可重用視圖。
4. UICollectionViewLayout、UICollectionViewLayoutAttributes和UICollectionViewUpdateItem布局
使用UICollectionViewLayout
的子類為集合視圖內元素提供位置、大小、視覺屬性等布局信息。在布局過程中,layout對象創建UICollectionViewLayoutAttributes
實例,用以告知特定item
如何布局。當collection view
的數據源發生插入、刪除、移動變化時,UICollectionView
會創建UICollectionViewUpdateItem
類的實例,并發送給layout
的prepareForCollectionViewUpdates:
方法,layout
會為即將到來的布局變化作出準備。你不需要創建該類的實例。
5. UICollectionViewFlowLayout和UICollectionViewDelegateFlowLayout協議
UICollectionViewFlowLayout
類是用于實現網格或其它基于行布局的具體類,可以直接使用,也可以將其與UICollectionViewDelegateFlowLayout
代理結合使用,以便自定義布局。
注意:上面的
UICollectionViewLayout
、UICollectionViewReusableView
類必須子類化才可以使用,其他類可以直接使用。
另外,UICollectionView
自iOS 6引入以來,其功能也是不斷豐富:
- iOS 9中為集合視圖添加了交互式重新排序功能。
- iOS 10 中為集合視圖添加了預加載cell數據功能,這在獲取cell內容非常耗時時(例如網絡請求)的情況下非常有用。
- iOS 11增加了系統范圍的施放措施drag and drop,讓用戶可以快速簡單的將文本、圖像和文件從一個app移動到另一個app。
現在我們就通過這篇文章,對UICollectionView
進行全面學習。
1.創建demo
這篇文章將使用純代碼創建一個UICollectionView
,用來學習集合視圖。效果如下:
打開Xcode,點擊file > new > project ,選擇iOS > application > single view app模板,點擊next; product name 為CollectionView, language為Objective-C,點擊next; 選擇文件位置,點擊create創建工程。
2.添加UICollectionView
為視圖控制器添加UICollectionView
,進入ViewController.m
,在接口部分添加以下聲明:
@interface ViewController ()
@property (strong, nonatomic) UICollectionView *collectionView;
@property (strong, nonatomic) UICollectionViewFlowLayout *flowLayout;
@end
在實現部分初始化UICollectionViewFlowLayout
、UICollectionView
對象。
- (UICollectionViewFlowLayout *)flowLayout {
if (!_flowLayout) {
// 初始化UICollectionViewFlowLayout對象,設置集合視圖滑動方向。
_flowLayout = [[UICollectionViewFlowLayout alloc] init];
_flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;
}
return _flowLayout;
}
- (UICollectionView *)collectionView {
if (!_collectionView) {
// 設置集合視圖內容區域、layout、背景顏色。
_collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.flowLayout];
_collectionView.backgroundColor = [UIColor whiteColor];
// 設置代理。
// _collectionView.dataSource = self;
// _collectionView.delegate = self;
}
return _collectionView;
}
最后添加到self.view
- (void)viewDidLoad {
[super viewDidLoad];
// 添加collection view。
[self.view addSubview:self.collectionView];
}
3.重用視圖以提高性能
UICollectionView
使用了視圖回收機制以提高性能。當視圖被滑出屏幕外時,從視圖層級結構中移除的視圖不會直接刪除,而是置于重用隊列中。當UICollectionView
顯示新的內容時,將從重用隊列中獲取視圖、填充新的內容。為便于回收和重用,UICollectionView
顯示的所有視圖必須派生自UICollectionReusableView
。
UICollectionView
支持三種不同類型的可重用視圖,每種視圖都有特定的用途:
集合視圖單元格
UICollectionViewCell
:顯示集合視圖的主要內容。cell必須時UICollectionViewCell
類的實例。cell默認支持管理自身高亮highlight、選中selection狀態。補充視圖
supplementary view
:顯示關于section
的信息。和cell
一樣supplementary view
也是數據驅動的,但與cell
不同的是supplementary view
的使用不是必須的,layout
控制supplementary view
的位置和是否使用。例如,流式布局UICollectionViewFlowLayout
可以選擇添加頁眉section header
和頁腳section footer
補充視圖。裝飾視圖
decoration view
:由layout
完全擁有的裝飾視圖,且不受數據源的束縛。例如,layout
可以使用裝飾視圖自定義集合視圖背景。
與UITableView
不同,UICollectionView
不會在數據源提供的cell
和supplementary view
上施加特定的樣式,只提供空白的畫布。你需要為其構建視圖層次結構、顯示圖像,也可以動態繪制內容。
UICollectionView
的數據源對象負責提供cell
和supplementary view
,但dataSource
從來不會直接創建cell
、supplementary view
。當需要展示新的視圖時,數據源對象使用集合視圖的dequeueReusableCellWithReuseIdentifier: forIndexPath:
或dequeueReusableSupplementaryViewOfKind: withReuseIdentifier: forIndexPath:
方法出列所需類型的視圖。如果隊列存在所需類型的視圖,則會直接出列所需視圖;如果隊列沒有所需視圖,則會利用提供的nib文件、storyboard或代碼創建。
現在,添加UICollectionReusableView
類,在重用視圖上添加UILabel
用以顯示header、footer
相關內容。
創建一個新的文件,選擇iOS > Source > Cocoa Touch Class模板,點擊Next;Class內容為CollectionReusableView,Subclass of一欄選擇UICollectionReusableView
,點擊Next;選擇文件位置,點擊Create創建文件。
進入CollectionReusableView.h
,聲明一個label屬性。
@interface CollectionReusableView : UICollectionReusableView
@property (strong, nonatomic) UILabel *label;
@end
進入CollectionReusableView.m
,在實現部分初始化UILabel對象:
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// 初始化label,設置文字顏色,最后添加label到重用視圖。
_label = [[UILabel alloc] initWithFrame:CGRectMake(20, 0, self.bounds.size.width-40, self.bounds.size.height)];
_label.textColor = [UIColor blackColor];
[self addSubview:_label];
}
return self;
}
4.數據源方法
UICollectionView
必須有數據源data source
,數據源對象為UICollectionView
提供展示的內容。數據源對象可能來自于app的data model,也可能來自管理UICollectionView
的視圖控制器。數據源對象必須遵守UICollectionViewDataSource
協議,并為UICollectionView
提供以下內容:
- 通過實現numberOfSectionsInCollectionView:方法獲取集合視圖包含的section數量。如果沒有實現該方法,section數量默認為1。
- 通過實現collectionView: numberOfItemsInSection:方法獲取指定section所包含的item數量。
- 通過實現collectonView: cellForItemAtIndexPath:方法返回指定item所使用的視圖類型。
Section和item是UICollectionView
基本組織結構。UICollectionView
至少包含一個section,每個section包含零至多個item。Item用來顯示主要內容,section將這些item分組顯示。
要實現UICollectionViewDataSource
數據源方法,必須遵守UICollectionViewDataSource
協議。在ViewController.m
的interface聲明遵守UICollectionViewDataSource
協議:
@interface ViewController ()<UICollectionViewDataSource>
將數據源委托給當前控制器,需要將collectionView
初始化方法中的_collectionView.dataSource = self
代碼取消注釋。
下面實現UICollectionViewDataSource
協議方法:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 2;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return 6;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
// randomColor為UIColor類擴展方法。
cell.backgroundColor = [UIColor randomColor];
return cell;
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
CollectionReusableView *reusableView;
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
// 設置header內容。
reusableView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:headerIdentifier forIndexPath:indexPath];
reusableView.label.textAlignment = NSTextAlignmentCenter;
reusableView.label.text = [NSString stringWithFormat:@"Section %li",indexPath.section];
} else {
// 設置footer內容。
reusableView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:footerIdentifier forIndexPath:indexPath];
reusableView.label.textAlignment = NSTextAlignmentNatural;
reusableView.label.text = [NSString stringWithFormat:@"Section %li have %li items",indexPath.section,[collectionView numberOfItemsInSection:indexPath.section]];
}
return reusableView;
}
NSTextAlignmentNatural會使用app當前本地化方式對齊文本。如果默認從左到右對齊,則為NSTextAlignmentLeft;如果默認從右到左對齊,則為NSTextAlignmentRight。
通過上面代碼可以看到,collectionView
有兩個section,每個section有6個item。randomColor為UIColor分類擴展方法。
現在添加UIColor
擴展文件,點擊File > New > File...,選擇iOS > Source > Objective-C File模板,點擊Next;在File名稱一欄填寫RandomColor,File Type選取Category,Class選取UIColor,點擊Next;選擇文件位置,點擊Create創建文件。
進入UIColor+RandomColor.h
方法,添加以下類方法:
@interface UIColor (RandomColor)
+ (UIColor *)randomColor;
@end
進入UIColor+RandomColor.m
,在實現部分添加以下代碼:
+ (UIColor *)randomColor {
CGFloat red = arc4random_uniform(255)/255.0;
CGFloat green = arc4random_uniform(255)/255.0;
CGFloat blue = arc4random_uniform(255)/255.0;
return [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
}
在調用dequeueReusableCellWithReuseIdentifier: forIndexPath:
方法前,必須使用registerClass: forCellWithReuseIdentifier:
或registerNib: forCellWithIdentifier:
方法告知集合視圖如何創建指定類型cell。當重用隊列中沒有指定類型cell時,collection view會使用上述注冊方法自動創建cell。如果你想要取消注冊,可以將class指定為nil。注冊時的標志符不能為nil和空字符串。
注冊supplementary view時,還需要額外指定一個稱為類型字符串kind string的附加標志符。layout負責定義各自支持的補充視圖種類。例如,UICollectionViewFlowLayout
支持兩種補充視圖:section header、section footer。為了識別這兩種類型視圖,flow layout定義了UICollectionElementKindSectionHeader
和UICollectionElementKindSectionFooter
字符串常量。在布局時,集合視圖將包括類型字符串和其它布局屬性的layout發送給數據源,數據源使用類型字符串kind string和重用標志符reuse identifier決定出列視圖。
注冊是一次性操作,且必須在嘗試出列cell、supplementary view前注冊。注冊之后,可以根據需要出列任意次數cell、supplementary view,無需再次注冊。不建議出列一個或多個視圖后更改注冊信息,最好一次注冊,始終使用。
下面注冊cell、header、footer:
static NSString * const cellIdentifier = @"cellIdentifier";
static NSString * const headerIdentifier = @"headerIdentifier";
static NSString * const footerIdentifier = @"footerIdentifier";
@implementation ViewController
- (void)viewDidLoad {
...
// 注冊cell、headerView。
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:cellIdentifier];
[self.collectionView registerClass:[CollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:headerIdentifier];
[self.collectionView registerClass:[CollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:footerIdentifier];
}
現在運行demo,顯示如下:
雖然是網格布局,但cell大小、間距均需修改,且沒有顯示section header、section footer,這些內容由UICollectionViewDelegateFlowLayout
協議定義。
5.使用Flow Layout
UICollectionViewDelegate
是一個可選但推薦實現的協議,用于管理與內容呈現、交互相關的問題。其主要工作是管理cell的高亮、選中,但可以為其擴展其它功能。例如,流布局UICollectionViewDelegateFlowLayout
協議增加了控制cell大小、間距功能。
Flow Layout實現了基于行的中斷布局,即layout將cell放置在線性路徑上,并盡可能多的沿著該路徑排布cell。如果當前路徑空間不足,layout將創建一個新路徑并繼續布局。下圖顯示了垂直滾動的流布局。在這種情況下,cell橫向放置,新增加的路徑位于之前路徑下方。Section可以選擇性的添加section header、section
footer視圖。
Flow Layout除了實現網格布局,還可以實現許多不同設計。例如:通過調整cell間距minimumInteritemSpacing
、大小itemSize
來創建在滾動方向只有一個cell的布局。cell大小也可以不同,這樣會產生比傳統網格更不對稱的布局。
可以通過Xcode中的Interface Builder,或純代碼配置flow layout。步驟如下:
創建flow layout,并將其分配給
UICollectionView
。配置cell大小
itemSize
。如果沒有設置,默認寬高均為50
。配置cell行
minimumLineSpacing
、cell間minimumInteritemSpacing
間距,默認值為10.0
。如果用到了section header、section footer,配置其大小
headerReferenceSize
、footerReferenceSize
。默認值為(0,0)。指定
layout
滑動方向scrollDirection
。默認滑動方向為UICollectionViewScrollDirectionVertical
。
UICollectionView
所使用的layout
與應用程序視圖層級結構中使用的自動布局Auto Layout不同,不要混淆集合視圖內layout
對象與父視圖內重新定位子視圖的layoutSubviews
。layout
對象從不直接觸及其管理的視圖,因為實質上layout
并不擁有任何視圖。相反,layout
只生成集合視圖中cell、supplementary view、decoration view的位置、大小和可視外觀屬性,并將這些屬性提供給UICollectionView
,由UICollectionView
將這些屬性應用于實際視圖對象。
聲明ViewController
遵守UICollectionViewDelegate
、UICollectionViewDelegateFlowLayout
協議。將delegate
賦給當前控制器,即取消collectionView
初始化方法中_collectionView.delegate = self;
的注釋。
5.1設置cell大小itemSize
所有cell大小一致,最為快捷方式是為itemSize屬性賦值,如果cell大小不同,則必須使用collectionView: layout: sizeForItemAtIndexPath:
方法。
如果cell大小不同,則每行cell數量可能不同。
進入ViewController.m
,在實現部分添加以下代碼,配置cell大小。
// 設置item大小。
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return CGSizeMake(153, 128);
}
運行demo,如下所示:
5.2設置section header和section footer大小
在布局section header、section footer時,只有與滑動方向相同的值會被采用。例如,垂直滾動的UICollectionView,layout只使用colllectionView: layout: referenceSizeForHeaderInSection:
、collectionView: layout: referenceSizeForFooterInSection:
、headerReferenceSize
、footerReferenceSize
提供的高,寬會被設置為UICollectionView
的寬。如果滑動方向的長度被設置為0,則supplementary view不可見。
進入ViewController.m
[圖片上傳中...(3151492-8159712abfe8cc7b.png-c34530-1516353881306-0)]
,在實現部分添加以下代碼,設置section header、section footer大小。
// 設置section header大小。
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
return section == 0 ? CGSizeMake(40, 40) : CGSizeMake(45, 45);
}
// 設置section footer大小。
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section {
return CGSizeMake(35, 35);
}
運行demo,如下所示:
5.3設置item間距minimumInteritemSpacing
利用flow layout可以指定cell間、行間最小間距,但其實際間距可能大于最小間距。當布局時,flow layout將cell添加到當前行,直到沒有足夠的空間來放置另一個cell。如果剛好可以排布整數個cell,那么cell間的間距等于最小間距。如果行尾有額外的空間,又不能放下另一個cell,flow layout將增加cell間距,直到cell在行內均勻排布,這時cell間距將大于minimumInteritemSpacing
。
進入ViewController.m
,在實現部分添加以下代碼,設置item間距。
// 設置item間距。
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
return 20;
}
運行demo,如下所示:
這里每行只能排布兩個cell,所以實際間距大于設置的最小間距20。
5.4設置行間距minimumLineSpacing
對于行間距,flow layout采用與設置cell間距一樣技術。如果所有cell大小相同,flow layout會嚴格遵守最小間距設置,即每一行的cell在同一條線上,相鄰行cell間距等于minimumLineSpacing
。
如果cell大小不同,flow layout會在滑動方向選取每行最大cell。例如,在垂直方向滑動,flow layout會選取每行高最大的cell,隨后設置這些高最大的cell間距為minimumLineSpacing
。如果這些高最大的cell位于行不同位置,行間距看起來會大于minimumLineSpacing
。如下所示:
進入ViewController.m
,在實現部分添加以下代碼,設置item行間距。
// 設置行間距。
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 20;
}
運行demo,如下所示:
這個demo中所有cell大小相同,所以這里的minimumLineSpacing會嚴格遵守設置的minimumLineSpacing
間距20。
5.5使用section inset設置內容邊距
使用sectionInset可以調整可供放置cell區域大小,如增加section header、section footer與cell間距,增加行首、行尾間距。下圖顯示了sectionInset如何影響垂直滾動的UICollectionView。
因為sectionInset
減少了可供放置cell的空間,可以用此屬性限制每行cell數量。例如,在非滑動方向設置inset,可以減少每行可用空間,同時配合設置itemSize,可以控制每行cell數量。
繼續在ViewController.m
實現部分添加以下代碼,設置sectionInset。
// 設置頁邊距。
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(0, 20, 0, 20);
}
運行demo,如下所示:
使用UICollectionViewDelegateFlowLayout
協議可以動態調整布局信息。例如,不同item大小不同,不同section內item間距不同。如果沒有提供代理方法,flow layout會使用通過屬性設置的值。上面代碼除設置section header大小部分,均可使用屬性進行設值,如下所示:
- (UICollectionViewFlowLayout *)flowLayout {
if (!_flowLayout) {
...
// 通過屬性設值。
_flowLayout.itemSize = CGSizeMake(153, 128);
_flowLayout.footerReferenceSize = CGSizeMake(35, 35);
_flowLayout.minimumLineSpacing = 20;
_flowLayout.minimumInteritemSpacing = 20;
_flowLayout.sectionInset = UIEdgeInsetsMake(0, 20, 0, 20);
}
return _flowLayout;
}
現在運行app,如下所示:
6.數據模型
高性能的數據源使用section和item來組織其底層數據對象,這樣會使數據源方法更易實現。數據源方法會被頻繁調用,所以在數據源檢索數據時必須足夠快。
一個簡單的解決辦法(但不是唯一的)是讓數據模型使用一組嵌套數組,嵌套數組內元素為section的數組,section數組內元素為該section內item。檢索某個item就變成了先檢索其section數組,再在該section數組內檢索該item。這種模式適合于中等規模的數據模型。
當設計數據結構時,始終可以從簡單數組開始,根據需要遷移到更高效結構。通常,數據對象不應成為性能瓶頸。UICollectionView
通過訪問數據對象以獲得共有多少個對象,并獲取當前屏幕上顯示對象的視圖。如果layout僅依賴于數據對象,當數據對象包含數千個對象時,性能會受到嚴重影響。
現在,為這個demo添加一個數據模型。
打開Xcode,選擇File > New > File...,在彈出窗口選擇iOS > Source > Cocoa Touch Class模板,點擊Next;Class一欄填寫SimpleModel
,Subclass of選擇NSObject
,點擊Next;選擇文件位置,點擊Create創建文件。
進入SimpleModel.h
文件,聲明一個可變數組model
。
@interface SimpleModel : NSObject
@property (strong, nonatomic) NSMutableArray *model;
@end
進入SimpleModel.m
文件,設置model可變數組包含另外兩個可變數組section1
、section2
,這兩個可變數組分別包含六個元素。
- (instancetype)init {
self = [super init];
if (self) {
NSMutableArray *section1 = [NSMutableArray arrayWithObjects:@"1",@"2",@"3",@"4",@"5",@"6", nil];
NSMutableArray *section2 = [NSMutableArray arrayWithObjects:@"A",@"B",@"C",@"D",@"E",@"F", nil];
_model = [NSMutableArray arrayWithObjects:section1,section2, nil];
}
return self;
}
打開Assets.xcassets,添加github/pro648/BasicDemos-iOS這里的照片,也可以通過文章底部的源碼鏈接下載源碼獲取。
7.自定義UICollectionViewCell
子類
自定義UICollectionViewCell
子類,并為其添加UIImageView
和UILabel
對象的屬性。
打開Xcode,選擇File > New > File...,在彈出窗口選擇iOS > Source > Cocoa Touch Class,點擊Next;Class一欄填寫CollectionViewCell,Subclass of選擇UICollectionViewCell
,點擊Next;選擇文件位置,點擊Create創建文件。
進入CollectionViewCell.h
文件,聲明一個imageView和一個label屬性。
@interface CollectionViewCell : UICollectionViewCell
@property (strong, nonatomic) UIImageView *imageView;
@property (strong, nonatomic) UILabel *label;
@end
進入CollectionViewCell.m
文件,初始化imageView
和label
屬性。
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// 1.初始化imageView、label。
CGFloat cellWidth = self.bounds.size.width;
CGFloat cellHeight = self.bounds.size.height;
_imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, cellWidth, cellHeight * 4/5)];
_label = [[UILabel alloc] initWithFrame:CGRectMake(0, cellHeight * 4/5, cellWidth, cellHeight * 1/5)];
_label.textAlignment = NSTextAlignmentCenter;
// 2.添加imageView、label到cell。
[self.contentView addSubview:_imageView];
[self.contentView addSubview:_label];
}
return self;
}
進入ViewController.m
文件,導入CollectionViewCell.h
和SimpleModel.h
文件,聲明類型為SimpleModel
的simpleModel
屬性。
#import "CollectionViewCell.h"
#import "SimpleModel.h"
@interface ViewController ()<UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
...
@property (strong, nonatomic) SimpleModel *simpleModel;
@end
更新cell注冊方法,并初始化simpleModel
屬性。
- (void)viewDidLoad {
...
// 更新cell注冊方法。
[self.collectionView registerClass:[CollectionViewCell class] forCellWithReuseIdentifier:cellIdentifier];
...
// 初始化simpleModel
self.simpleModel = [[SimpleModel alloc] init];
}
現在更新數據源方法。
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return self.simpleModel.model.count;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return [self.simpleModel.model[section] count];
}
- (CollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {
CollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
// 設置imageView圖片,label文字。
NSString *imageName = [self.simpleModel.model[indexPath.section] objectAtIndex:indexPath.item];
cell.imageView.image = [UIImage imageNamed:imageName];
NSString *labelText = [NSString stringWithFormat:@"(%li, %li)",indexPath.section, indexPath.item];
cell.label.text = labelText;
return cell;
}
dataSource必須返回一個有效的視圖,不能為nil,即使由于某種原因該視圖不該被顯示。layout期望返回有效視圖,如果返回nil視圖會導致app終止。
運行app,如下所示:
8.重新排序cell
自iOS 9,Collection View允許根據用戶手勢重新排序cell。如需支持重新排序功能,需要添加手勢識別器跟蹤用戶手勢與集合視圖的交互,同時更新數據源中item位置。
為UICollectionView
添加長按手勢識別器,并實現響應方法。
- (void)viewDidLoad {
...
// 為collectionView添加長按手勢。
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(reorderCollectionView:)];
[self.collectionView addGestureRecognizer:longPressGesture];
}
// 長按手勢響應方法。
- (void)reorderCollectionView:(UILongPressGestureRecognizer *)longPressGesture {
switch (longPressGesture.state) {
case UIGestureRecognizerStateBegan:{
// 手勢開始。
CGPoint touchPoint = [longPressGesture locationInView:self.collectionView];
NSIndexPath *selectedIndexPath = [self.collectionView indexPathForItemAtPoint:touchPoint];
if (selectedIndexPath) {
[self.collectionView beginInteractiveMovementForItemAtIndexPath:selectedIndexPath];
}
break;
}
case UIGestureRecognizerStateChanged:{
// 手勢變化。
CGPoint touchPoint = [longPressGesture locationInView:self.collectionView];
[self.collectionView updateInteractiveMovementTargetPosition:touchPoint];
break;
}
case UIGestureRecognizerStateEnded:{
// 手勢結束。
[self.collectionView endInteractiveMovement];
break;
}
default:{
[self.collectionView cancelInteractiveMovement];
break;
}
}
}
長按手勢響應步驟如下:
- 要開始交互式移動item,Collection View調用
beginInteractiveMovementForItemAtIndexPath:
方法; - 當手勢識別器跟蹤到手勢變化時,集合視圖調用
updateInteractiveMovementTargetPosition:
方法報告最新觸摸位置; - 當手勢結束時,
UICollectionView
調用endInteractiveMovement
方法結束交互并更新視圖; - 當手勢中途取消或識別失敗,
UICollectionView
調用cancelInteractiveMovement
方法結束交互。
如果想要對手勢識別器進行更全面了解,可以查看手勢控制:點擊、滑動、平移、捏合、旋轉、長按、輕掃這篇文章。
在交互過程中,Collection view會動態的使布局無效,以反映當前item最新布局。默認的layout
會自動重新排布item,你也可以自定義布局動畫。
UICollectionViewController默認安裝了長按手勢識別器,用來重新排布集合視圖中cell,如果需要禁用重新排布cell手勢,設置installStandardGestureForInteractiveMovement屬性為NO。
當交互手勢結束時,如果item位置放生了變化,UICollectionView
會調用以下方法更新數據源。
// 是否允許移動item。
- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
// 更新數據源。
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
NSString *sourceObject = [self.simpleModel.model[sourceIndexPath.section] objectAtIndex:sourceIndexPath.item];
[self.simpleModel.model[sourceIndexPath.section] removeObjectAtIndex:sourceIndexPath.item];
[self.simpleModel.model[destinationIndexPath.section] insertObject:sourceObject atIndex:destinationIndexPath.item];
// 重新加載當前顯示的item。
[collectionView reloadItemsAtIndexPaths:[collectionView indexPathsForVisibleItems]];
}
集合視圖會先調用collectionView: canMoveItemAtIndexPath:
方法,看當前item是否允許移動。如果沒有實現該方法,但實現了collectionView: moveItemAtIndexPath: toIndexPath:
方法,集合視圖會允許所有item被移動。當交互手勢結束時,UICollectionView
會自動調用collectionView: moveItemAtIndexPath: toIndexPath:
,如果該方法沒有實現,則移動cell請求會被忽略。
運行app,移動item。
在更新數據源時,按照以下步驟操作:
更新數據源中數據。
調用
UICollectionView
方法進行插入、刪除、移動section或item操作。
必須先更新數據源,后更改UICollectionView
。UICollectionView
中方法會假定當前數據源包含正確數據,如果數據有誤,集合視圖可能會得到錯誤數據,也可能請求不存在的數據,導致app崩潰。
以編程的方式添加、刪除、移動單個item時,collection view會自動創建動畫以反映更改。如果你想要將多個插入、刪除、移動操作合并為一個動畫,則必須將這些操作放到一個塊內,并將該塊傳遞給performBatchUpdates: completion:
方法。批量更新會在同一時間更新所有操作。
在performBatchUpdates: completion:方法中,刪除操作會在插入操作之前進行。也就是說,刪除操作的index是collection view在執行批量更新batch update前的index,插入操作的index是collection view在執行完批量更新中刪除操作后的index。
9.使用drag and drop排序
iOS 11增加了系統范圍的拖放操作drag and drop,讓用戶可以快速簡單的將文本、圖像和文件從一個app移動到另一個app,只需輕點并按住即可提取其內容,拖放到其它位置。
UICollectionView
通過專用API支持drag和drop,我們可以使用drag和drop來重新排序cell。
- 為了支持drag操作,定義一個drag delegate對象,并將其賦值給collection view的dragDelegate,該對象必須遵守
UICollectionViewDragDelegate
協議; - 為了支持drop操作,定義一個drop delegate對象,并將其賦值給collection view的dropDelegate,該對象必須遵守
UICollectionViewDropDelegate
協議。
注釋掉上一部分使用長按手勢重新排序cell的代碼,現在使用drag and drop重新排序。
所有拖放drag and drop功能都可以在iPad上使用。在iPhone上,拖放功能只能在應用內使用,不可在應用間拖放。
app可以只遵守UICollectionViewDragDelegate、UICollectionViewDropDelegate中的一個協議。
進入ViewController.m
文件,聲明視圖控制器遵守UICollectionViewDragDelegate
、UICollectionViewDropDelegate
協議。同時,將視圖控制器賦值給dragDelegate、dropDelegate屬性。
@interface ViewController ()<UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDragDelegate, UICollectionViewDropDelegate>
- (void)viewDidLoad {
...
// 開啟拖放手勢,設置代理。
self.collectionView.dragInteractionEnabled = YES;
self.collectionView.dragDelegate = self;
self.collectionView.dropDelegate = self;
}
9.1從集合視圖中拖起item
UICollectionView
管理大部分與拖動相關的交互,但你需要指定要拖動的item。當拖動手勢發生時,集合視圖創建一個拖動會話,調用collectionView:itemsForBeginningDragSession:atIndexPath:
代理方法。如果該方法返回非空數組,則集合視圖將開始拖動指定item。如果不允許拖動指定索引路徑的item,則返回空數組。
在實現collectionView:itemsForBeginningDragSession:atIndexPath:
方法時,按照以下步驟操作:
- 創建一個或多個
NSItemProvider
,使用NSItemProvider
傳遞集合視圖item內容。 - 將每個
NSItemProvider
封裝在對應UIDragItem
對象中。 - 考慮為每個
dragItem
的localObject
分配要傳遞的數據。這一步驟是可選的,但在同一app內拖放時,localObject
可以加快數據傳遞。
返回dragItem。
在ViewController.m
文件中,實現上述方法:
- (NSArray <UIDragItem *>*)collectionView:(UICollectionView *)collectionView itemsForBeginningDragSession:(id<UIDragSession>)session atIndexPath:(NSIndexPath *)indexPath {
NSString *imageName = [self.simpleModel.model[indexPath.section] objectAtIndex:indexPath.item];
NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:imageName];
UIDragItem *dragItem = [[UIDragItem alloc] initWithItemProvider:itemProvider];
dragItem.localObject = imageName;
return @[dragItem];
}
如果需要支持一次拖動多個item,還需要實現collectionView:itemsForAddingToDragSession:atIndexPath:point:
方法,其實現代碼與上面部分相同。
運行app,如下所示:
使用collectionView:dragPreviewParametersForItemAtIndexPath:
方法,可以自定義拖動過程中cell外觀。如果沒有實現該方法,或實現后返回nil,collection view將使用cell原樣式呈現。
在該方法的實現部分,創建一個UIDragPreviewParameters
對象,并更新指定item的預覽信息。使用UIDragPreviewParameters
可以指定cell的可視部分,或改變cell背景顏色,如下所示:
// 設置拖動預覽信息。
- (nullable UIDragPreviewParameters *)collectionView:(UICollectionView *)collectionView dragPreviewParametersForItemAtIndexPath:(NSIndexPath *)indexPath {
// 預覽圖為圓角,背景色為clearColor。
UIDragPreviewParameters *previewParameters = [[UIDragPreviewParameters alloc] init];
CollectionViewCell *cell = (CollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
previewParameters.visiblePath = [UIBezierPath bezierPathWithRoundedRect:cell.bounds cornerRadius:10];
previewParameters.backgroundColor = [UIColor clearColor];
return previewParameters;
}
運行app,如下所示:
可以看到,預覽cell為圓角。
9.2 接收拖動cell內容
當內容被拖入集合視圖邊界內時,集合視圖會調用collectonView:canHandleDropSession:
方法,查看當前數據模型是否可以接收拖動的內容。如果可以接收拖動的內容,集合視圖會繼續調用其它方法。
當用戶手指移動時,集合視圖跟蹤手勢,檢測可能的drop位置,并通知collectionView:dropSessionDidUpdate:withDestinationIndexPath:
代理方法。該方法可選實現,但一般推薦實現。實現該方法后,UICollectonView
會及時反饋將如何合并、放置拖動的cell到當前視圖。該方法會被頻繁調用,實現過程要盡可能快速、簡單。
當手指離開屏幕時,UICollectionView
會調用collectionView:performDropWithCoordinator:
方法,必須實現該方法以接收拖動的數據。實現步驟如下:
枚舉`coordinator`的`items`屬性。
不同類型item,采取不同接收方法:
如果`item`的`sourceIndexPath`存在,則item始于集合視圖,可以使用批量更新batch update從當前位置刪除item,插入到新的位置。
如果item的localObject屬性存在,則item始于app其它位置,必須插入item到數據模型。
前面兩種均不滿足時,使用`NSItemProvider的itemProvider`屬性,異步提取數據,插入到數據模型。
更新數據模型,刪除、插入collection view中item。
繼續在ViewController.m
中添加以下代碼:
// 是否接收拖動的item。
- (BOOL)collectionView:(UICollectionView *)collectionView canHandleDropSession:(id<UIDropSession>)session {
return [session canLoadObjectsOfClass:[NSString class]];
}
// 拖動過程中不斷反饋item位置。
- (UICollectionViewDropProposal *)collectionView:(UICollectionView *)collectionView dropSessionDidUpdate:(id<UIDropSession>)session withDestinationIndexPath:(NSIndexPath *)destinationIndexPath {
UICollectionViewDropProposal *dropProposal;
if (session.localDragSession) {
// 拖動手勢源自同一app。
dropProposal = [[UICollectionViewDropProposal alloc] initWithDropOperation:UIDropOperationMove intent:UICollectionViewDropIntentInsertAtDestinationIndexPath];
} else {
// 拖動手勢源自其它app。
dropProposal = [[UICollectionViewDropProposal alloc] initWithDropOperation:UIDropOperationCopy intent:UICollectionViewDropIntentInsertAtDestinationIndexPath];
}
return dropProposal;
}
- (void)collectionView:(UICollectionView *)collectionView performDropWithCoordinator:(id<UICollectionViewDropCoordinator>)coordinator {
// 如果coordinator.destinationIndexPath存在,直接返回;如果不存在,則返回(0,0)位置。
NSIndexPath *destinationIndexPath = coordinator.destinationIndexPath ? coordinator.destinationIndexPath : [NSIndexPath indexPathForItem:0 inSection:0];
// 在collectionView內,重新排序時只能拖動一個cell。
if (coordinator.items.count == 1 && coordinator.items.firstObject.sourceIndexPath) {
NSIndexPath *sourceIndexPath = coordinator.items.firstObject.sourceIndexPath;
// 將多個操作合并為一個動畫。
[collectionView performBatchUpdates:^{
// 將拖動內容從數據源刪除,插入到新的位置。
NSString *imageName = coordinator.items.firstObject.dragItem.localObject;
[self.simpleModel.model[sourceIndexPath.section] removeObjectAtIndex:sourceIndexPath.item];
[self.simpleModel.model[destinationIndexPath.section] insertObject:imageName atIndex:destinationIndexPath.item];
// 更新collectionView。
[collectionView deleteItemsAtIndexPaths:@[sourceIndexPath]];
[collectionView insertItemsAtIndexPaths:@[destinationIndexPath]];
} completion:nil];
}
}
現在運行app,如下所示:
對于必須使用NSItemProvider
檢索的數據,需要使用dropItem:toPlaceHolderInsertedAtIndexPath:withReuseIdentifier:cellUpdateHandler:
方法先將占位符placeholder
插入,之后異步檢索數據,具體方法這里不再介紹。
iOS 11也為UITableView增加了drag和drop功能,其API非常相似。
10. 總結
UICollectionView
非常強大,除系統提供的這些布局風格,你還可以使用自定義布局custom layout滿足你的各種需求。
如果覺得從數據源獲取數據很耗時,可以使用UICollectionViewDataSourcePrefetching
協議,該協議會協助你的數據源在還未調用collectionView:cellForItemAtIndexPath:
方法時進行預加載。詳細內容可以查看文檔進一步學習。
Demo名稱:CollectionView
源碼地址:https://github.com/pro648/BasicDemos-iOS
參考資料: