Photos框架 - 自定义媒体资源选择器(数据部分)

引言
在iOS开发中,系统已经为我们提供了多种便捷的媒体资源选择方式,如UIImagePickerController和PHPickerViewController。这些方式不仅使用方便、界面友好,而且我们完全不需要担心性能和稳定性问题,因为它们是由系统提供的,经过充分测试和优化。
然而,在实际开发过程中,为了使我们的APP更加独特,界面更加新颖,设计团队往往会提出个性化的媒体资源选择页面的需求。这时候,我们就需要放弃系统提供的方案,转而创建自定义的媒体资源选择器。本文将介绍如何使用Photos框架来自定义媒体资源选择器,以满足特定的设计和功能需求。
AssetsLibrary -> Photos
在iOS 8之前,开发者主要使用AssetsLibrary框架来访问和管理用户的照片和视频。然而,从iOS 8开始,Apple引入了全新的Photos框架,并逐步弃用AssetsLibrary。
自iOS 9起,AssetsLibrary被正式标记为弃用,Apple强烈建议开发者迁移到Photos框架。Photos框架不仅提供了更高效的性能和更丰富的功能,还为开发者提供了更强大的工具来管理和操作用户的媒体资源。
通过Photos框架,开发者可以更轻松地获取媒体元数据、编辑照片、创建自定义相册,以及实现更多自定义功能。这一过渡标志着iOS媒体管理能力的重大提升,为开发者提供了更广泛的可能性和更强大的控制力。
下面我们就使用Photos框架,来创建一个初级的媒体资源选择器,之后的博客中,我们再不停的来完善它的功能。
创建媒体选择器
我打算把它分成数据和UI两部分来实现这个媒体选择器,本篇博客我们就先从数据部分说起。
媒体数据读取
1.创建配置信息
在媒体资源读取时有很多数据我们可以进行任意配置,比如读取的媒体类型、读取的视频最大时长、获取缩略图尺寸,图片缓存个数等等,为此我们创建了一个名为PHMediaConfig的类,代码如下:
import UIKit
import Photosenum PHMediaType {/// 图片case image/// 视频case video/// 图片和视频case all
}class PHMediaConfig: NSObject {/// 获取资源类型(默认视频和图片)var mediaType: PHMediaType = .all/// 缩略图缓存数量var thumbnailCacheCount: Int = 40/// 大图缓存数量var originalImageCacheCount: Int = 10/// 获取视频的时长最大值var videoMaxDuration: TimeInterval = 60/// 是否直接加载原图var isLoadOriginalImage: Bool = false/// 可选图片最大数量var maxSelectedImageCount: Int = 9/// 缩略图尺寸var thumbnailSize: CGSize = CGSize(width: 200, height: 200)}
里定义了很多配置信息,并且也都设置了初始值。
2.创建媒体资源管理类
创建一个继承自NSObjct名为PHMediaManager的类,用来读取媒体资源数据,获取缩略,原图等等一切和数据相关的内容,并通过初始化方法传入配置信息,代码如下:
class PHMediaManager: NSObject {/// 缩略图缓存private var thumbnailCache = NSCache<NSString, UIImage>()/// 原图缓存private var originalImageCache = NSCache<NSString, UIImage>()/// 配置private var config: PHMediaConfig!init(config: PHMediaConfig = PHMediaConfig()) {super.init()self.config = configthumbnailCache.countLimit = config.thumbnailCacheCountoriginalImageCache.countLimit = config.originalImageCacheCount}....
}
除此之外,我们还定义了两个缓存表,稍后的代码中会使用到它们。
3.获取媒体库权限
苹果对隐私权限的申请非常重视,所以在获取媒体资源前一定要检查权限和申请权限,并给用户友好的提示,包括infoplist文件内的文案也需要认真填写,表明申请权限的用途。

检查和申请权限的代码如下:
/// 查看相册权限func checkPhotoLibraryAuthorization() -> Bool {let status = PHPhotoLibrary.authorizationStatus()if status == .authorized {return true} else {return false}}
/// 查看并获取相册权限/// - Parameter completion: 回调func requestPhotoLibraryAuthorization(completion: @escaping (Bool) -> Void) {PHPhotoLibrary.requestAuthorization { status inif status == .authorized {print("获取相册权限成功")completion(true)} else {print("获取相册权限失败")completion(false)}}}
4.获取媒体资源
权限申请通过后,就可以开始获取媒体资源了,这时候有两个方案可以供我们选择:
方案一:在读取资源时,通过PHAsset直接读取缩略图构建模型数组。
如果采用方案1的话,在渲染列表时,我们就可以直接使用UIImage进行渲染,页面反应很快,用户体验会很好。
但是呢预先加载所有的缩略图这样会占用很大的内存,尤其是相册资源比较多的情况甚至可能会导致崩溃,这样的话我们就需要手动控制一次加载资源的数量。
方案二:在读取资源时,只保存PHAsset。
这个方案呢,我们到不需要考虑内存的问题,因为只有在现实的时候才会加载缩略图,显示完成之后就会自动被释放,但是这就会有新的问题,每次图片都是重新加载,可能会使得页面不流畅,影响用户体验。这样的话我们就需要自己来创建和管理缓存来提升用户体验。
我们来采取方案二 + 自定义缓存的方式来读取媒体资源,这样的话我们的数据模型只需要保存PHAsset就可以了,我们先来看一下自定义数据模型的代码:
import UIKit
import Photosclass PHMediaModel: NSObject {/// 资源var asset: PHAsset?/// 是否选中var isSelected: Bool = false/// 资源标识var identifier: String?/// 类型var mediaType: PHAssetMediaType {get {return asset?.mediaType ?? .unknown}}/// 视频时长var videoDuration: TimeInterval {get {return asset?.duration ?? 0}}
}
除了PHAsset以外,还定义了一些选中状态已经视频时长等数据,稍后我们会使用到它们。
下面就开始读取媒体资源数据,构建自定义数据模型:
/// 获取相册资源/// - Parameters:func fetchLocalAlbums(completion: @escaping ([PHMediaModel]) -> Void) {self.fetchLocalAlbums(type: config.mediaType, completion: completion)}/// 获取本地相册资源/// - Parameters:/// - type: 类型/// - completion: 回调private func fetchLocalAlbums(type: PHMediaType, completion: @escaping ([PHMediaModel]) -> Void) {let options = PHFetchOptions()options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]if type == .image {options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue)} else if type == .video {options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.video.rawValue)} else {options.predicate = NSPredicate(format: "mediaType = %d || mediaType = %d", PHAssetMediaType.image.rawValue, PHAssetMediaType.video.rawValue)}let fetchResult = PHAsset.fetchAssets(with: options)var assets = [PHMediaModel]()fetchResult.enumerateObjects { asset, index, stop inif asset.mediaType == .image {let model = PHMediaModel()model.asset = assetmodel.identifier = asset.localIdentifierassets.append(model)} else if asset.mediaType == .video {if asset.duration <= self.config.videoMaxDuration {let model = PHMediaModel()model.asset = assetmodel.identifier = asset.localIdentifierassets.append(model)}}}completion(assets)}
通常情况下,我们只关心图片类型和视频类型的数据,并且根据视频的时长还进行了进一步的过滤。
5.获取资源缩略图
另外我们还单独定义了一个读取资源缩略图的方法,并且在这个方法里面使用了缩略图缓存,代码如下:
/// 获取缩略图/// - Parameters:/// - asset: 资源/// - size: 尺寸/// - completion: 回调func fetchThumbnail(asset: PHAsset, size: CGSize? = nil, completion: @escaping (UIImage?) -> Void) {if let size = size {config.thumbnailSize = size}let key = asset.localIdentifier as NSStringif let image = thumbnailCache.object(forKey: key) {completion(image)} else {let options = PHImageRequestOptions()options.isSynchronous = falseoptions.resizeMode = .fastoptions.deliveryMode = .opportunisticoptions.isNetworkAccessAllowed = truePHImageManager.default().requestImage(for: asset, targetSize: config.thumbnailSize, contentMode: .aspectFill, options: options) { image, info inif let image = image {self.thumbnailCache.setObject(image, forKey: key)completion(image)} else {completion(nil)}}}}
6.获取图片资源原图
除了缩略图之外,还需要读取图片原图用来图片单张预览,代码如下:
/// 获取原图/// - Parameters:/// - asset: 资源/// - completion: 回调func fetchOriginalImage(asset: PHAsset, completion: @escaping (UIImage?) -> Void) {let key = asset.localIdentifier as NSStringif let image = originalImageCache.object(forKey: key) {completion(image)} else {let options = PHImageRequestOptions()options.isSynchronous = falseoptions.resizeMode = .fastoptions.deliveryMode = .opportunisticoptions.isNetworkAccessAllowed = truePHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: options) { image, info inif let image = image {self.originalImageCache.setObject(image, forKey: key)completion(image)} else {completion(nil)}}}}
7.获取视频原数据
获取完图片数据,视频也需要原始的预览数据,毕竟我们上传时不能只上传一个缩略图,代码如下:
/// 获取视频原数据/// - Parameters:/// - asset: 资源/// - completion: 回调func fetchVideoData(asset: PHAsset, completion: @escaping (Data?) -> Void) {let options = PHVideoRequestOptions()options.isNetworkAccessAllowed = truePHImageManager.default().requestAVAsset(forVideo: asset, options: options) { avAsset, audioMix, info inif let urlAsset = avAsset as? AVURLAsset {do {let data = try Data(contentsOf: urlAsset.url)completion(data)} catch {completion(nil)}} else {completion(nil)}}}
结语
我们从自定义个媒体选择器入手,来探讨一些Photos框架的用法,本篇博客我们主要介绍了使用Photos获取相册权限,读取媒体数据,以及如何配置读取的数据参数。
下一篇博客我们将开始使用这些数据来构建一个媒体资源选择器的UI页面。
相关文章:
Photos框架 - 自定义媒体资源选择器(数据部分)
引言 在iOS开发中,系统已经为我们提供了多种便捷的媒体资源选择方式,如UIImagePickerController和PHPickerViewController。这些方式不仅使用方便、界面友好,而且我们完全不需要担心性能和稳定性问题,因为它们是由系统提供的&…...
Spring Boot + Spring Cloud 入门
运行配置 java -jar spring-boot-config-0.0.1-SNAPSHOT.jar --spring.profiles.activetest --my1.age32 --debugtrue "D:\Program Files\Redis\redis-server.exe" D:\Program Files\Redis\redis.windows.conf "D:\Program Files\Redis\redis-cli.exe" &q…...
怎么使用动态IP地址上网
如何设置动态IP地址上网? 设置动态IP地址上网的步骤如下: 一、了解动态IP地址 动态IP地址是由网络服务提供商(ISP)动态分配给用户的IP地址,它会根据用户的需求和网络情况实时改变。相比于静态IP地址,动态…...
【源码+文档+调试讲解】智慧物流小程序的设计与实现
摘 要 互联网发展至今,无论是其理论还是技术都已经成熟,而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播,搭配信息管理工具可以很好地为人们提供服务。针对高校教师成果信息管理混乱,出错率高,信息安全…...
QT:控件圆角设置、固定窗口大小
实现控件圆角度设置//使用的是setStyleSheet方法 //改变的控件是QTextEdit,如果你想改变其他控件,将QTextEdit进行更换 this->setStyleSheet("QTextEdit{background-color:#FFFFFF;border-top-left-radius:15px;border-top-right-radius:15px;bo…...
【JavaScript】深入理解 `let`、`var` 和 `const`
文章目录 一、var 的声明与特点二、let 的声明与特点三、const 的声明与特点四、let、var 和 const 的对比五、实战示例六、最佳实践 在 JavaScript 中,变量声明是编程的基础,而 let、var 和 const 是三种常用的变量声明方式。本文将详细介绍这三种变量声…...
云监控(华为) | 实训学习day7(10)
水一篇。。。。。。。。。。。。。 强迫症打卡必须要满 企拓 今天没有将东西 2024/7/22 规划学习路线对于进入AI行业至关重要。以下是一个详细的学习路线规划,旨在帮助你从零基础到成为一名合格的AI或大数据分析师: 第一阶段:基础知识建设…...
JS_plus.key.addEventListener监听键盘按键
官方文档:https://www.html5plus.org/doc/zh_cn/key.html 监听事件 plus.key.addEventListener(keydown, e > {console.log("keydown: "e.keyCode) }) plus.key.addEventListener(keyup, e > {console.log("keyup: "e.keyCode) })移除事…...
对话系统(Chat)与自主代理(Agent)对撞
随着生成式AI技术的不断进步,关于其未来发展方向的讨论也愈发激烈。究竟生成式AI的未来是在对话系统(Chat)中展现智慧,还是在自主代理(Agent)中体现能力?这一问题引发了广泛的讨论和探索。 首先…...
sql server 连接报错error 40
做个简单的记录,造成40 的原因有很多,你的错误并不一定就是我遇到的这种情况. 错误描述: 首先我在使用ssms 工具连接的时候是可以正常连接的,也能对数据库进行操作. 在使用 ef core 连接 Sql Server 时报错: Microsoft.Data.SqlClient.SqlException (0x80131904): A network-r…...
邮件安全篇:如何防止邮件泄密?
本文主要讨论组织内部用户违反保密规定通过邮件泄密的场景。其他场景导致邮箱泄密的问题(如账号被盗、邮件系统存在安全漏洞等)不在本文的讨论范围。本文主要从邮件系架构设计、邮件数据防泄漏系统、建立健全规章制度、安全意识培训等方面分别探讨。 1. …...
MySQL查询优化:提升数据库性能的策略
在数据库管理和应用中,优化查询是提高MySQL数据库性能的关键环节。随着数据量的不断增长,如何高效地检索和处理数据成为了一个重要的挑战。本文将介绍一系列优化MySQL查询的策略,帮助开发者和管理员提升数据库的性能。 案例1: 使用索引优化查…...
vue-快速入门
Vue 前端体系、前后端分离 1、概述 1.1、简介 Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,可以高效地开发用户界面。…...
【网络流】——初识(最大流)
网络流-最大流 基础信息引入一些概念基本性质 最大流定义 Ford–Fulkerson 增广Edmons−Karp算法Dinic 算法参考文献 基础信息 引入 假定现在有一个无限放水的自来水厂和一个无限收水的小区,他们之间有多条水管和一些节点构成。 每一条水管有三个属性:…...
【STM32嵌入式系统设计与开发---拓展】——1_10矩阵按键
这里写目录标题 1、矩阵按键2、代码片段分析 1、矩阵按键 通过将4x4矩阵按键的每一行依次设为低电平,同时保持其它行为高电平,然后读取所有列的电平状态,可以检测到哪个按键被按下。如果某列变为低电平,说明对应行和列的按键被按下…...
长期更新方法库推荐pmq-ui
# pmq-ui pmq-ui 好用方法库ui库, 欢迎您的使用 ## 安装 1. 克隆项目库到本地: 2. 进入项目目录:cd pmq-ui 3. 安装依赖:npm install pmq-ui ## 使用 <!-- 1. 启动应用: 2. 访问 [http://localhost:3000](http://localhost:300…...
<数据集>抽烟识别数据集<目标检测>
数据集格式:VOCYOLO格式 图片数量:4860张 标注数量(xml文件个数):4860 标注数量(txt文件个数):4860 标注类别数:1 标注类别名称:[smoking] 使用标注工具:labelImg 标注规则:对…...
SQL Server 端口设置教程
引言 你好,我是悦创。 在配置 SQL Server 的过程中,设置正确的端口非常关键,因为它影响到客户端如何连接到 SQL Server 实例。默认情况下,SQL Server 使用 TCP 端口 1433,但在多实例服务器上或出于安全考虑ÿ…...
【React1】React概述、基本使用、脚手架、JSX、组件
文章目录 1. React基础1.1 React 概述1.1.1 什么是React1.1.2 React 的特点声明式基于组件学习一次,随处使用1.2 React 的基本使用1.2.1 React的安装1.2.2 React的使用1.2.3 React常用方法说明React.createElement()ReactDOM.render()1.3 React 脚手架的使用1.3.1 React 脚手架…...
k8s部署kafka集群
k8s部署kafka集群 kafka(Kafka with KRaft) mkdir -p ~/kafka-ymlkubectl create ns kafkacat > ~/kafka-yml/kafka.yml << EOF apiVersion: v1 kind: Service metadata:name: kafka-headlessnamespace: kafkalabels:app: kafka spec:type: C…...
label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...
阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...
JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...
QT3D学习笔记——圆台、圆锥
类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体(对象或容器)QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质(定义颜色、反光等)QFirstPersonC…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...
C#中的CLR属性、依赖属性与附加属性
CLR属性的主要特征 封装性: 隐藏字段的实现细节 提供对字段的受控访问 访问控制: 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性: 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑: 可以…...
打手机检测算法AI智能分析网关V4守护公共/工业/医疗等多场景安全应用
一、方案背景 在现代生产与生活场景中,如工厂高危作业区、医院手术室、公共场景等,人员违规打手机的行为潜藏着巨大风险。传统依靠人工巡查的监管方式,存在效率低、覆盖面不足、判断主观性强等问题,难以满足对人员打手机行为精…...
