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() } }源码下载:hangge_1512.zip
附:实现视频多选功能
要想实现视频文件多选功能的话,只需要把 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跑了一下
站长回复:多谢你的提醒,这个之前还真没注意到,代码现已修改。