版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.01.10 星期四 |
前言
iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
开始
首先看下写作环境
Swift 4.2, iOS 12, Xcode 10
在本教程中,您将升级您的UICollectionView技能,学习如何为section headers
实现可重用的视图,选择单元格,根据选择更新布局,以及通过拖放(drag and drop)
重新排序。
Adding Section Headers
该应用当前显示每个搜索执行的新部分。 您将使用搜索文本作为节标题添加新的节标题(section header)
。 要显示此节标题,您将使用UICollectionReusableView
。
此类与UICollectionViewCell
类似,但它通常显示headers and footers
。 您将直接将此元素添加到故事板并创建子类,以便它可以动态更新其标题。
准备开始了吗?
创建一个新文件以表示标头的自定义类。 选择File ▸ New ▸ File
。 选择Cocoa Touch Subclass
,确保它是UICollectionReusableView
的子类,并将其命名为FlickrPhotoHeaderView
。 单击Next
和Create
以添加文件。
接下来,打开Main.storyboard
并在FlickrPhotosViewController
场景中选择集合视图。 选择Attributes inspector
,然后选中Accessories
部分中的Section Header
复选框。 请注意,故事板中会出现UICollectionReusableView
。
选择可重复使用的视图,打开Size inspector
并将高度设置为90。返回Attributes inspector
,将Reuse Identifier
设置为FlickrPhotoHeaderView
。 将Background
颜色设置为Group Table View Background Color
,使其与集合视图的其余部分具有良好的偏移。 打开Identity
检查器并将Custom Class
设置为FlickrPhotoHeaderView
。
现在,使用组合键Command-Shift-L
打开对象库,并将标签拖到可重用视图上。 使用guides将其居中。 将字体更新为System 32
并使用故事板窗口底部的Auto Layout align
按钮将其固定到视图的水平和垂直中心。
1. Connecting the Section Header to Data
在仍然选择可重用视图的情况下,打开Assistant editor
,确保打开FlickrPhotoHeaderView.swift
。 如果打开错误的文件,请手动打开FlickrPhotoHeaderView.swift
。 从header view中按住Control键将label拖动到文件,并命名outlet label
。
class FlickrPhotoHeaderView: UICollectionReusableView {
@IBOutlet weak var label: UILabel!
}
最后,实现一种新的数据源方法来连接所有内容。 打开FlickrPhotosViewController.swift
并在UICollectionViewDataSource
扩展中添加以下方法:
override func collectionView(_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView {
// 1
switch kind {
// 2
case UICollectionView.elementKindSectionHeader:
// 3
guard
let headerView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: "\(FlickrPhotoHeaderView.self)",
for: indexPath) as? FlickrPhotoHeaderView
else {
fatalError("Invalid view type")
}
let searchTerm = searches[indexPath.section].searchTerm
headerView.label.text = searchTerm
return headerView
default:
// 4
assert(false, "Invalid element type")
}
}
此方法类似于collectionView(_:cellForItemAt :)
,但它返回UICollectionReusableView
而不是UICollectionViewCell
。
以下是此方法的细分:
- 1) 使用提供给委托方法的
kind
,确保您收到正确的元素类型。 - 2)
UICollectionViewFlowLayout
为您提供UICollectionView.elementKindSectionHeader
。 通过检查前面步骤中的标题框,您告诉流布局开始提供标题。 如果您没有使用流布局,则不会自由获得此行为。 - 3) 使用故事板标识符将标题出列,并在标题label上设置文本。
- 4) 在此处放置一个断言以确保这是正确的响应类型。
这是一个建立和运行的好地方。 您将为每个搜索获得一个漂亮的section header
,并且布局在所有设备类型的旋转方案中都能很好地适应:
Interacting With Cells
在本节中,您将学习与单元格交互的一些不同方法。 首先,您将学习选择单个单元格并更改集合视图布局以更大的尺寸显示所选单元格。 然后,您将学习如何选择多个单元格来共享图像。 最后,您将使用iOS 11中引入的拖放系统通过将单元格拖动到新的位置来重新排序单元格。
1. Selecting a Single Cell
UICollectionView
可以为布局更改设置动画。 在本节中,您将选择一个单元格,并使布局为响应中的更改设置动画。
首先,将以下计算属性添加到FlickrPhotosViewController
的顶部。 这会跟踪当前选定的单元格:
// 1
var largePhotoIndexPath: IndexPath? {
didSet {
// 2
var indexPaths: [IndexPath] = []
if let largePhotoIndexPath = largePhotoIndexPath {
indexPaths.append(largePhotoIndexPath)
}
if let oldValue = oldValue {
indexPaths.append(oldValue)
}
// 3
collectionView.performBatchUpdates({
self.collectionView.reloadItems(at: indexPaths)
}) { _ in
// 4
if let largePhotoIndexPath = self.largePhotoIndexPath {
self.collectionView.scrollToItem(at: largePhotoIndexPath,
at: .centeredVertically,
animated: true)
}
}
}
}
这是一个解释:
- 1)
largePhotoIndexPath
是一个Optional
项,用于保存当前选定的照片项目。 - 2) 此属性更改时,您还必须更新
collection view
。didSet
是一种管理它的简单方法。 如果用户之前选择了不同的单元格或者第二次点击同一单元格以取消选择,则可能需要重新加载两个单元格。 - 3)
performBatchUpdates(_:completion :)
将动画对集合视图的更改。 - 4) 动画完成后,将所选单元格滚动到屏幕中间。
点击单元格将使集合视图选中它。 您希望借此机会设置largePhotoIndexPath
属性,但您不希望实际选择它。 当您实施多个选择时,选择它可能会导致问题。 UICollectionViewDelegate
在这里帮助你。
在文件底部的新扩展中,实现以下方法,该方法告诉collection view
是否应该选择特定单元格:
// MARK: - UICollectionViewDelegate
extension FlickrPhotosViewController {
override func collectionView(_ collectionView: UICollectionView,
shouldSelectItemAt indexPath: IndexPath) -> Bool {
if largePhotoIndexPath == indexPath {
largePhotoIndexPath = nil
} else {
largePhotoIndexPath = indexPath
}
return false
}
}
这种方法非常简单。 如果已选择用户点击的单元格的IndexPath
,请将largePhotoIndexPath
设置为nil
。 否则,将其设置为indexPath
的当前值。 这将触发刚刚实现的didSet
属性观察者。
要更新用户刚刚点击的单元格的大小,您需要修改collectionView(_:layout:sizeForItemAt :)
。 将以下代码添加到方法的开头:
if indexPath == largePhotoIndexPath {
let flickrPhoto = photo(for: indexPath)
var size = collectionView.bounds.size
size.height -= (sectionInsets.top + sectionInsets.bottom)
size.width -= (sectionInsets.left + sectionInsets.right)
return flickrPhoto.sizeToFillWidth(of: size)
}
此逻辑计算单元格的大小,以尽可能多地填充集合视图,同时保持单元格的纵横比。 由于您增加了单元格的大小,因此需要更大的图像才能使其看起来更好。 这需要根据请求下载较大的图像。
2. Providing Selection Feedback
接下来,按照步骤下载图像时添加一些显示活动的UI反馈。
打开Main.storyboard
并打开对象库。 将活动指示器(activity indicator)
拖到集合视图单元格中的图像视图上。 打开“属性”检查器,将样式设置为Large White
,然后选中Hides When Stopped
按钮。 使用布局指南,将指示器拖动到ImageView
的中心,然后使用Align
菜单设置约束以使指示器水平和垂直居中。
打开Assistant editor
,确保FlickrPhotoCell.swift
已打开,然后按住Control
键从活动指示器拖动到FlickrPhotoCell
,命名outlet activityIndicator
:
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
在FlickrPhotoCell.swift
中,添加以下内容让单元格控制边框:
override var isSelected: Bool {
didSet {
imageView.layer.borderWidth = isSelected ? 10 : 0
}
}
override func awakeFromNib() {
super.awakeFromNib()
imageView.layer.borderColor = themeColor.cgColor
isSelected = false
}
3. Loading the Large Image
现在打开FlickrPhotosViewController.swift
并在私有扩展中添加一个便利的方法来下载Flickr
图像的大版本:
func performLargeImageFetch(for indexPath: IndexPath, flickrPhoto: FlickrPhoto) {
// 1
guard let cell = collectionView.cellForItem(at: indexPath) as? FlickrPhotoCell else {
return
}
// 2
cell.activityIndicator.startAnimating()
// 3
flickrPhoto.loadLargeImage { [weak self] result in
// 4
guard let self = self else {
return
}
// 5
switch result {
// 6
case .results(let photo):
if indexPath == self.largePhotoIndexPath {
cell.imageView.image = photo.largeImage
}
case .error(_):
return
}
}
}
以下是上述代码的细分:
- 1) 确保您已正确输入正确类型的单元格。
- 2) 启动活动指示器以显示网络活动。
- 3) 使用
FlickrPhoto
上的便捷方法开始图像下载。 - 4) 由于您处于捕获self的闭包中,因此请确保视图控制器仍然是有效对象。
- 5) 打开结果类型。 如果成功,并且如果
indexPath
执行fetch
与当前的largePhotoIndexPath
匹配,则将单元格上的imageView
设置为照片的largeImage
。
最后,在FlickrPhotosViewController
中,将collectionView(_:cellForItemAt :)
替换为以下内容:
override func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: reuseIdentifier,
for: indexPath) as? FlickrPhotoCell else {
preconditionFailure("Invalid cell type")
}
let flickrPhoto = photo(for: indexPath)
// 1
cell.activityIndicator.stopAnimating()
// 2
guard indexPath == largePhotoIndexPath else {
cell.imageView.image = flickrPhoto.thumbnail
return cell
}
// 3
guard flickrPhoto.largeImage == nil else {
cell.imageView.image = flickrPhoto.largeImage
return cell
}
// 4
cell.imageView.image = flickrPhoto.thumbnail
// 5
performLargeImageFetch(for: indexPath, flickrPhoto: flickrPhoto)
return cell
}
以下是对上述工作的解释:
- 1) 如果活动指示器当前处于活动状态,请将其停止
- 2) 如果
largePhotoIndexPath
与当前单元格的indexPath
不匹配,请将图像设置为缩略图并返回。 - 3) 如果
largePhotoIndexPath
不是nil
,请将图像设置为大图像并返回。 - 4) 此时,这是需要大图像显示的单元。 首先设置缩略图。
- 5) 调用上面创建的私有便利方法来获取大图像版本并返回单元格。
现在是构建和运行检查工作的好时机。 执行搜索,然后选择图像单元格。 你会看到它向上扩展并动画到屏幕的中心。 再次点击它可以将其恢复到原始状态。
接下来,您将实现多个选择和共享。
4. Multiple Selection
在UICollectionView
中选择单元格的过程与UITableView
非常相似。 一定要让UICollectionView
知道允许通过设置属性进行多项选择。
共享流程遵循以下步骤:
- 1) 用户点击共享栏按钮以调用多选模式。
- 2) 然后,用户根据需要点击尽可能多的照片,将每个照片添加到阵列中。
- 3) 用户再次点击共享按钮,打开本机共享表。
- 4) 共享操作完成或取消时,单元格取消选择,集合视图返回单选模式。
首先,将支持此新功能所需的属性添加到FlickrPhotosViewController
的顶部:
private var selectedPhotos: [FlickrPhoto] = []
private let shareLabel = UILabel()
selectedPhotos
在共享模式下跟踪所有当前选定的照片。 shareTextLabel
为用户提供有关当前选择的照片数量的反馈。
接下来,将以下方法添加到私有扩展:
func updateSharedPhotoCountLabel() {
if sharing {
shareLabel.text = "\(selectedPhotos.count) photos selected"
} else {
shareLabel.text = ""
}
shareLabel.textColor = themeColor
UIView.animate(withDuration: 0.3) {
self.shareLabel.sizeToFit()
}
}
您将调用此方法以使shareLabel
文本保持最新。 此方法检查共享属性。 如果应用当前正在共享照片,则会正确设置标签文本并为尺寸更改设置动画以适应导航栏中的所有其他元素。
5. Keeping Track of Sharing
接下来,在largePhotoIndexPath
下面添加以下属性:
var sharing: Bool = false {
didSet {
// 1
collectionView.allowsMultipleSelection = sharing
// 2
collectionView.selectItem(at: nil, animated: true, scrollPosition: [])
selectedPhotos.removeAll()
guard let shareButton = self.navigationItem.rightBarButtonItems?.first else {
return
}
// 3
guard sharing else {
navigationItem.setRightBarButton(shareButton, animated: true)
return
}
// 4
if largePhotoIndexPath != nil {
largePhotoIndexPath = nil
}
// 5
updateSharedPhotoCountLabel()
// 6
let sharingItem = UIBarButtonItem(customView: shareLabel)
let items: [UIBarButtonItem] = [
shareButton,
sharingItem
]
navigationItem.setRightBarButtonItems(items, animated: true)
}
}
shared
是一个Bool
,其属性观察器类似于largePhotoIndexPath
。 当此视图控制器进入和离开共享模式时,它负责跟踪和更新。
设置属性后,属性观察者如何响应:
- 1) 将集合视图的
allowsMultipleSelection
属性设置为sharing
属性的值。 - 2) 取消选择所有单元格,滚动到顶部并从阵列中删除任何现有共享项目。
- 3) 如果未启用共享,请将共享栏按钮设置为默认状态并返回。
- 4) 确保
largePhotoIndexPath
设置为nil
。 - 5) 调用上面创建的便捷方法来更新共享标签。
- 6) 相应地更新栏按钮项。
6. Adding a Share Button
打开Main.storyboard
并将一个条形按钮项拖到导航栏中搜索栏右侧的FlickrPhotosViewController
上。 在“属性”检查器中将System Item
设置为Action
,为其指定hare图标。 打开助手编辑器,确保选中FlickrPhotosViewController.swift
,然后按住Ctrl键从共享按钮拖动到View控制器。 创建一个名为share
的Action
,并将Type
设置为UIBarButtonItem
。
填写方法如下:
guard !searches.isEmpty else {
return
}
guard !selectedPhotos.isEmpty else {
sharing.toggle()
return
}
guard sharing else {
return
}
// TODO: Add photo sharing logic!
目前,此方法将确保用户执行了一些搜索并且sharing
属性为true
。 现在,打开选择单元格的功能。 将以下代码添加到collectionView(_:shouldSelectItemAt:)
的顶部:
guard !sharing else {
return true
}
这将允许在用户处于共享模式时进行选择。
接下来,将以下方法添加到collectionView(_:shouldSelectItemAt :)
下面的UICollectionViewDelegate
扩展中:
override func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {
guard sharing else {
return
}
let flickrPhoto = photo(for: indexPath)
selectedPhotos.append(flickrPhoto)
updateSharedPhotoCountLabel()
}
此方法允许选择照片并将照片添加到sharedPhotos
数组并更新shareLabel
文本。
接下来,仍然在UICollectionViewDelegate
扩展中,实现以下方法:
override func collectionView(_ collectionView: UICollectionView,
didDeselectItemAt indexPath: IndexPath) {
guard sharing else {
return
}
let flickrPhoto = photo(for: indexPath)
if let index = selectedPhotos.firstIndex(of: flickrPhoto) {
selectedPhotos.remove(at: index)
updateSharedPhotoCountLabel()
}
}
此方法从selectedPhotos
数组中删除项目,并在点击选定的单元格以取消选择时更新label
。
最后,回到share(_:)
中的// TODO
注释,并将其替换为共享表的实现:
let images: [UIImage] = selectedPhotos.compactMap { photo in
if let thumbnail = photo.thumbnail {
return thumbnail
}
return nil
}
guard !images.isEmpty else {
return
}
let shareController = UIActivityViewController(
activityItems: images,
applicationActivities: nil)
shareController.completionWithItemsHandler = { _, _, _, _ in
self.sharing = false
self.selectedPhotos.removeAll()
self.updateSharedPhotoCountLabel()
}
shareController.popoverPresentationController?.barButtonItem = sender
shareController.popoverPresentationController?.permittedArrowDirections = .any
present(shareController, animated: true, completion: nil)
此方法将在selectedPhotos
数组中找到所有FlickrPhoto
对象,确保它们的缩略图不是nil
并将它们传递给UIActivityController
进行演示。 iOS处理呈现可处理图像列表的任何系统应用程序或服务的工作。
再一次,检查你的工作!
构建并运行,执行搜索,然后点击导航栏中的共享按钮。 选择多个图像,并在选择新单元格时实时查看标签更新:
再次点击分享按钮,将显示本地共享表。 如果您使用的是设备,则可以选择接受要共享的图片的任何应用或服务:
Reordering Cells
在最后一段中,您将学习如何使用本机拖放功能对集合视图中的单元格重新排序。 iOS为UITableView
和UICollectionView
内置了一些不错的便利方法,它们利用了iOS 11中添加的拖放功能。
要使用拖放,您必须了解两个协议。 在集合视图中,UICollectionViewDragDelegate
驱动拖动交互,UICollectionViewDropDelegate
驱动放置交互。 您将首先实现拖动委托,测试行为,然后使用放置委托完成此功能。
1. Implementing Drag Interactions
在FlickrPhotosViewController.swift
的底部添加一个新扩展,以添加遵守UICollectionViewDragDelegate
:
// MARK: - UICollectionViewDragDelegate
extension FlickrPhotosViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView,
itemsForBeginning session: UIDragSession,
at indexPath: IndexPath) -> [UIDragItem] {
let flickrPhoto = photo(for: indexPath)
guard let thumbnail = flickrPhoto.thumbnail else {
return []
}
let item = NSItemProvider(object: thumbnail)
let dragItem = UIDragItem(itemProvider: item)
return [dragItem]
}
}
collectionView(_:itemsForBeginning:at :)
是此协议唯一required
的方法,它也是此功能所需的唯一方法。
在此方法中,您可以在缓存的数组中找到正确的照片,并确保它具有缩略图。 必须返回一个UIDragItem
对象数组。 UIDragItem
使用NSItemProvider
对象初始化。 您可以使用要提供的任何数据对象初始化NSItemProvider
。
接下来,您需要让集合视图知道它能够处理拖动交互。 在share(_ :)
上面实现viewDidLoad()
:
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dragInteractionEnabled = true
collectionView.dragDelegate = self
}
在此方法中,您让集合视图知道已启用拖动交互并将拖动委托设置为self
。
现在是构建和运行的好时机。 执行搜索,现在您将能够长按单元格以查看拖动交互。 你会看到它抬起来,你可以将它拖动,但你还是无法将其drop
。
接下来,是时候实现drop行为了。
2. Implementing Drop Interactions
现在,您需要从UICollectionViewDropDelegate
实现一些方法,以使集合视图能够接受拖动drag
会话中的dropped
项目。 这还允许您通过利用drop方法提供的索引路径对单元格重新排序。
在FlickrPhotosViewController.swift
结束时,创建另一个扩展以遵守UICollectionViewDropDelegate
:
// MARK: - UICollectionViewDropDelegate
extension FlickrPhotosViewController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView,
canHandle session: UIDropSession) -> Bool {
return true
}
}
通常在此方法中,您将检查建议的放置drop项并确定是否要接受它们。 由于您只在此应用中为一种项目类型启用drag-and-drop
,因此您只需在此处返回true
。
在实现下一个方法之前,返回到FlickrPhotosViewController
上的专用扩展,并实现两个便利的方法,这将有助于下一步的工作。
将这些方法放在photo(for:)
下面:
func removePhoto(at indexPath: IndexPath) {
searches[indexPath.section].searchResults.remove(at: indexPath.row)
}
func insertPhoto(_ flickrPhoto: FlickrPhoto, at indexPath: IndexPath) {
searches[indexPath.section].searchResults.insert(flickrPhoto, at: indexPath.row)
}
要使用更改来更新源数据阵列,您需要使其变为可变。 打开FlickrSearchResults.swift
并更新searchResults
,如下所示:
var searchResults: [FlickrPhoto]
3. Making the Drop
接下来,切换回FlickrPhotosViewController.swift
并在drop delegate
扩展中实现以下方法:
func collectionView(_ collectionView: UICollectionView,
performDropWith coordinator: UICollectionViewDropCoordinator) {
// 1
guard let destinationIndexPath = coordinator.destinationIndexPath else {
return
}
// 2
coordinator.items.forEach { dropItem in
guard let sourceIndexPath = dropItem.sourceIndexPath else {
return
}
// 3
collectionView.performBatchUpdates({
let image = photo(for: sourceIndexPath)
removePhoto(at: sourceIndexPath)
insertPhoto(image, at: destinationIndexPath)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
}, completion: { _ in
// 4
coordinator.drop(dropItem.dragItem,
toItemAt: destinationIndexPath)
})
}
}
此委托方法接受放置drop项并对集合视图和基础数据存储阵列执行维护,以正确重新排序已drop的项。 你会在这里看到一个新对象:UICollectionViewDropCoordinator
。 这是一个UIKit
对象,它为您提供有关建议的放置drop项的更多信息。
以下是详细情况:
- 1) 从
drop
协调器获取destinationIndexPath
。 - 2) 遍历
items
- 一个UICollectionViewDropItem
对象数组 - 并确保每个对象都有一个sourceIndexPath
。 - 3) 在集合视图上执行批量更新,从源索引处的数组中删除项目并将其插入目标索引。 完成后,对集合视图单元格执行删除和更新。
- 4) 完成后,执行drop操作。
返回viewDidLoad()
并让集合视图知道dropDelegate
:
collectionView.dropDelegate = self
好的,最后一个方法。 在drop delegate
扩展的底部实现以下方法:
func collectionView(
_ collectionView: UICollectionView,
dropSessionDidUpdate session: UIDropSession,
withDestinationIndexPath destinationIndexPath: IndexPath?)
-> UICollectionViewDropProposal {
return UICollectionViewDropProposal(
operation: .move,
intent: .insertAtDestinationIndexPath)
}
UIKit drop session
在交互期间不断调用此方法,以插入用户拖动项目的位置,并为其他对象提供对会话作出反应的机会。
此委托方法使集合视图使用UICollectionViewDropProposal
响应drop
会话,该UICollectionViewDropProposal
指示drop
将移动项目并将其插入目标索引路径。
好的,是时候构建和运行了。
您现在将在集合视图中看到拖动会话以及一些非常好的行为。 当您在屏幕上拖动项目时,其他项目会移动到移开状态,表示可以在该位置接受drop。 您甚至可以在不同的搜索部分之间重新排序项目!
后记
本篇主要讲述了UICollectionView的重用、选择和重排序,感兴趣的给个赞或者关注~~~