UICollectionView教程:开始

原文:UICollectionView Tutorial: Getting Started
作者:Bradley Johnson
译者:kmyhy

注:本文由 Bardley Johnson 升级至 Swift 和 iOS 9,原文作者是 Brandon Trebitowski。

iOS 的照片程序采用时髦的多布局方式显示照片。你可以用网格方式浏览照片:

还可以通过“相册”的方式浏览照片:

你还可以通过捏合手势来切换两种布局。你可能会想:“我也想在我的 App 中使用这个!”

通过 UICollectionView 你可以很容易实现自定义布局和布局转换动画(就如照片程序一样)。

不仅仅是网格布局或栈式布局,因为 Collection View 是非常可定制化的。你可以用它们实现环形布局,封面流布局,Pulse news style 布局——只要你能想到的都能做到。

幸好,只要你对 UITableView 不陌生,那么 Collection View 对你来说都不会成为问题——它们的用法和 Table View 的数据源/委托模式差不多。

在本文,你会通过创建自己的网格式照片浏览 App 来学习 UICollectionView。当你学完本教程,你会学会如何使用基本的 Collection View,并可以在自己的 App 中使用这些神奇的技术!

剖析 UICollectionView

我们来看一下最终完成的项目。UICollectionView 有几个构建组件,如下所示:

它们分别是:

1、UICollectionView – 用于显示主要内容的 UIView。和 UITableView 一样,Collection View 也是 UIScrollView 子类。
2、UICollectionViewCell – 和 TableView 中的 UITableViewCell 一样。这些 cell 构成了视图的内容,并以 Collection View 的 subview 形式存在。cell 可以编码创建,也可以用 IB 创建。
3、Supplementary Views – 如果你需要显示一些不能在 cell 中展示、但仍然需要放在 Collection View 中显示的额外信息,你可以使用 Supplementary View。比如 header 或 footer。

注:Collection View 也可以有 Decoration View —— 如果你想通过一些特殊的 view 增强 Collection View 的外观(通常不会放真正有用的数据),你可以使用 Decoration View。比如背景图片或者其他可视化的装饰性组件。在本文中你不会用到 Decoration View,因为它需要我们编写自定义的布局类。

除此之外,Collection View 还有一个 layout 对象用于管理内容的大小、位置或其它属性。Layout 对象是一个 UICollectionViewLayout 子类。Layout 对象可以在运行时改变,Collection View 甚至为 Layout 之间的切换提供内置的动画。

你可以继承 UICollectionViewLayout 来创建自己的布局,苹果为开发者提供了一个基本的“流式”布局即 UICollectionViewFlowLayout。它将 cell 根据它们的大小一个接一个地排列,就像一个网格。你可以用这个现成的布局,或者对它进行扩展(子类化)实现自己的行为和可视化效果。

在本文和下一篇教程中,你讲深入学习这些元素。现在,请从一个实际项目开始!

FlickrSearch 简介

本文接下来的内容,将创建一个很酷的照片浏览程序,叫做 FlickrSearch。它允许你从著名的照片分享网站 Flickr 上按关键词搜索照片,并将搜索到的照片下载和显示到一个网格视图中,正如早先的截图中所示。

准备好了吗?打开 Xcode ,点开 File\New\Project… 菜单,选择 iOS\Application\Single View Application 模板。

这个模板会提供一个简单的 UIViewController 和 storyboard 故事板文件。这是一个很好的开头。

点击 Next 填写 App 信息。将 Product Name 设为 FlickrSearch, 设备类型选择 iPad ,语言设为 Swift。点击 Next 选择一个文件路径,然后点 Create。

View Controller 和故事板文件对你来说没有用处——我们要使用的是 UICollectionViewController。它和 UITableViewController一样,是一个特殊的 view controller 子类,拥有一个 collection view。将ViewController.swift 删除,然后将 Main.storyboard 中的 View controller 也删除。现在你的故事板是空的了。

打开 AppDelegate.swift,加一个常量用于存放一个阴影色,叫做 Wenderlich Green。在 import 语句下加入:

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

Wenderlich Green 作为整个 App 的主题色。然后修改 application(_:didFinishLaunchingWithOptions:) 方法:

func application(application: UIApplication!, didFinishLaunchingWithOptions
  launchOptions: [NSObject: AnyObject]?) -> Bool {

  window?.tintColor = themeColor
  return true
}

开始

打开 Main.storyboard ,拖一个 Collection View Controller。点击 Editor\Embed in\Navigation Controller 菜单,这将创建一个 navigation controller 并将 collection view controller 设置为 root View controller。

现在故事板应该是这个样子:

然后,选择 Navigation Controller ,将它设为 initial view controller:

接着,回到 collection view controller,选中 UICollectionView 背景色修改为白色:

注:知道 Scroll Direction 属性是干什么的吗?这个属性是 UICollectionViewFlowLayout 专有的,默认值是 Vertical。一个垂直的流式布局表示将 cell 按照从左到右排列如果排到最右边,则移到下一行。如果 cell 太多,用户可以通过垂直滚动来查看。
于此对应,水平流式布局将 cell 按照从上到下排列,直到排到最底部。用户可以用水平滚动的方式浏览。本文中使用垂直布局。

选择 collection view 中的 cell,将 Reuse Identifier 设为 FlickrCell。这和 Table View 中是类似的——就像数据源方法会用这个 ID 来重用或创建新的 cell。

拖一个 text field 到 collection view 上方的导航栏中心。这会用于给用户输入搜索关键字。将 Placeholder Text 设置为 Search ,Return Key 设为 Search:

然后,右键从 text field 拖一条线到 collection view controller 并选择 delegate outlet:

!{}(https://koenig-media.raywenderlich.com/uploads/2016/06/rw-cv7.png)

UICollectionViewController 是不错,但通常我们应该创建它的一个子类。点击 File\New\File…, 选择 iOS\Source\Cocoa Touch Class 并点 Next。类名命名为 FlickrPhotosViewController, 继承 UICollectionViewController。模板生成的文件中有许多模板代码,但理解这个类的最好方法还是从空白开始。打开 FlickrPhotosViewController.swift 修改内容为:

import UIKit

final class FlickrPhotosViewController: UICollectionViewController {

  // MARK: - Properties
  private let reuseIdentifier = "FlickrCell"
}

接着,定义一个常量用于 section 的 insets(后面我们会用到):

private let sectionInsets = UIEdgeInsets(top: 50.0, left: 20.0, bottom: 50.0, right: 20.0)

在本文接下来的内容中,你将填充剩余的代码。

回到 Main.storyboard, 选中 collection view controller ,在 Identity 面板中,将 Class 设置为 FlickrPhotosViewController:

从 Flickr 抓取照片

在这一节,你的第一个任务是快速念出本节标题十次。哈哈,开个玩笑。

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 一个并拷贝底部的 Url —— 将 “&api_key=” 和后面的 “&” 之间的内容拷贝到文本编辑器中待用。

例如,如果 URL 是:
http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=6593783 efea8e7f6dfc6b70bc03d2afb&format=rest&api_sig=f24f4e98063a9b8ecc8b522b238 d5e2f

则 API key 就是 6593783efea8e7f6dfc6b70bc03d2afb。

注:如果你使用了示例 API key,注意它每隔几天就会改变一次。因此如果你过几天来做这个例子,你会发现必须更新 API key。因此,如果你完成本文的例子需要几天时间的话,注册自己的 API key 会更轻松一点。

本文的目的是介绍 UICollectionView and 而非 Flickr API,所以有一些关于 Flickr 类已经为你创建好了,这样能减少你搜索 Flickr 时的代码。你可以从这里下载。

将 zip 文件解压缩并将解压缩后的文件拖到你的项目中,确保已勾选 Copy items into destination group’s folder(if needed),然后点击 Finish。

这些文件包含两个类和一个结构:

  • FlickrSearchResults: 一个用于封装关键字和搜索结果的结构。
  • FlickrPhoto: 用于保存从 Flickr 返回的照片数据——它的缩略图、完整图以及一些元数据比如 ID。还有一些方法,比如用于创建 Flick URL 和关于尺寸大小计算的方法。FlickrSearchResults 存储了一个包含这些对象的数组。
  • Flickr: 一个简单的闭包式的 API,用于执行搜索并返回一个 FlickrSearchResult 对象。

你可以看看它们的代码——代码都很简单,但可能会引起你在自己的 App 中使用 Flickr 的兴趣。

在你能够搜索 Flickr 之前,你需要一个 API key。打开 Flickr.swift ,将 apiKey 修改为你获取到的 API key。可能会是这样的:

let apiKey = "hh7ef5ce0a54b6f5b8fbc36865eb5b32"

接下来,进入下一节——在使用 Flickr 之前需要做一点小小准备。

准备数据结构

App 要实现这个效果,每次搜索完之后,都会将搜索结果显示在一个新的 section 里(而不是直接替换掉原来的 section)。也就是说,如果你先搜索了“忍者”,有搜索了“海盗”,则 Collection View 中会同时显示一个“忍者”的 section 和一个“海盗”的 section。来一个灾难的列表怎么样?

要实现这个效果,你需要用一个数据结构将每个 section 所需的数据单独存储。这个可以用一个 FlickrSearchResults 数组来代替。

打开 FlickrPhotosViewController.swift 在 sectonInsets 常量声明下加入:

private var searches = [FlickrSearchResults]()
private let flickr = Flickr()

searches 数组用于保存所有的搜索结果,flickr 用于引用我们要用来进行搜索的对象。然后在文件最后增加一个私有扩展:

// MARK: - Private
private extension FlickrPhotosViewController {
  func photoForIndexPath(indexPath: NSIndexPath) -> FlickrPhoto {
    return searches[indexPath.section].searchResults[indexPath.row]
  }
}

photoForIndexPath 是一个实用方法,根据 Collection View 中的某个的 Index Path 返回对应的 FlickrPhoto 对象。你用 Index Path 来访问照片的地方非常多,这样你就不用重复写同样的代码了。

获取结果

现在开始搜索 Flickr!当用户输入后点击 Search,我们就开始搜索。在 Collectoin View Controller 中,我们已经将 TextField 的 delegate 设置为 self,我们可以利用它来做些事情了。

打开 FlickrPhotosViewController.swift ,添加一个 text field delegate 的扩展:

extension FlickrPhotosViewController : 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


      activityIndicator.removeFromSuperview()


      if let error = error {
        // 2
        print("Error searching : \(error)")
        return
      }

      if let results = results {
        // 3
        print("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 类(我提供的用于搜索 Flickr 上的照片)异步搜索给定的关键字。当搜索完成,完成块被调用,并传入一个由 FlickrPhoto 对象构成的结果集,以及一个 NSError 对象(如果有的话)。
  2. 将错误打印到控制台。当然,在真正的 App 中你应当将错误显示给用户。
  3. 打印结果集并将其插入到 searches 数组的前列。
  4. 到这里,你收到新数据,需要刷新 UI。你可以调用 reloadData() 方法,这个方法的作用就如同 TableView 的同名方法。

运行 App。在文本框中进行一个搜索,你会在控制台看到输出的日志,描述结果集的行数:

Found 20 matching bananas

注意 Flickr 类将结果集限制在 20 条数据,以保证加载时间不会太长。

不幸的是,你在 Collectoin View 中看不到图片!正如 TableView,Collection View 需要你实现一系列数据源方法和委托方法。

填充 UICollectionView

你可能想到了,在使用 Table View 时必须设置它的 dataSource 和 delegate 才能为它提供显示数据和处理事件(选中行事件)。

类似地,在使用 Collection View 时也必须设置它的数据源和委托属性。它们的职责分别是:

  • 数据源(UICollectionViewDataSource)负责提供要显示的 item 的个数和视图。
  • 委托(UICollectionViewDelegate)负责处理事件,比如 cell 被选择、加亮或删除等。

UICollectionViewFlowLayout 也有一个委托协议—— UICollectionViewDelegateFlowLayout。它允许你对布局行为进行调整,对某些对象进行配置,比如 cell 间距、滚动方向等等。

在本文,你会实现必要的 UICollectionViewDataSource 和 UICollectionViewDelegateFlowLayout 方法,以使你的 Collection View 能够正常运行。UICollectionViewDelegate 方法在这里不需要,你将在下一教程中使用它。

UICollectionViewDataSource

打开 FlickrPhotosViewController.swift, 添加一个针对 UICollectionViewDataSource 协议的扩展:

// MARK: - UICollectionViewDataSource
extension FlickrPhotosViewController {
  //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)
    cell.backgroundColor = .blackColor()
    // Configure the cell
    return cell
  }
}

这是较为简单的几个方法:

  1. 一个 section 代表一个搜索,因此 section 的数目就是 searches 数组中的元素个数。
  2. 每个 section 中的 item 个数,等于对应 FlickrSearch 对象中 searchResults 数组的元素个数。
  3. 一个暂时的实现,仅仅返回空白的 cell——后面我们会用数据填充 cell。注意 Collectoin View 需要你为 cell 注册一个重用 ID,否则运行时会出现错误。

运行 app,执行搜索。你会看到多出 20 个 cell,可能看起来会有点奇怪:

UICollectionViewFlowLayoutDelegate

前面说过,每个 Collection View 都有一个与之关联的 layout。你现在用在项目中的 layout 就是系统内置的流式布局,这种布局简单、易用,提供了一个网格式的布局。

回到 FlickrPhotosViewController.swift, 在 flickr 常量下面声明:

private let itemsPerRow: CGFloat = 3

然后,增加一个对 UICollectionViewDelegateFlowLayout 的扩展,以使你的 view controller 采用这个流式布局委托协议:

extension FlickrPhotosViewController : UICollectionViewDelegateFlowLayout {
  //1
  func collectionView(collectionView: UICollectionView,
                      layout collectionViewLayout: UICollectionViewLayout,
                             sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
    //2
    let paddingSpace = sectionInsets.left * (itemsPerRow + 1)
    let availableWidth = view.frame.width - paddingSpace
    let widthPerItem = availableWidth / itemsPerRow

    return CGSize(width: widthPerItem, height: widthPerItem)
  }

  //3
  func collectionView(collectionView: UICollectionView,
                      layout collectionViewLayout: UICollectionViewLayout,
                             insetForSectionAtIndex section: Int) -> UIEdgeInsets {
    return sectionInsets
  }

  // 4
  func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
    return sectionInsets.left
  }
}
  1. collectionView(_:layout:sizeForItemAtIndexPath:) 方法负责告诉布局引擎每个 cell 应该是多大尺寸。
  2. 计算所有 padding 加起来有多少。即 n+1 乘以每个 cell 所留下的 padding,其中 n 为每行的 cell 个数。cell 间距采用 section inset 的左边边距。将视图的宽减去所有 padding 后再除以每行 cell 数即得到每个 cell 或 item 的宽。然后以这个宽和相等的高作为 item 的 size 返回(也就是一个正方形)。
  3. collectionView(_:layout:insetForSectionAtIndex:) 方法负责返回 cell 之间的间距,cell 和 header/footer 之间的间距。这个值保存在一个常量中。
  4. 这个方法布局中每行之间的间距,我们设为和左右 Padding 相同。

运行程序,进行一次搜索。看!比原来更大的黑色方块!

一切就绪,现在让我们来显示照片吧!

创建自定义 UICollectionViewCells

UICollectionView 中最精彩的一点就是,它和 Table View 一样,通过故事板能够用可视化的方式很方便地配置 CollectionView。你可以拖一个 UICollctionView 到你的 View Controller 中,在故事板编辑器中设计你的 cell。让我们来看看怎么做。

打开 Main.storyboard 选择 collection view。在 Size 面板中将 cell 大小修改为 200x200,让它的空间更大一点。

注:修改 cell 的 size 并不会影响 App 中实际 cell 的大小,因为我们会通过委托方法来配置 cell 的大小,它会覆盖你在故事板中的设置。

拖一个 Image View 到 cell,拖拉其大小占据整个 cell。在保持 Image View 被选中的状态下,打开 Pin 菜单,清除 Constrain to margins 选项,将 4 个方向的间距设置为 0:

选中状态下,将 Image View 的 Mode 设置为 Aspect Fit,这样图片无论哪个方向都不会被剪切和拉伸:

UICollectionViewCell 不允许进行任何定制,除了改变它的背景色外。你只能通过子类化进行定制,并方便地访问所有添加在上面的 subview。

打开 File\New\File… 菜单,选择 iOS\Source\Cocoa Touch Class 模板,点击 Next,类名命名为 FlickrPhotoCell, 并继承 UICollectionViewCell。

打开 Main.storyboard 选择 cell。在 Identity 面板,将 cell 的 Class 设为 FlickrPhotoCell:

打开助手编辑器,源代码窗口切换到 FlickrPhotoCell.swift ,右键从 Image View 拖一条线到源文件,创建一个 IBOutlet,命名为 imageView:

现在你创建了一个带有一个 ImageView 的自定义 cell。让我们来将照片显示在上面!打开 FlickrPhotosViewController.swift 将 collectionView(_:cellForItemAtIndexPath:) 方法替换为:

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
  //1
  let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier,
               forIndexPath: indexPath) as! FlickrPhotoCell
  //2
  let flickrPhoto = photoForIndexPath(indexPath)
  //3
  cell.imageView.image = flickrPhoto.thumbnail

  return cell
}

这个方法和之前的占位方法有点不同了。

  1. cell 现在变成了一个 FlickrPhotoCell 对象。
  2. 你需要获得存储有要显示的照片的 FlickrPhoto 对象,你可以调用先前定义的实用方法来获得。
  3. 将缩略图传给 Image View。

运行程序,进行一次搜索,你可以看到你所找到的图片了!

耶!我们成功了!

注:如果你看到的结果与此不同,或者图片看起来有点奇怪,很可能是你的自动布局配置不正确。如果遇到坎,你可以对项目中的设置进行比较。

到此,你的所有工作就完成了(非常酷)—— 给自己一个表扬吧!这是最终完成的项目

结尾

但还不够!继续阅读第二部分的教程,你会学习到:

  • 如何添加自定义的 section header
  • 如何通过拖放来移动 cell
  • 如何在选中一个 cell 时打开详情页
  • 如何选择多个 cell

同时,如果在学习中有任何问题或建议,请留言!

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页