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

译:Beginning iOS Collection Views in Swift: Part 2/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。

上文中,学习到了如何使用UICollectionView来展示一个网格的图片显示App。

本章中,我们会继续探索和学习如何如何自定义一个headers。你可以接着上一部分的代码继续进行,也可以下载上一章节完整的代码,但你还是需要像上章所描述的那样去申请一个新的API key。

添加一个header

App中每一组搜索返回结果都有一个Section。如果能为每组结果集前都加一个header来说明这组照片的信息,对户名来说会非常友好。

你将通过继承UICollectionReusableView来创建一个新的header。这个类和collection view cell(事实上,cell就是继承这个类)很像,它但主要用于像header或footer的地方。

该View可以在stroyboard中添加然后连接到该自定义类中。首先创建该类文件,通过 File\New\File…,选择 iOS\Cocoa Touch\Objective-C class template, 点击Next。给这个类起名叫作FlickrPhotoHeaderView并继承自UICollectionReusableView,点击下一步完成创建。

打开MainStoryboard.storyboard,在左侧的Scene Inspector找到collection view。打开其Attributes Inspector 并在Accessories中选中Section Header

查看左侧的scene inspector,你会发现Collection Reusable View自动被加入了Collection View的下方。点击选中Collection Reusable View,就可以进行编辑了。

将鼠标移到View的下边缘然后向下拉,将View高设为90像素(或者在Size Inspector中通过设值来调整大小)。这样你就有大一点的空间来布局了。

拖一个label到header view中,然后居中放置。调整Font 为System 32.0大小,然后在对齐菜单中设置它水平且垂直居中,然后更新它的frame:

Aligning a label to the center of it's superview

选中header view,在indetity inspector中设置它的ClassFlickrPhotoHeaderView

打开Attributes Inspector设置Background为 90% Write,然后设置Identifier为FlickrPhotoHeaderView,该值在视图复用时会被用到。

打开Assistant editor,确保FlickrPhotoHeaderView.swift被打开,然后按住control并拖住label到class文件中来创建新的outlet,起名叫label:

1
2
3
class FlickrPhotoHeaderView: UICollectionReusableView {
@IBOutlet weak var label: UILabel!
}

这时,如果你build并run这个app的话,你会发现header并未出现(哪怕只是一个带着”Label”字样的空白View)。这是因为你需要实现另一个datasource 协议。打开FlickrPhotosViewController.swift,添加下面一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
override func collectionView(collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
//1
switch kind {
//2
case UICollectionElementKindSectionHeader:
//3
let headerView =
collectionView.dequeueReusableSupplementaryViewOfKind(kind,
withReuseIdentifier: "FlickrPhotoHeaderView",
forIndexPath: indexPath)
as FlickrPhotoHeaderView
headerView.label.text = searches[indexPath.section].searchTerm
return headerView
default:
//4
assert(false, "Unexpected element kind")
}
}

这段代码看起来和cellForItemAtIndexPath很相似,但它是给supplementary view所使用的。下面我们来详细解释下这段代码的意思:

  1. The kind parameter is supplied by the layout object and indicates which sort of supplementary view is being asked for.
  2. UICollectionElementKindSectionHeader is a supplementary view kind belonging to the flow layout. By checking that box in the storyboard to add a section header, you told the flow layout that it needs to start asking for these views. There is also a UICollectionElementKindSectionFooter, which you’re not currently using. If you don’t use the flow layout, you don’t get header and footer views for free like this.
  3. The header view is dequeued using the identifier added in the storyboard. This works just like cell dequeuing. The label’s text is then set to the relevant search term.
  4. An assert is placed here to make it clear to other developers (including future you!) that you’re not expecting to be asked for anything other than a header view.

现在已经可以真正的运行了。你会发现UI差不多已经完成了。如果你测试多次搜索,不同的结果集将会被新的header完美区分。作为额外收获,尝试转动设备 - 看看布局如何,包括headwe,是不是很完美 :]

Collection view showing section header

Of course, black and white cabbages are what you’d expect from this search?

Cell 交互

In this final section of the tutorial you will learn some ways to interact with collection view cells. You’ll take two different approaches. The first will display a larger version of the image. The second will demonstrate how to support multiple selection in order to share images.

在接下来的部分,你会学习到几种和collection view cell交互的方式。你将来实现两种效果,第一个是显示一张大图。第二种则是弹出共享图片的多种方式。

Single selection

Collection views can animate changes to their layout. Your first task is to show a larger version of a photo when it is tapped.

Collection view可以通过动画改变它的布局。你的第一个任务是当点击时显示一个大图。

首先,你需要添加一个属性来记录被点击的cell。打开FlickrPhotosViewController.swift,然后添加下面代码:

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
//1
var largePhotoIndexPath : NSIndexPath? {
didSet {
//2
var indexPaths = [NSIndexPath]()
if largePhotoIndexPath != nil {
indexPaths.append(largePhotoIndexPath!)
}
if oldValue != nil {
indexPaths.append(oldValue!)
}
//3
collectionView!.performBatchUpdates({
self.collectionView.reloadItemsAtIndexPaths(indexPaths)
}) {
completed in
//4
if self.largePhotoIndexPath != nil {
self.collectionView.scrollToItemAtIndexPath(
self.largePhotoIndexPath!,
atScrollPosition: .CenteredVertically,
animated: true)
}
}
}
}

分解下来看:

  1. largePhotoIndexPath is an optional that will hold the index path of the tapped photo, if there is one.
  2. Whenever this property gets updated, the collection view needs to be updated. a didSet property observer is the safest place to manage this. There may be two cells that need reloading, if the user has tapped one cell then another, or just one if the user has tapped the first cell, then tapped it again to shrink.
  3. performBatchUpdates will animate any changes to the collection view performed inside the block. You want it to reload the affected cells.
  4. Once the animated update has finished, it’s a nice touch to scroll the enlarged cell to the middle of the screen

“What enlarged cell?”, I hear you asking. You’ll get to that in a minute!
什么放大Cell, 你或许会这样问。过会就知道了!

点击一个cell会让collection view选中它。你想知道一个cell被选中,所以你能给它设置 largeIndexPath 属性,但你并不想真正的选中它,因为它会让你困惑当你做多个选择时。UICollectionViewDelegate。 Collection view问它的delegate是否要选择一个cell。 还在FlickrPhotosViewController.swift中,添加下面代码:

1
2
3
4
5
6
7
8
9
10
11
override func collectionView(collectionView: UICollectionView,
shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
if largePhotoIndexPath == indexPath {
largePhotoIndexPath = nil
}
else {
largePhotoIndexPath = indexPath
}
return false
}

这个方法非常简单。如果所点的cell已经是大图了,设置 largePhotoIndexPath 为 nil。否则设为index path。 它的didSet方法会被调用,然后重新加载受影响的Cell。

要实现点击显示大图,还需要修改sizeForItemAtIndexPath flow layout代理方法,替换为以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func collectionView(collectionView: UICollectionView!,
layout collectionViewLayout: UICollectionViewLayout!,
sizeForItemAtIndexPath indexPath: NSIndexPath!) -> CGSize {
let flickrPhoto = photoForIndexPath(indexPath)
// New code
if indexPath == largePhotoIndexPath {
var size = collectionView.bounds.size
size.height -= topLayoutGuide.length
size.height -= (sectionInsets.top + sectionInsets.right)
size.width -= (sectionInsets.left + sectionInsets.right)
return flickrPhoto.sizeToFillWidthOfSize(size)
}
// Previous code
if var size = flickrPhoto.thumbnail?.size {
size.width += 10
size.height += 10
return size
}
return CGSize(width: 100, height: 100)
}

You’ve added the code highlighted in the comments. This calculates the size of the cell to fill as much of the collection view as possible whilst maintaining its aspect ratio.

这里并不需要一个很大的cell,除非你的图很大。

打开Main.storyboard然后拖一个activity indicator到collection view cell的image view上。打开Attributes inspector中,设置Style为Large White然后选 中Hides When Stopped。使用Alignment Tool,将其水平和垂直居中。

Cell contents including activity indicator centred in the superview

打开assistant editor,按住control拖动activity indicator 到 FlickrPhotoCell.swift中创建新的outlet,起名为activityIndicator

1
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!

同时在该文件中,添加下面代码来让Cell控制它的背景颜色。接下来会用到:

1
2
3
4
5
6
7
8
9
10
override func awakeFromNib() {
super.awakeFromNib()
self.selected = false
}
override var selected : Bool {
didSet {
self.backgroundColor = selected ? themeColor : UIColor.blackColor()
}
}

最后,在FlickrPhotosCollectionViewController.swift中更新 cellForItemAtIndexPath实现代码:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(
reuseIdentifier, forIndexPath: indexPath) as FlickrPhotoCell
let flickrPhoto = photoForIndexPath(indexPath)
//1
cell.activityIndicator.stopAnimating()
//2
if indexPath != largePhotoIndexPath {
cell.imageView.image = flickrPhoto.thumbnail
return cell
}
//3
if flickrPhoto.largeImage != nil {
cell.imageView.image = flickrPhoto.largeImage
return cell
}
//4
cell.imageView.image = flickrPhoto.thumbnail
cell.activityIndicator.startAnimating()
//5
flickrPhoto.loadLargeImage {
loadedFlickrPhoto, error in
//6
cell.activityIndicator.stopAnimating()
//7
if error != nil {
return
}
if loadedFlickrPhoto.largeImage == nil {
return
}
//8
if indexPath == self.largePhotoIndexPath {
if let cell = collectionView.cellForItemAtIndexPath(indexPath) as? FlickrPhotoCell {
cell.imageView.image = loadedFlickrPhoto.largeImage
}
}
}
return cell
}

这段代码比较长,让我们一点点看:

  1. Always stop the activity spinner – you could be reusing a cell that was previously loading an image
  2. This part is as before – if you’re not looking at the large photo, just set the thumbnail and return
  3. If the large image is already loaded, set it and return
  4. By this point, you want the large image, but it doesn’t exist yet. Set the thumbnail image and start the spinner going. The thumbnail will stretch until the download is complete
  5. Ask for the large image from Flickr. This loads the image asynchronously and has a completion block
  6. The load has finished, so stop the spinner
  7. If there was an error or no photo was loaded, there’s not much you can do.
  8. Check that the large photo index path hasn’t changed while the download was happening, and retrieve whatever cell is currently in use for that index path (it may not be the original cell, since scrolling could have happened) and set the large image.

Build and run, perform a search and tap a nice-looking photo – it grows to fill the screen, and the other cells move around to make space!
再次运行,执行一次搜索,然后找一张好看的照片点击下 - 它会慢慢的填充整个屏幕,其它的Cell都向一边跑去。

Collection view with large image
You look bigger to him as well.

再次点击这个cell,或者移动下点击其它Cell,你不再需要写代码,coolection view和 layout 自动帮你完成了余下的工作!

Multiple selection

Your final task for this tutorial is to let the user select multiple photos and share them with a friend. The process for multi-selection on a collection view is very similar to that of a table view. The only trick is to tell the collection view to allow multiple selection.
本章最后的工作就是让用户选择多张照片,然后共享给好友。

操作起来的步骤如下:

  1. The user taps the Share button to tell the UICollectionView to allow multi- selection and set the sharing property to YES.
  2. The user taps multiple photos that they want to share, adding them to an array.
  3. The user taps the Share button again, which brings up the sharing interface.
  4. When the user finishes sharing the images or taps Cancel, the photos are deselected and the collection view goes back to single selection mode.

First, add the following code in FlickrPhotosViewController.swift:

1
2
3
4
5
6
7
8
private var selectedPhotos = [FlickrPhoto]()
private let shareTextLabel = UILabel()
func updateSharedPhotoCount() {
shareTextLabel.textColor = themeColor
shareTextLabel.text = "\(selectedPhotos.count) photos selected"
shareTextLabel.sizeToFit()
}

The selectedPhotos array will keep track of the photos the user has selected, and the shareTextLabel will provide feedback to the user on how many photos have been selected. You will call updateSharedPhotoCount to keep shareTextLabel up to date.

Next, (also in FlickrPhotosViewController.swift) create the property that will hold the sharing state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var sharing : Bool = false {
didSet {
collectionView.allowsMultipleSelection = sharing
collectionView.selectItemAtIndexPath(nil, animated: true, scrollPosition: .None)
selectedPhotos.removeAll(keepCapacity: false)
if sharing && largePhotoIndexPath != nil {
largePhotoIndexPath = nil
}
let shareButton =
self.navigationItem.rightBarButtonItems!.first as UIBarButtonItem
if sharing {
updateSharedPhotoCount()
let sharingDetailItem = UIBarButtonItem(customView: shareTextLabel)
navigationItem.setRightBarButtonItems([shareButton,sharingDetailItem], animated: true)
}
else {
navigationItem.setRightBarButtonItems([shareButton], animated: true)
}
}
}

sharing is a Bool with another property observer, similar to largePhotoIndexPath above. In this observer, you toggle the multiple selection status of the collection view, clear any existing selection, and empty the selected photos array. You also update the bar button items to include and update the label added above.

Open Main.storyboard and drag a UIBarButtonItem to the right of the navigation bar above the collection view controller. In the Attributes Inspector, set the Identifier to Action to give it the familiar sharing icon. Open the assistant editor, making sure FlickrPhotosViewController.swift is open, and control-drag from the bar button into the class to create a new action. Call the action share:
Fill in the action method as shown:

1
2
3
4
5
6
7
8
9
10
11
@IBAction func share(sender: AnyObject) {
if searches.isEmpty {
return
}
if !selectedPhotos.isEmpty {
// TODO
}
sharing = !sharing
}

At the moment, all this method does is toggle the sharing state, kicking off all the changes in the property observer method added earlier.

You actually want to allow the user to select cells now, so update shouldSelectItemAtIndexPath to take this into account. Add the following code to the top of the method:

1
2
3
if (sharing) {
return true
}

This will allow selection in sharing mode.

Implement the delegate method to add selected photos to the shared photos array and update the label:

1
2
3
4
5
6
7
8
override func collectionView(collectionView: UICollectionView,
didSelectItemAtIndexPath indexPath: NSIndexPath) {
if sharing {
let photo = photoForIndexPath(indexPath)
selectedPhotos.append(photo)
updateSharedPhotoCount()
}
}

And remove them when the cell is deselected (tapped again):

1
2
3
4
5
6
7
8
9
override func collectionView(collectionView: UICollectionView!,
didDeselectItemAtIndexPath indexPath: NSIndexPath!) {
if sharing {
if let foundIndex = find(selectedPhotos, photoForIndexPath(indexPath)) {
selectedPhotos.removeAtIndex(foundIndex)
updateSharedPhotoCount()
}
}
}

Build and run, and perform a search. Tap the share button to go into sharing mode and select different photos. The label will update and the selected cells will get a fetching Wenderlich Green border.

Multiple cells selected in a collection view

I don’t know what I was expecting this search to show up, but it wasn’t this
If you tap the share button again, everything just gets deselected, and you go back into non-sharing mode, where tapping a single photo enlarges it.
Of course, this share button isn’t terribly useful unless there’s actually a way to share the photos! Replace the TODO comment in your share method with the following code:

1
2
3
4
5
6
7
8
9
10
var imageArray = [UIImage]()
for photo in self.selectedPhotos {
imageArray.append(photo.thumbnail!);
}
let shareScreen = UIActivityViewController(activityItems: imageArray, applicationActivities: nil)
let popover = UIPopoverController(contentViewController: shareScreen)
popover.presentPopoverFromBarButtonItem(self.navigationItem.rightBarButtonItems!.first as UIBarButtonItem,
permittedArrowDirections: UIPopoverArrowDirection.Any, animated: true)

First, this code creates an array of UIImage objects from the FlickrPhoto‘s thumbnails. The UIImage array is much more convenient, as we can simply pass it to a UIActivityViewController. The UIActivityViewController will show the user any image sharing services or actions available on the device: iMessage, Mail, Print, etc. You simply present your UIActivityViewController from within a popover (because this is an iPad app), and let the user take care of the rest!

Build and run, enter sharing mode, select some photos and hit the share button again. Your share dialog will appear!

Share Screen

Note: Testing exclusively on a simulator? You will find the simulator has far fewer sharing options than on device. If you are having trouble confirming that your share screen is properly sharing your images, try the Save (x) Images option. Whether on device or on simulator, this will save the selected images to Photos app, where you can review them to ensure everything worked.

Where To Go From Here?

Here is the complete project that you developed in the tutorial series.
Congratulations, you have finished creating your very own stylish Flickr photo browser, complete with a cool UICollectionView based grid view!
In the process, you learned how to make custom UICollectionViewCells, create headers with UICollectionReusableView, detect when rows are tapped, implement multi-cell selection, and much more!
If you have any questions or comments about UICollectionViews or this tutorial, please join the forum discussion below!