Swift - 相册图片多选功能的实现
作者:hangge | 2017-01-18 08:10
使用 UIImagePickerController,我们可以很方便的从系统相册中选择照片。但 UIImagePickerController 每次只能选择一张图片,不支持多选。这样如果我们需要一次上传多张图片到服务器,使用 UIImagePickerController 效率就会很低。
本文演示如何实现一个多选组件,类似微信发朋友圈那样,可以一次打勾选择多张照片。
一、组件介绍
1,实现原理
(1)我们通过 Photos 框架(PhotoKit)读取出所有图片数据,并使用自定义的 collection view 将缩略图显示出来,同时实现相关的多选功能。
(2)相簿列表同样是通过 PhotoKit 读取出所有相簿信息,并使用 tableview 展示出来。
2,效果图
(1)点击“开始选择照片”按钮,会弹出我们自定义的图片多选组件。默认显示出“相机胶卷”里的所有照片。

(2)点击导航栏左上角的“相簿”按钮,即可列出设备中所有相簿,点击便显示出该相簿中的所有照片。

(3)点击图片缩略图即可在选中和取消选中状态间切换,同时图片右上角的打勾图标在状态改变时也会有弹性变化效果。

(4)选择图片的同时下方工具栏也会实时显示出选中的图片数量,这个改变时同样有动画效果。如果超过限制(可用设置),则会弹出消息提示。

(5)点击“完成”按钮,退出图片选择组件。这里我们在在回调方法中把选中的结果给打印出来。

二、组件介绍
整个组件的代码结构如下,下面分别进行介绍。

1,相簿列表相关
(1)HGImagePickerController.swift(相簿列表页控制器)
- 注意最下面,为方便使用,我扩展了 UIViewController,添加了个 presentHGImagePicker() 方法,调用后便会出现照片选择组件。
- 虽然上面方法调用时加载的是相簿列表视图。但一开始时会先加载相机胶卷图片,并自动跳转到图片选择视图。
import UIKit
import Photos
//相簿列表项
struct HGImageAlbumItem {
//相簿名称
var title:String?
//相簿内的资源
var fetchResult:PHFetchResult<PHAsset>
}
//相簿列表页控制器
class HGImagePickerController: UIViewController {
//显示相簿列表项的表格
@IBOutlet weak var tableView:UITableView!
//相簿列表项集合
var items:[HGImageAlbumItem] = []
//每次最多可选择的照片数量
var maxSelected:Int = Int.max
//照片选择完毕后的回调
var completeHandler:((_ assets:[PHAsset])->())?
//从xib或者storyboard加载完毕就会调用
override func awakeFromNib() {
super.awakeFromNib()
//申请权限
PHPhotoLibrary.requestAuthorization({ (status) in
if status != .authorized {
return
}
// 列出所有系统的智能相册
let smartOptions = PHFetchOptions()
let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum,
subtype: .albumRegular,
options: smartOptions)
self.convertCollection(collection: smartAlbums)
//列出所有用户创建的相册
let userCollections = PHCollectionList.fetchTopLevelUserCollections(with: nil)
self.convertCollection(collection: userCollections
as! PHFetchResult<PHAssetCollection>)
//相册按包含的照片数量排序(降序)
self.items.sort { (item1, item2) -> Bool in
return item1.fetchResult.count > item2.fetchResult.count
}
//异步加载表格数据,需要在主线程中调用reloadData() 方法
DispatchQueue.main.async{
self.tableView?.reloadData()
//首次进来后直接进入第一个相册图片展示页面(相机胶卷)
if let imageCollectionVC = self.storyboard?
.instantiateViewController(withIdentifier: "hgImageCollectionVC")
as? HGImageCollectionViewController{
imageCollectionVC.title = self.items.first?.title
imageCollectionVC.assetsFetchResults = self.items.first?.fetchResult
imageCollectionVC.completeHandler = self.completeHandler
imageCollectionVC.maxSelected = self.maxSelected
self.navigationController?.pushViewController(imageCollectionVC,
animated: false)
}
}
})
}
//页面加载完毕
override func viewDidLoad() {
super.viewDidLoad()
//设置标题
title = "相簿"
//设置表格相关样式属性
self.tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
self.tableView.rowHeight = 55
//添加导航栏右侧的取消按钮
let rightBarItem = UIBarButtonItem(title: "取消", style: .plain, target: self,
action:#selector(cancel) )
self.navigationItem.rightBarButtonItem = rightBarItem
}
//转化处理获取到的相簿
private func convertCollection(collection:PHFetchResult<PHAssetCollection>){
for i in 0..<collection.count{
//获取出但前相簿内的图片
let resultsOptions = PHFetchOptions()
resultsOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate",
ascending: false)]
resultsOptions.predicate = NSPredicate(format: "mediaType = %d",
PHAssetMediaType.image.rawValue)
let c = collection[i]
let assetsFetchResult = PHAsset.fetchAssets(in: c , options: resultsOptions)
//没有图片的空相簿不显示
if assetsFetchResult.count > 0 {
let title = titleOfAlbumForChinse(title: c.localizedTitle)
items.append(HGImageAlbumItem(title: title,
fetchResult: assetsFetchResult))
}
}
}
//由于系统返回的相册集名称为英文,我们需要转换为中文
private func titleOfAlbumForChinse(title:String?) -> String? {
if title == "Slo-mo" {
return "慢动作"
} else if title == "Recently Added" {
return "最近添加"
} else if title == "Favorites" {
return "个人收藏"
} else if title == "Recently Deleted" {
return "最近删除"
} else if title == "Videos" {
return "视频"
} else if title == "All Photos" {
return "所有照片"
} else if title == "Selfies" {
return "自拍"
} else if title == "Screenshots" {
return "屏幕快照"
} else if title == "Camera Roll" {
return "相机胶卷"
}
return title
}
//取消按钮点击
func cancel() {
//退出当前视图控制器
self.dismiss(animated: true, completion: nil)
}
//页面跳转
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
//如果是跳转到展示相簿缩略图页面
if segue.identifier == "showImages"{
//获取照片展示控制器
guard let imageCollectionVC = segue.destination
as? HGImageCollectionViewController,
let cell = sender as? HGImagePickerCell else{
return
}
//设置回调函数
imageCollectionVC.completeHandler = completeHandler
//设置标题
imageCollectionVC.title = cell.titleLabel.text
//设置最多可选图片数量
imageCollectionVC.maxSelected = self.maxSelected
guard let indexPath = self.tableView.indexPath(for: cell) else { return }
//获取选中的相簿信息
let fetchResult = self.items[indexPath.row].fetchResult
//传递相簿内的图片资源
imageCollectionVC.assetsFetchResults = fetchResult
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
//相簿列表页控制器UITableViewDelegate,UITableViewDataSource协议方法的实现
extension HGImagePickerController:UITableViewDelegate,UITableViewDataSource{
//设置单元格内容
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
//同一形式的单元格重复使用,在声明时已注册
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
as! HGImagePickerCell
let item = self.items[indexPath.row]
cell.titleLabel.text = "\(item.title ?? "") "
cell.countLabel.text = "(\(item.fetchResult.count))"
return cell
}
//表格单元格数量
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count
}
//表格单元格选中
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
extension UIViewController {
//HGImagePicker提供给外部调用的接口,同于显示图片选择页面
func presentHGImagePicker(maxSelected:Int = Int.max,
completeHandler:((_ assets:[PHAsset])->())?)
-> HGImagePickerController?{
//获取图片选择视图控制器
if let vc = UIStoryboard(name: "HGImage", bundle: Bundle.main)
.instantiateViewController(withIdentifier: "imagePickerVC")
as? HGImagePickerController{
//设置选择完毕后的回调
vc.completeHandler = completeHandler
//设置图片最多选择的数量
vc.maxSelected = maxSelected
//将图片选择视图控制器外添加个导航控制器,并显示
let nav = UINavigationController(rootViewController: vc)
self.present(nav, animated: true, completion: nil)
return vc
}
return nil
}
}
(2)HGImagePickerCell.swift(相簿列表单元格)
import UIKit
//相簿列表单元格
class HGImagePickerCell: UITableViewCell {
//相簿名称标签
@IBOutlet weak var titleLabel:UILabel!
//照片数量标签
@IBOutlet weak var countLabel:UILabel!
override func awakeFromNib() {
super.awakeFromNib()
self.layoutMargins = UIEdgeInsets.zero
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
2,图片选择页面相关
(1)HGImageCollectionViewController.swift(图片缩略图集合页控制器)
(2)HGImageCollectionViewCell.swift(图片缩略图集合页单元格)
(3)HGImageCompleteButton.swift(照片选择页下方工具栏的“完成”按钮)
里面就两个图标图片,分别表示选中和未选中状态。

import UIKit
import Photos
//图片缩略图集合页控制器
class HGImageCollectionViewController: UIViewController {
//用于显示所有图片缩略图的collectionView
@IBOutlet weak var collectionView:UICollectionView!
//下方工具栏
@IBOutlet weak var toolBar:UIToolbar!
//取得的资源结果,用了存放的PHAsset
var assetsFetchResults:PHFetchResult<PHAsset>?
//带缓存的图片管理对象
var imageManager:PHCachingImageManager!
//缩略图大小
var assetGridThumbnailSize:CGSize!
//每次最多可选择的照片数量
var maxSelected:Int = Int.max
//照片选择完毕后的回调
var completeHandler:((_ assets:[PHAsset])->())?
//完成按钮
var completeButton:HGImageCompleteButton!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//根据单元格的尺寸计算我们需要的缩略图大小
let scale = UIScreen.main.scale
let cellSize = (self.collectionView.collectionViewLayout as!
UICollectionViewFlowLayout).itemSize
assetGridThumbnailSize = CGSize(width: cellSize.width*scale ,
height: cellSize.height*scale)
}
override func viewDidLoad() {
super.viewDidLoad()
//背景色设置为白色(默认是黑色)
self.collectionView.backgroundColor = UIColor.white
//初始化和重置缓存
self.imageManager = PHCachingImageManager()
self.resetCachedAssets()
//设置单元格尺寸
let layout = (self.collectionView.collectionViewLayout as!
UICollectionViewFlowLayout)
layout.itemSize = CGSize(width: UIScreen.main.bounds.size.width/4-1,
height: UIScreen.main.bounds.size.width/4-1)
//允许多选
self.collectionView.allowsMultipleSelection = true
//添加导航栏右侧的取消按钮
let rightBarItem = UIBarButtonItem(title: "取消", style: .plain,
target: self, action: #selector(cancel))
self.navigationItem.rightBarButtonItem = rightBarItem
//添加下方工具栏的完成按钮
completeButton = HGImageCompleteButton()
completeButton.addTarget(target: self, action: #selector(finishSelect))
completeButton.center = CGPoint(x: UIScreen.main.bounds.width - 50, y: 22)
completeButton.isEnabled = false
toolBar.addSubview(completeButton)
}
//重置缓存
func resetCachedAssets(){
self.imageManager.stopCachingImagesForAllAssets()
}
//取消按钮点击
func cancel() {
//退出当前视图控制器
self.navigationController?.dismiss(animated: true, completion: nil)
}
//获取已选择个数
func selectedCount() -> Int {
return self.collectionView.indexPathsForSelectedItems?.count ?? 0
}
//完成按钮点击
func finishSelect(){
//取出已选择的图片资源
var assets:[PHAsset] = []
if let indexPaths = self.collectionView.indexPathsForSelectedItems{
for indexPath in indexPaths{
assets.append(assetsFetchResults![indexPath.row] )
}
}
//调用回调函数
self.navigationController?.dismiss(animated: true, completion: {
self.completeHandler?(assets)
})
}
}
//图片缩略图集合页控制器UICollectionViewDataSource,UICollectionViewDelegate协议方法的实现
extension HGImageCollectionViewController:UICollectionViewDataSource
,UICollectionViewDelegate{
//CollectionView项目
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return self.assetsFetchResults?.count ?? 0
}
// 获取单元格
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//获取storyboard里设计的单元格,不需要再动态添加界面元素
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell",
for: indexPath) as! HGImageCollectionViewCell
let asset = self.assetsFetchResults![indexPath.row]
//获取缩略图
self.imageManager.requestImage(for: asset, targetSize: assetGridThumbnailSize,
contentMode: .aspectFill, options: nil) {
(image, nfo) in
cell.imageView.image = image
}
return cell
}
//单元格选中响应
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath)
as? HGImageCollectionViewCell{
//获取选中的数量
let count = self.selectedCount()
//如果选择的个数大于最大选择数
if count > self.maxSelected {
//设置为不选中状态
collectionView.deselectItem(at: indexPath, animated: false)
//弹出提示
let title = "你最多只能选择\(self.maxSelected)张照片"
let alertController = UIAlertController(title: title, message: nil,
preferredStyle: .alert)
let cancelAction = UIAlertAction(title:"我知道了", style: .cancel,
handler:nil)
alertController.addAction(cancelAction)
self.present(alertController, animated: true, completion: nil)
}
//如果不超过最大选择数
else{
//改变完成按钮数字,并播放动画
completeButton.num = count
if count > 0 && !self.completeButton.isEnabled{
completeButton.isEnabled = true
}
cell.playAnimate()
}
}
}
//单元格取消选中响应
func collectionView(_ collectionView: UICollectionView,
didDeselectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath)
as? HGImageCollectionViewCell{
//获取选中的数量
let count = self.selectedCount()
completeButton.num = count
//改变完成按钮数字,并播放动画
if count == 0{
completeButton.isEnabled = false
}
cell.playAnimate()
}
}
}
(2)HGImageCollectionViewCell.swift(图片缩略图集合页单元格)
import UIKit
//图片缩略图集合页单元格
open class HGImageCollectionViewCell: UICollectionViewCell {
//显示缩略图
@IBOutlet weak var imageView:UIImageView!
//显示选中状态的图标
@IBOutlet weak var selectedIcon:UIImageView!
//设置是否选中
open override var isSelected: Bool {
didSet{
if isSelected {
selectedIcon.image = UIImage(named: "hg_image_selected")
}else{
selectedIcon.image = UIImage(named: "hg_image_not_selected")
}
}
}
//播放动画,是否选中的图标改变时使用
func playAnimate() {
//图标先缩小,再放大
UIView.animateKeyframes(withDuration: 0.4, delay: 0, options: .allowUserInteraction,
animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.2,
animations: {
self.selectedIcon.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
})
UIView.addKeyframe(withRelativeStartTime: 0.2, relativeDuration: 0.4,
animations: {
self.selectedIcon.transform = CGAffineTransform.identity
})
}, completion: nil)
}
open override func awakeFromNib() {
super.awakeFromNib()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
}
}
(3)HGImageCompleteButton.swift(照片选择页下方工具栏的“完成”按钮)
import UIKit
//照片选择页下方工具栏的“完成”按钮
class HGImageCompleteButton: UIView {
//已选照片数量标签
var numLabel:UILabel!
//按钮标题标签“完成”
var titleLabel:UILabel!
//按钮的默认尺寸
let defaultFrame = CGRect(x:0, y:0, width:70, height:20)
//文字颜色(同时也是数字背景颜色)
let titleColor = UIColor(red: 0x09/255, green: 0xbb/255, blue: 0x07/255, alpha: 1)
//点击点击手势
var tapSingle:UITapGestureRecognizer?
//设置数量
var num:Int = 0{
didSet{
if num == 0{
numLabel.isHidden = true
}else{
numLabel.isHidden = false
numLabel.text = "\(num)"
playAnimate()
}
}
}
//是否可用
var isEnabled:Bool = true {
didSet{
if isEnabled {
titleLabel.textColor = titleColor
tapSingle?.isEnabled = true
}else{
titleLabel.textColor = UIColor.gray
tapSingle?.isEnabled = false
}
}
}
init(){
super.init(frame:defaultFrame)
//已选照片数量标签初始化
numLabel = UILabel(frame:CGRect(x: 0 , y: 0 , width: 20, height: 20))
numLabel.backgroundColor = titleColor
numLabel.layer.cornerRadius = 10
numLabel.layer.masksToBounds = true
numLabel.textAlignment = .center
numLabel.font = UIFont.systemFont(ofSize: 15)
numLabel.textColor = UIColor.white
numLabel.isHidden = true
self.addSubview(numLabel)
//按钮标题标签初始化
titleLabel = UILabel(frame:CGRect(x: 20 , y: 0 ,
width: defaultFrame.width - 20,
height: 20))
titleLabel.text = "完成"
titleLabel.textAlignment = .center
titleLabel.font = UIFont.systemFont(ofSize: 15)
titleLabel.textColor = titleColor
self.addSubview(titleLabel)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
//用户数字改变时播放的动画
func playAnimate() {
//从小变大,且有弹性效果
self.numLabel.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5,
initialSpringVelocity: 0.5, options: UIViewAnimationOptions(),
animations: {
self.numLabel.transform = CGAffineTransform.identity
}, completion: nil)
}
//添加事件响应
func addTarget(target: Any?, action: Selector?) {
//单击监听
tapSingle = UITapGestureRecognizer(target:target,action:action)
tapSingle!.numberOfTapsRequired = 1
tapSingle!.numberOfTouchesRequired = 1
self.addGestureRecognizer(tapSingle!)
}
}
3,相关资源
(1)HGImage.xcassets里面就两个图标图片,分别表示选中和未选中状态。

(2)HGImage.storyboard
- 在 storyboard 中添加两个 View Controller Scene,分别绑定上面的 HGImagePickerController 和 HGImageCollectionViewController。
- 同时两个 Scene 中的 cell 分别绑定 HGImagePickerCell 和 HGImageCollectionViewCell。
- Image Picker Controller Scene 的 Storyboard ID 设置为 imagePickerVC。单元格 identifier 设置为 cell。
- Image Collection View Controller Scene 的 Storyboard ID 设置为 hgImageCollectionVC。单元格 identifier 设置为 cell。
- 从 Image Picker Controller Scene 的单元格拖动一个 Segue 到 Image Collection View Controller Scene。类型为 Show,identifier 为 showImages。
三、使用样例
1,Info.plist配置
由于苹果安全策略更新,在使用 Xcode8 开发时,需要在 Info.plist 配置请求照片相的关描述字段(Privacy - Photo Library Usage Description)
2,样例代码
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
//“开始选择照片”按钮点击
@IBAction func buttonTapped(_ sender: AnyObject) {
//开始选择照片,最多允许选择4张
_ = self.presentHGImagePicker(maxSelected:4) { (assets) in
//结果处理
print("共选择了\(assets.count)张图片,分别如下:")
for asset in assets {
print(asset)
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
源码下载:附:实现视频多选功能
要想实现视频文件多选功能的话,只需要把 HGImagePickerController.swift 中的资源类型改成视频即可://转化处理获取到的相簿
private func convertCollection(collection:PHFetchResult<PHAssetCollection>){
for i in 0..<collection.count{
//获取出但前相簿内的图片
let resultsOptions = PHFetchOptions()
resultsOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate",
ascending: false)]
resultsOptions.predicate = NSPredicate(format: "mediaType = %d",
PHAssetMediaType.video.rawValue)
let c = collection[i]
let assetsFetchResult = PHAsset.fetchAssets(in: c , options: resultsOptions)
//没有图片的空相簿不显示
if assetsFetchResult.count > 0 {
let title = titleOfAlbumForChinse(title: c.localizedTitle)
items.append(HGImageAlbumItem(title: title,
fetchResult: assetsFetchResult))
}
}
}
全部评论(13)
大佬,您写的代码太好了~~~能放在GitHub上,然后可以用pods安装使用,让跟多人维护拓展使用吗?
站长回复:谢谢你的夸奖。主要目前太忙,光写文章都觉得时间不够用,实在抽不出时间弄GitHub。
航哥,为什么我把相册取的类型换成视频,返回不了选中的值啊
站长回复:我改了下是可以的啊,不太清楚你那边什么情况。
HGImagePickerController let assetsFetchResult = PHAsset.fetchAssets(in: c , options: resultsOptions) 行 崩溃
log:
2018-01-23 18:19:01.384202+0800 Test2[1978:1716369] -[PHCollectionList assetCollectionType]: unrecognized selector sent to instance 0x1d4363780
2018-01-23 18:19:01.384339+0800 Test2[1978:1716369] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[PHCollectionList assetCollectionType]: unrecognized selector sent to instance 0x1d4363780'
站长回复:我又测试了下文章下方的项目,没有发现你说的问题啊?
并没有图片预览功能
站长回复:这个只是演示图片选择功能,如果想加个预览也简单,把前面选择图片展示出来就好了。
航哥,为什么复制你的代码,加了权限,为什么报错呢!!!还有就是谢谢航哥你前面回复,新手学习中....
站长回复:报什么错误?还有你直接下载运行文章末尾的项目程序会报错吗?如果不报错的话,估计是你复制代码时那一步错了。
航哥啊,这份代码你那还有没有swift2.3的版本啊,
站长回复:没有了,如果我更新代码的话老的是不会留的。
代码清晰,看的好爽,谢谢分享
站长回复:不客气,欢迎常来看看,我会持续更新下去的。
大神好:
这个框架叫什么名字啊?是HGImage吗 还是 HGImagePicker啊?
站长回复:之前主要精力都放在功能实现上,给这个框架起什么名字我倒没想过。就这两个比较的话,HGImagePicker会更适合些吧。
航哥,我选完图片,点击完成后,不显示图片啊?是没有做这个功能还是我的错误没有显示?
站长回复:这篇文章讲的只是多选功能的实现,选完后我这里只是直接将获取到的图片assets给打印出来。后续是上传还是显示出来大家就自行处理了。
不得不佩服 真高手!代码干净利落,sb板玩的66转。给纯code写界面的信徒,一个闪亮耳光
站长回复:哈哈,多谢夸奖。条条大陆通罗马,适合自己的才是最好的。
点击完成,怎么让所选择的图片显示在界面上呢?
站长回复:选择完图片后会返回assets,通过 PHImageManager 可以获取到对应的原图,最后将图片显示在 imageView 上即可。PHImageManager的使用参考我之前写的这篇文章:Swift - 使用PhotoKit获取照片1(获取所有照片缩略图、原图及其信息)
我的会奔溃,这是奔溃原因:-[PHCollectionList assetCollectionType]: unrecognized selector sent to instance。
站长回复:是不是你设备里没有照片,之前代码我没有判断照片为空的情况。现在应该没问题了,你可以再试试。
有个bug,如果相册里没有图片的画,会崩溃。恰好我用最新的pad跑了一下
站长回复:多谢你的提醒,这个之前还真没注意到,代码现已修改。