iOS 相册的使用

1 访问相册功能

1.1 配置相关

1.在Info.plist文件中设置相册权限

<key>NSPhotoLibraryAddUsageDescription</key>
<string>允许此权限才能使用添加照片功能</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>允许此权限才能使用相册功能</string>

2.使用PHPhotoLibrary 的API和UI组件,需要导入以下2个头文件

import Photos
import PhotosUI

1.2 iOS 14以后新新增选择照片...权限

image.png

1.2.1 PHAuthorizationStatus.limited,每次冷启动都会弹窗提示

image.png

  • Info.plist文件中设置PHPhotoLibraryPreventAutomaticLimitedAccessAlertYES可关闭弹窗
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
<true/>

1.2.2 手动触发选择更多照片弹窗

PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: self)
选择照片....jpg

也可以在设置->照片中编辑所选照片

设置->照片.jpg

1.3 获取相册权限和请求相册权限

  1. iOS 14.0包括以上使用
@available(iOS 14, *)
open class func authorizationStatus(for accessLevel: PHAccessLevel) -> PHAuthorizationStatus

@available(iOS 14, *)
open class func requestAuthorization(for accessLevel: PHAccessLevel, handler: @escaping (PHAuthorizationStatus) -> Void)

相册访问方式新增PHAccessLevel

  • addOnly 只读
  • readWrite 读写

PHAuthorizationStatus 权限新增.limited,请求权限在readWrite时生效

     func checkPHAuthorization(_ handler: @escaping ((Bool) -> Void)) {
        // 获取相册权限
        let authStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
        
        switch (authStatus) {
        // 允许访问所有照片
        case .authorized:
            handler(true)
        // 选择照片...
        case .limited:
            handler(true)
        // 首次弹窗权限未授权
        case .notDetermined:
            // 请求相册权限
            PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
                DispatchQueue.main.async {
                    switch status {
                    case .authorized, .limited:
                        handler(true)
                        
                    default:
                        handler(false)
                    }
                }
            }
        // 拒绝等
        default:
            handler(false)
        }
    }
  1. iOS 14.0以下使用
@available(iOS, introduced: 8, deprecated: 100000)
open class func authorizationStatus() -> PHAuthorizationStatus

@available(iOS, introduced: 8, deprecated: 100000)
open class func requestAuthorization(_ handler: @escaping (PHAuthorizationStatus) -> Void)
   func checkPHAuthorization(_ handler: @escaping ((Bool) -> Void)) {
        let authStatus = PHPhotoLibrary.authorizationStatus()
        
        switch (authStatus) {
        // 允许访问所有照片,选择照片...
        case .authorized:
            handler(true)
            
        case .notDetermined:
            PHPhotoLibrary.requestAuthorization { status in
                DispatchQueue.main.async {
                    switch status {
                    case .authorized:
                        handler(true)
                        
                    default:
                        handler(false)
                    }
                }
            }
        default:
            handler(false)
        }
    }

注意:
未设置PHAccessLevel时,选择照片...的权限为.authorized,设置了readWrite时权限为. limited

1.4 单选,使用系统API:UIImagePickerController

let imagePickerController = UIImagePickerController()
imagePickerController.modalPresentationStyle = .fullScreen
imagePickerController.delegate = self
// 是否允许编辑
imagePickerController.allowsEditing = true

需要遵守UINavigationControllerDelegateUIImagePickerControllerDelegate协议

extension LITBImagePickerController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    // 取消
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }
    // 选择照片
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        var fileImage: UIImage?
        
        if picker.allowsEditing {
            if let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
                fileImage = image
            }
        } else {
            if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
                fileImage = image
            }
        }
        
        if let img = fileImage {
            systemHandler?(img)
        }
        
        picker.dismiss(animated: true, completion: nil)
    }
}

1.5 多选,多选UI需要自己实现,相关API

1.5.1 获取缩略图

    /// 获取缩略图
    /// - Parameters:
    ///   - asset: PHAsset
    ///   - imageSize: 图片大小
    ///   - completion: 获取系统图片
    func getThumbnailWithAsset(asset: PHAsset, imageSize: CGSize, completion: @escaping ((UIImage?) -> Void)) {
        let requestOptions = PHImageRequestOptions()
        requestOptions.resizeMode = .fast
        
        let scale = UIScreen.main.scale
        let size = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
        
        PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: requestOptions) { image, info in
            completion(image)
        }
    }

1.5.2 获取原图

    /// 获取原图
    /// - Parameters:
    ///   - asset: PHAsset
    ///   - isSync: 开启同步执行,默认是flase(异步执行)
    ///   - progressHandler: 加载图片进度回调
    ///   - completion: 获取图片回调
    func getOriginalPhotoWithAsset(asset: PHAsset, isSync: Bool = false, progressHandler: ((Double?, Error?) -> Void)?, completion: @escaping ((UIImage?) -> Void)) {
        let requestOptions = PHImageRequestOptions()
        requestOptions.resizeMode = .fast
        requestOptions.isSynchronous = isSync
        
        requestOptions.progressHandler = { (progress, error, stop, info) in
            DispatchQueue.main.async {
                progressHandler?(progress, error)
            }
        }
        
        // 该方法是异步执行,会回调多次,当isSynchronous设为true时,deliveryMode属性就会被忽略,并被当作.HighQualityFormat来处理。
        PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: requestOptions) { image, info in
            /*
             info?[PHImageResultIsDegradedKey]
             
             PHImageResultIsDegradedKey 1表示低质量图片,0是高质量图片
             
             设置isSynchronous为true时,阻塞线程,直到图片被准备好或发生错误时才会回调,回调一次
             */
            completion(image)
        }
    }

1.5.3 获取相薄

    /// 获取相簿列表
    func getAlbumList() -> [AlbumItem] {
        var items: [AlbumItem] = []
        
        // 获取系统的相机相册
        let smartOptions = PHFetchOptions()
        let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: smartOptions)
        items.append(contentsOf: convertCollection(collection: smartAlbums))
        
        // 获取用户创建的相册
        let userCustomCollections = PHFetchOptions()
        let userCustomAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: userCustomCollections)
        items.append(contentsOf: convertCollection(collection: userCustomAlbums))
        
        return items
    }
    
    /// 转化处理获取到的相簿
    private func convertCollection(collection: PHFetchResult<PHAssetCollection>) -> [AlbumItem] {
        var items: [AlbumItem] = []
        
        // 获取转化前相簿内容的图片
        collection.enumerateObjects { object, index, stop in
            let resultsOptions = PHFetchOptions()
            // 按照创建时间,进行倒序
            resultsOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
            resultsOptions.predicate = NSPredicate(format: "mediaType=%d", PHAssetMediaType.image.rawValue)
            
            let assetsFetchResult = PHAsset.fetchAssets(in: object, options: resultsOptions)
            
            // 过滤隐藏相册,最近删除相册(1000000201)
            if object.assetCollectionSubtype == .smartAlbumAllHidden || object.assetCollectionSubtype.rawValue > 1000000000 { return }
            
            if assetsFetchResult.count > 0 {
                let item = AlbumItem(title: object.localizedTitle ?? "Unnamed", fetchResult: assetsFetchResult)
                
                // 最近项目显示在最前面
                if object.assetCollectionSubtype == .smartAlbumUserLibrary {
                    items.insert(item, at: 0)
                } else {
                    items.append(item)
                }
            }
        }
        
        return items
    }

1.5.4 监听相册变化,PHAuthorizationStatus.limited时需要监听

class LITBSelectImagesViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // 开始监听
        PHPhotoLibrary.shared().register(self)
    }
    
    deinit {
        // 结束监听
        PHPhotoLibrary.shared().unregisterChangeObserver(self)
    }
}

extension LITBSelectImagesViewController: PHPhotoLibraryChangeObserver {
    func photoLibraryDidChange(_ changeInstance: PHChange) {
        // selectedFetchResult: PHFetchResult<PHAsset> 为当前选中的相薄
        guard let selectedFetchResult = controller.selectedFetchResult else { return }
        
        DispatchQueue.main.async {
            guard let changes = changeInstance.changeDetails(for: selectedFetchResult) else { return }
            // 获取更新的相册,更新UI
            self.controller.getAllAssets(assets: changes.fetchResultAfterChanges) {
                self.collectionView.reloadData()
            }
        }
    }
}

2 使用PHPickerViewController

  • 不需要设置相册访问权限
  • 不能编辑照片
  • 不支持查看大图
import UIKit
import PhotosUI
import Photos

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func onSingle(_ sender: Any) {
        showPikcer(limitCount: 1)
    }
    
    @IBAction func onMultiple(_ sender: Any) {
        showPikcer(limitCount: 9)
    }
    
    func showPikcer(limitCount: Int) {
        var config = PHPickerConfiguration()
        // 可选择的资源数量,0表示不设限制,默认为1
        config.selectionLimit = limitCount
        // 可选择的资源类型,只显示图片(注:images包含livePhotos)
        config.filter = .images
        // 显示livePhotos和视频(注:livePhotos不包含images)
//        config.filter = .any(of: [.livePhotos, .videos])
        // 如果要获取视频,最好设置该属性,避免系统对视频进行转码
        config.preferredAssetRepresentationMode = .current
        
        let pickerVC = PHPickerViewController(configuration: config)
        pickerVC.delegate = self
        present(pickerVC, animated: true)
    }
}

extension ViewController: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        // 关闭页面
        dismiss(animated: true)
        
        // 取消选择也会触发代理方法,会返回空的results
        guard !results.isEmpty else { return }
        
        results.forEach { result in
            // 判断类型是否为UIImage
            if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
                // 获取图片
                result.itemProvider.loadObject(ofClass: UIImage.self) { itemProvider, error in
                    // 回调结果在异步线程,展示时需要切换到主线程
                    if let image = itemProvider as? UIImage {
                        DispatchQueue.main.async {
                            print("---:", image)
                        }
                    }
                }
            }
        }
    }
}
最多选择9张照片.jpg

更多PHPickerViewController参考资料:https://juejin.cn/post/6881513652176814093

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容