[译] 精通 UICollectionView (Swift版): 1/2

译:Beginning iOS Collection Views in Swift: Part 1/2

Create your own grid-based photo browsing app with collection views!
Create your own grid-based photo browsing app with collection views!

更新说明:原文作者 Brandon Trebitowski,现 Richard Turton 已将内容升级为 Swift 和 iOS 8。

这个iOS 图片小App通过多个layout来实现时髦的图片显示。你可以使用网格视图来查看你的图片集:

iOS Photos app in album view

或者,你可以通过Stacks的方式来浏览相册:

iOS Photos app in album view

你甚至可以在两个Layouts之前通过很Cool的手势来做转换。“我想要这个”,你可能这样想。

UICollectionView 使得你构建自定义的layout或layout 之间转变变得非常简单(就像我们的Photo App)。

不仅限于 stacks 和 grid,Collection view 支持高度定制
。你可以使用它来做一个环形的布局,cover-flow 样式布局,Pulse news 样式布局 - 差不多就是你可以用它来自定义任何想要的布局!好消息是,如果你熟悉 UITableView,你对 Collection view也不陌生 - 它们使用相同的 data source 和 delegate 模式。

在该教程中,你会通过一个网格型图片浏览App来体验和学习 UICollectionView。当你随着这篇教程结束时,你将学到collection view的基本知识,并可以在你自己的app中使用这个很赞的技术。

剖悉 UICollectionView

我们直接从一个已完成的项目开始,UICollectionView 包含多个关键点,如下:

View of collection view app with parts highlighted

一个一个来看:

  1. UICollectionView – 内容显示的主要视图,和 UITableView 相像,和 table view一样的是,collection view 也继承自 UIScrollView
  2. UICollectionViewCell – 像是 table view 中的 UITableViewCell。这些Cell组成内容视图,并作为子视图被添加到collection view。Cell可以通过手写代码或IB创建。
  3. Supplementary Views – 如果你需要显示一些特别数据,但这些内容不是写在cell中,而是位于其它位置,你会需要supplementray views。通常来说我们指的是header 或 footers。

Note: Collection views can also have Decoration Views – if you want to add some extra views to enhance the appearance of the collection view (but don’t really contain useful data), you should use decoration views. Background images or other visual embellishments are good examples of decoration views. You won’t be using decoration views in this tutorial as it requires you to write a custom layout class. Collection views

每个collection view有一个layout对象,它负责内容的大小,位置和其它的一些属性。Layout 对象 继承自 UICollectionViewLayout。Layout 不仅能在 collection view 运行时进行转换,还能自动从一个布局切换到另一个。

你可以继承UICollectionViewLayout 来创建自定义的layout,Apple本身也提供了一个很基础的 叫作 UICollectionViewFlowLayout。它按照元素的大小一个接一个的进行排列,很像grid view。你可以直接使用该布局,或者继承它来实现更多好玩的动作和效果。

接下来你会深入了解这些元素。但现在,是时候动手开始这个项目了!

介绍 FlickrSearch

接下来的部分,你将去创建一个很酷的图片浏览app,叫 FlickrSearch。你可以在著名的图片分享网站Flickr上搜索图片,然后下图并按grid view的样式显示,就像刚才的那张截图一样。

准备好了?打开Xcode,点击 File\New\Project… 然后选择 iOS\Application\Single View Application

Xcode choosing single view template

该模板只为你创建了一个简单的 UIViewController 和一个storyboard。作为一个从头开始的项目,已经足够了。

点击 Mext 然后填 写Application的其它信息,设置 Production Name 为 FlickrSearch,Device type设为 iPad,language使用 Swift。 点击 Next 来选择项目路径,最后点击 Create。

Xcode project setup

这个新建的类和Staryboard都是通过Single View applelication 模板创建的,这个不适合我们,-我们想要的是UICollectionViewController,一个像是UITableViewController的类,继承它从而实现collection view的操作。删除ViewController.swift,然后将Storyboard中的ViewController 也都删掉。现在我们算是拥有一个真正的空项目: ;]

打开 AppDelegate.swift 并添加一个常量来设定一个让人愉悦的绿色。将下面代码添加到 import UIKit 下面:

1
let themeColor = UIColor(red: 0.01, green: 0.41, blue: 0.22, alpha: 1.0)

使用该颜色作为App主题色。在application(_:didFinishLaunchingWithOptions)加入下面代码:

1
2
3
4
5
6
func application(application: UIApplication!, didFinishLaunchingWithOptions
launchOptions: NSDictionary!) -> Bool {
window?.tintColor = themeColor
return true
}

开始使用 Collection

打开 Main.stroyboard,拖入一个Collection View Controller,把这个Controller嵌入到一个Navigation Controller,并把该Navigation Controller设置为root

Storyboard中现在应该有一个layout,看起来如下:

UICollectionViewController embedded in a navigation controller

确保Navigation Controller已被设置为初始 View Controller(注意图中最左边小箭头)。

选择 collection view,然后通过Attributes inspector设置背景颜色为白色。

Setting the collection view's background color

注意:在截图中可以看到Layout被设置为Layout - 我们之前提到过的 flow layout。

Note: Wondering what the Scroll Direction property does? This property is specific to UICollectionViewFlowLayout, and defaults to Vertical. A vertical flow layout means the layout class will place items from left to right across the top of the view until it reaches the view’s right edge, at which point it moves down to the next line. If there are two many elements to fit in the view at once, the user will be able to scroll vertically to see more.

Conversely, a horizontal flow layout places items from top to bottom across the left edge of the view until it reaches the bottom edge. Users would scroll horizontally to see items that don’t fit on the screen. In this tutorial, you’ll stick with the more common Vertical collection view.

在Collection view 中选中一个single cell 并在Attributes inspector中设置它的 Reuse IndentifierFlickrCell。这依然和table view相似 - data source 会通过这些 identifier 去重用或创建新的cell。

向collection view的navigation bar中间拖入一个text field。用户可以在这里输入要进行搜索的文字。设置 placeholder 显示文字,然后,同时按住 control 并从text field拖拽到controller view,选择 delegate outlet:

Setting the text field's delegate

UICollectionViewController 中有很多操作,但通常来说,需要去创建一个新的子类。点击File\New\File…,选择 Cocoa Touch Class,为新的类全名为 FlickrPhotosViewController,让它继承自UICollectionViewController。模板会自动为你添加大量代码,但最好的方式是理解这个类到底做了什么。打开FlickrPhotosViewController.swift,然后删除除了类定义和import UIKit之外的所有代码,这个文件看起来如下:

1
2
3
4
5
import UIKit
class FlickrPhotosViewController : UICollectionViewController {
}

在类定义中,添加一个常量来匹配storyboard中定义过的reuse identifier

1
private let reuseIdentifier = "FlickrCell"

接下来的学习中,我们会一点点的完成这些代码工作。

现在回到 Main.stroyboard,选择 collection view controller,在indentity inspector中设置它的类为 FlickrPhotosViewController

Setting the custom class of a collection view controller

获取 Flickr 的照片集

你的第一个任务就是喊出章节标题,大喊10次,OK,当然了,这是开玩笑的。

Flickr是一个很赞的图片分享服务,它有着大量的简单易用的API给开发者使用。利用这些API你可以搜索、添加图片,评论图片等等。

使用 Flickr API前,你需要一个 API key,如果你是在做一个真正的项目,建议你自行注册: http://www.flickr.com/services/api/keys/apply/.

测试用的项目不必申请,Flickr同时提供了一个简单的key用于那些不常调用的用户:http://www.flickr.com/services/api/explore/?method=flickr.photos.search 从下面复制出API key,每对值之前用&间隔,然后把这串值复制到一个文档编辑器中,以供以后使用。

这段URL格式如下:

1
2
http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=6593783 efea8e7f6dfc6b70bc03d2afb&
format=rest&api_sig=f24f4e98063a9b8ecc8b522b238 d5e2f

API key: 6593783efea8e7f6dfc6b70bc03d2afb

Note: If you use the sample API key, note that it is changed periodically. So if you’re doing this tutorial over the course of several days, you might find that you have to get a new API key every so often. For this reason it might be easier to get an API key of your own from Flickr if you think you’re going to spend several days on this project.

鉴于本文讲的是UICollectionView而非Flickr API,我创建了多个类来抽像搜索部分。你可以直接下载使用。

解压并拖拽FlickrSearcher.swift到你的项目中,确保 Copy items into destination group’s folder (if needed) 被选中,然后点击完成。

该文件中包含两个class和一个struct

  • FlickrSearchResults: 搜索结果集。
  • FlickrPhoto: 从Flickr上拉下来的数据 - 包括 thumbnail, image, 和其它 metadata 信息,像 ID. 也有一些方法来构建 Flickr URL和大小计算。FlickrSearchResults是一个包含这个对象的数组。
  • Flickr: 提供了简单的 block-based API 进行查询,并返回FlickrSearchResults结果集。

随意看下这些代码,你会发现它相当简单,你可以把它用它在你的Flickr项目中。

在你要进行搜索前,你需要先输入一个API key。打开 FlickrSearcher.swift 然后替换掉apiKey的值。

1
let apiKey = "hh7ef5ce0a54b6f5b8fbc36865eb5b32"

在下一章节,我们将会做一些简单的准备工作。

准备数据结构

我们计划在App中每次执行搜索后,都添加一个新的section来显示结果(而不是简单替换掉之前的搜索结果)。举例来说,如果你先搜索“ninjas” 然后再搜索 “pirates”,table view中将会有两个section,一个是nanjas,另一个是pirates。

要实现这个功能, ,我们需要建一个数据结构来为每一个section保存数据。FlickrSearchResults 类型的数组显然就是干这事的。

打开 FlickrPhotosViewController.swift,然后添些几个属性和一个实用方法:

1
2
3
4
5
6
private var searches = [FlickrSearchResults]()
private let flickr = Flickr()
func photoForIndexPath(indexPath: NSIndexPath) -> FlickrPhoto {
return searches[indexPath.section].searchResults[indexPath.row]
}

searches变量是一个数组,用来保存所有搜索的结果集,flickr常量则用来提供查询方法。

photoForIndexPath 作为一个便利方法,可以让我们很方便的根据collection view中的index来取到相对应的photo数据。接下来我们会大量的用到这种方式,写成独立方法会方便很多。

得到搜索结果

现在,你已经可以开始实现搜索了!你想在用户输入完成后触发搜索功能。你已经将text field的delegate添加到该collection view controller,现在我们来实现它。

打开FlickrPhotosViewController.swift 文件,在声明部分添加UITextFieldDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class FlickrPhotosViewController: UICollectionViewController, UITextFieldDelegate {
Then add the relevant delegate method:
// MARK : UITextFieldDelegate
func textFieldShouldReturn(textField: UITextField!) -> Bool {
// 1
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
textField.addSubview(activityIndicator)
activityIndicator.frame = textField.bounds
activityIndicator.startAnimating()
flickr.searchFlickrForTerm(textField.text) {
results, error in
//2
activityIndicator.removeFromSuperview()
if error != nil {
println("Error searching : \(error)")
}
if results != nil {
//3
println("Found \(results!.searchResults.count) matching \(results!.searchTerm)")
self.searches.insert(results!, atIndex: 0)
//4
self.collectionView.reloadData()
}
}
textField.text = nil
textField.resignFirstResponder()
return true
}

这段代码的原理:

  1. 添加完activity view之后,使用之前提供的 Flickr wrapper 类来实现Flickr上照片搜索。搜索执行结束后,会通过block返回 FlickrPhoto 数组,和一个Error(如果有。)
  2. 把错误输出到console。当然,线上产口是不应该输出这些错误给用户的。
  3. 结果已经被输出并添加到结果数组的顶部。
  4. 在这个阶段,你拿到了新的数据然后需要刷新UI。你应该使用insertSections方法来将结果插入到列表集的顶部。

接下来运行该程序。在文本框中输入并执行搜索,你可以在console中看到搜索记录,像是:

1
Found 20 matching bananas

注意,Flickr把搜索结果限定在20内来保证加载时间。

很不幸的是,你在collection view中看不到任何的图片!就像table view,你要手工来实现data source和delegate协议方法,它才会真正的工作。

Feeding the UICollectionView

可能你已经猜到了,当你使用table view时,你需要设置它的data source和delegate来确保数据显示和事件处理(像行选中)。

相同的,当你要使用collection view你也需要实现它的data source和delegate:

  • data source (UICollectionViewDataSource) 返回collection view的数量信息和包含 的views。
  • The delegate (UICollectionViewDelegate) 当事件触发时的通知,像cell被选中,高亮或者删除。

UICollectionViewFlowLayout 除此之处还有一个 deletate 协议 - UICollectionViewDelegateFlowLayout。它允许你去扭动布局的行为,配置像cell间隙,滚动方向等属性。

这一部分中,你需要在view controller中实现UICollectionViewDataSourceUICollectionViewDelegateFlowLayout 的协议方法,UICollectionViewDelegate在这一部分不是必须的,你将在第二部分来使用它。

UICollectionViewDataSource

打开FlickrPhotosViewController.swift, 添加如下的datasource方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// MARK: UICollectionViewDataSource
//1
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return searches.count
}
//2
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return searches[section].searchResults.count
}
//3
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as UICollectionViewCell
cell.backgroundColor = UIColor.blackColor()
// Configure the cell
return cell
}

这些方法都相当的简单:

  1. 每一次搜索都会创建一个section,所以section的数量就是搜索的次数。
  2. number of items in a section 是searchResults搜索结果个数,里面包含的是FlickrSearch对象。
  3. 这个方法暂时只是返回一个空的cell,后面会来实现它。记往,collection view需要为注册的cell实现一个reuse identifier,否则会在运行时出错。

Build然后再次run,重新执行搜索。你应该看到20个新的Cell,虽然看起有点呆:

Boring black cells

UICollectionViewFlowLayoutDelegate

就像我在之前所提到过的,每一个collection view都有一个相关联的layout。我们在项目中用的就是系统预置的 flow layout,现在它很好并且很简单的去使用grid view样式。仍然在FlickrPhotosViewController.swift文件中,更新类的声明部分,声时类将实现flow layout协议。

1
2
3
4
class FlickrPhotosViewController:
UICollectionViewController,
UITextFieldDelegate,
UICollectionViewDelegateFlowLayout {

和下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// MARK: UICollectionViewDelegateFlowLayout
//1
func collectionView(collectionView: UICollectionView!,
layout collectionViewLayout: UICollectionViewLayout!,
sizeForItemAtIndexPath indexPath: NSIndexPath!) -> CGSize {
let flickrPhoto = photoForIndexPath(indexPath)
//2
if var size = flickrPhoto.thumbnail?.size {
size.width += 10
size.height += 10
return size
}
return CGSize(width: 100, height: 100)
}
//3
private let sectionInsets = UIEdgeInsets(top: 50.0, left: 20.0, bottom: 50.0, right: 20.0)
func collectionView(collectionView: UICollectionView!,
layout collectionViewLayout: UICollectionViewLayout!,
insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return sectionInsets
}
  1. `collectionView(_:layout:sizeForItemAtIndexPath:) 用来返回给定cell的大小。你首先要知道你要找的是哪一个FlickrPhoto。
  2. Here, optional binding is used to determine which size should be returned. The thumbnail property of FlickrPhoto is an optional, which means it may not exist. If that is the case, a default size is returned. If the thumbnail does exist, padding is added to the size to give a border to the image.
  3. 这里用来检测应该返回什么size,thumbnail属性在FlickrPhoto中是optional,这表示它有可能不存在,这种情况下,应返回一个默认值。如果该值存在,给
  4. collectionView(_:layout:insetForSectionAtIndex:) 返回cells, headers或footers之间的间距,我们用一个常量来存储这些值。

再次build & Run,执行一个搜索,等下,不同大小的黑块块!

Slightly less boring black cells

有了上面的这些,就可以在屏幕上显示真正的照片了。

自定义 UICollectionViewCells

UICollectionView有一个很好的地方,和table view一样,它可以很方便的使用Storyboard编辑器来进行设置。你可以拖拽一个collection view到View controller中,然后在右边的Storyboard编辑器中对layout进行设置!看下如何进行设置。

打开 Main.storyboard 然后选中collection view,在size inspector中设置cell的大小为200x200:

Setting a cell's size

注意:在Storyboard中对cell所设置的size不会对app起任何作用,因为实现delegate的方法会在运行时动态给每一个cell赋值的大小。

拖一个image view到cell上。它会自动的填充整个cell。还记得刚才在layout delegate中设置 border padding的方法吗?无论size是多大,我们都需要添加autolayout约束来让image view总是对齐到cell的边距。With the image view selected, open the pin menu and add constraints of 5 points all the way round. Untick Prefer margins relative.

Adding constraints to size the image view

UICollectionViewCell不允许自定义backgroud color。我们需要创建自定义子类,来方便的添加任何我们需要的内容。

打开File\New\File…,然后选择Cocoa Touch Class,给新的类起名叫作FlickrPhotoCell,并让他继承自UICollectionViewCell

打开Main.storyboard,并选中cell。在identity inspector中设置该cell的类为FlickrPhotoCell`:

Setting the identity of a custom cell

打开Assistant 编辑器,确认对应的是FlickrPhotoCell.swift,然后按住control并拖住image view到class文件中来创建一个新的outlet:

Adding an image view outlet

现在,有了一包含 image view的自定义cell的类。可以往里面添加一些照片了!打开FlickrPhotosViewController.swift,然后用下面代码替换collectionView(cellForItemAtIndexPath:):

1
2
3
4
5
6
7
8
9
10
11
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//1
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as FlickrPhotoCell
//2
let flickrPhoto = photoForIndexPath(indexPath)
cell.backgroundColor = UIColor.blackColor()
//3
cell.imageView.image = flickrPhoto.thumbnail
return cell
}

这和之前返回空cell的方法有一点不一样。

  1. 返回的cell类型变为了FlickrPhotoCell
  2. 需要使用之前写的方法来获取FlickrPhoto从而显示图片
  3. 在cell使用缩略图来显示

再次运行,尝试一个Search,现在,终于可以看到我们要搜索的关键词图片了!

Project showing actual photos

Yes! Success! Notice that each photo fits perfectly inside its cell, with the cell echoing the photo’s dimensions. The credit is due to the work you did inside of sizeForItemAtIndexPath to tell the cell size to be the size of the photo plus 10 points, as well as the Auto Layout settings you modified.
完美,请注意这里每张图片都在它对应的cell中正确显示,cell也刚好是图片的大小。这是因为我们在sizeForItemAtIndexPath方法中

注意:如果你最终的结果和示例中不一致,有可能是你的Autolayout设置不正确。如果你找不出问题,试着和示例项目进行对比。

Where To Go From Here?

这章到些结束!在第二部分中,我们会学到:

  • 如何为collection view添加自定义header
  • 如何实现点击Cell显示详细页
  • 如果实现多行选中