Swift 制作 iOS 实况图:HEIC/MOV 元数据配对与相册写回

发布时间:2026/10/26 10:33:24
Swift 制作 iOS 实况图:HEIC/MOV 元数据配对与相册写回 简介这份 Swift 实况图制作 Demo 面向具备 iOS 基础、希望掌握 Live Photo 合成与展示的开发者聚焦把静态 JPEG 与短视频合成为可交互实况图这一典型场景。压缩包共 21 个文件、约 4.37MB包含 Swift 源码、plist 配置、json 数据、storyboard 界面以及 jpeg/mp4 示例素材工程结构涉及视图控制、工具封装与资源管理等模块便于直接运行和对照修改。已有 1333 人学习说明其在实况图入门与工程实践中具备一定参考价值。读者可借助完整 Xcode 工程理解素材准备、内容合成、请求参数设置、PHLivePhoto 展示与进度监听等关键环节并参考封装好的使用案例快速把实况图能力迁移到自己的 iOS 应用中减少从零摸索与联调成本也可为动态壁纸等扩展玩法提供实现思路。工程基于苹果 Photos 框架适合作为实况图功能原型、课程示例或二次开发起点。1. 相册里多出一张图和一段视频点开却不会动这是把素材转实况图时最典型的出错情况一张 HEIC 加一段 MOV 放进相册系统只当它们是两张互不相干的资源。原因在于系统并不看文件是不是相邻存放它只认两处字节静态图元数据里的 asset identifier以及视频里那条 still-image-time 元数据轨两边必须严格对上。这个 swift-livePhoto 实况图制作 demo 做的就是把这件事拆开给你看——用 Swift 把任意一张图加一段视频重新封装成 iOS 相册认得的 Live Photo并把制作、预览、写回相册三条链路串起来。适合三类人做相机或修图类 App 需要导出实况图的、要把短视频批量转成实况封面的、以及想搞明白实况图到底靠哪几个字节生效的 iOS 开发者。跟着走一遍你应该能自己写一个不依赖任何第三方库的转换器。2. 拆开骨架四层职责与一条能复现的数据流拿到这类 demo 之后先不要急着点 Run因为多数人的第一次失败都发生在“不知道哪一层出了问题”。这个包的思路很清楚把“拼元数据”和“改像素”彻底分开中间用一条固定的数据流串起来。按职责找文件文件名各家写法不同但分层基本一致。2.1 四层职责以及一条不能越界的红线素材层负责把输入统一成CGImage加AVAsset相机拍的、相册挑的、网络下载的都要归一化成同一形态。拼装层只做一件事写元数据、搬容器绝不改像素它的输出是两个 URL——一个带 17 号键的静态图、一个带两条元数据轨的 MOV。转码层才允许动像素裁剪、滤镜、变速、补帧都在这里它的输入输出仍然是视频帧序列。写回层只碰 PhotoKit把两个 URL 变成一个PHAsset。这条红线要守住拼装层不能顺手压缩图片尺寸一转尺寸视频和静态图比例就对不上预览时长按会先闪一下再拉伸转码层不能重新生成 identifier一旦重新生成前面写好的静态图就白写了。2.2 从两个 URL 到相册里一张会动的图七步第一步归一化输入。视频统一成正方形或 4:3、30fps、时长裁到 3 秒以内静态图从视频里取一帧转CGImage或者由用户单独指定。第二步生成一个UUID().uuidString作为本次制作的 identifier后面所有环节共用这一个字符串。第三步写静态图把 identifier 塞进 MakerApple 字典。第四步重封装 MOV写入 content.identifier 和 still-image-time 两条元数据轨。第五步离线校验把两个文件重新读回来比对 identifier、检查元数据轨是否存在、确认时长——这一步很多人省掉结果只能靠相册里反复试。第六步写相册PHAssetCreationRequest先加.photo再加.pairedVideo。第七步预览验收用PHLivePhoto.request(withResourceFileURLs:...)把两个本地文件直接拉起来播能播说明字节层面已经合法再去相册确认长按效果。// 第七步不写相册也能先验证这一对文件合不合法 PHLivePhoto.request(withResourceFileURLs: [imageURL, videoURL], placeholderImage: nil, targetSize: CGSize(width: 1200, height: 1200), contentMode: .aspectFit) { livePhoto, info in if let error info[PHLivePhotoInfoErrorKey] as? Error { print(配对失败:, error) // 走这里基本就是 identifier 或元数据轨的问题 return } if info[PHLivePhotoInfoIsDegradedKey] as? Bool true { return // 降级结果等下一次回调 } previewView.livePhoto livePhoto // PHLivePhotoView 直接赋值 }targetSize别给得太小太小系统会返回降级结果你会误以为文件坏了给到 1200 上下够用。回调会多次触发IsDegraded为 true 的那次先丢掉只认最后一次。2.3 工程配置权限、部署版本、真机验收只写入不读取的 App理论上NSPhotoLibraryAddUsageDescription就够但只要调了PHAsset相关的读取比如编辑回写路径两个都得声明缺一个会在performChanges里抛权限错。keyNSPhotoLibraryAddUsageDescription/key string用于把制作好的实况图保存到相册/string keyNSPhotoLibraryUsageDescription/key string用于读取相册中的实况图做编辑回写/string部署版本上这套 API 门槛不高iOS 12 都能跑只有统一类型标识那套写法需要 iOS 14低版本用public.heic、com.apple.quicktime-movie这种字符串常量兜底即可。模拟器能跑通写入但长按播放的表现和真机有差异验收一定上真机。注意从 iOS 16 起AVAsset.tracks这类同步属性会打并发警告demo 里如果用的同步写法包一层 async 就行不包也能跑但警告会掩盖真正的错误日志。3. 配对协议落到字节上MakerApple 的 17 号键与两条元数据轨实况图没有一个“实况图文件”格式它是两套已有格式的约定。静态图用 HEIC 或 JPEG视频用 QuickTime MOV两者靠一个字符串绑定。搞清这一点后面所有报错都能定位。3.1 静态图侧把 identifier 写进 MakerApple 字典写静态图不是UIImage.jpegData就完事那个接口不给你塞私有元数据的机会。得走 ImageIO 的CGImageDestination在属性字典里加上kCGImagePropertyMakerAppleDictionary里面放一个键为17、值为 identifier 的字符串。import ImageIO import UniformTypeIdentifiers func writeStillImage(_ image: CGImage, to url: URL, assetIdentifier: String, quality: CGFloat 0.92) throws { // HEIC 体积小但个别系统版本对私有元数据更挑不稳就先换 public.jpeg 验证 let type UTType.heic.identifier as CFString guard let dest CGImageDestinationCreateWithURL(url as CFURL, type, 1, nil) else { throw LivePhotoError.destinationFailed } let properties: [CFString: Any] [ kCGImagePropertyMakerAppleDictionary: [17: assetIdentifier], kCGImageDestinationLossyCompressionQuality: quality ] CGImageDestinationAddImage(dest, image, properties as CFDictionary) guard CGImageDestinationFinalize(dest) else { throw LivePhotoError.writeFailed } } // 写完立刻读回来确认别等相册告诉你 func readAssetIdentifier(from imageURL: URL) - String? { guard let src CGImageSourceCreateWithURL(imageURL as CFURL, nil), let props CGImageSourceCopyPropertiesAtIndex(src, 0, nil) as? [CFString: Any], let maker props[kCGImagePropertyMakerAppleDictionary] as? [String: Any] else { return nil } return maker[17] as? String }quality给 0.9 以上太低会让封面帧出现块状噪点长按播放起点不好看。kCGImagePropertyMakerAppleDictionary的值类型必须是[String: Any]用NSString当值也能过但取的时候记得转回String比较。如果读回来是 nil八成是CGImageDestination的类型参数和文件扩展名不一致或者写的是 JPEG 却给了 HEIC 的 UTI。3.2 视频侧content.identifier 与 still-image-timeMOV 里要写两条 QuickTime 元数据轨它们的标识符都是mdta/开头的全限定名数据类型一个是 UTF-8 字符串一个是 8 位整数。轨道标识符数据类型值作用mdta/com.apple.quicktime.content.identifiercom.apple.metadata.datatype.utf-8与静态图一致的 identifier把视频和静态图绑在一起mdta/com.apple.quicktime.still-image-timecom.apple.metadata.datatype.int80标记这是一张实况图并给出封面帧位置缺第一条相册会把它当普通视频缺第二条相册会把它当普通图片加一段视频。两条都在但 identifier 不一致表现和缺第一条一样这也是最容易被忽略的一种文件明明都生成成功了就是不动。still-image-time 的值写0就行覆盖范围我一般设成从.zero到视频结尾。也见过只在封面帧时刻写一个单样本的实现同样能被识别但在部分系统版本上会被忽略所以覆盖全时长更稳。3.3 用 AVAssetWriter 重封装一段合法的配对视频关键点是不重编码。把原视频轨用outputSettings: nil直通读出来再原样写进去只额外挂两条元数据轨。这样速度快、没有画质损失也不会因为重编码改变帧时间戳导致封面帧偏移。import AVFoundation private func metadataFormatDescription(identifier: String, dataType: String) throws - CMMetadataFormatDescription { let spec: [String: Any] [ kCMMetadataFormatDescriptionMetadataSpecificationKey_Identifier as String: identifier, kCMMetadataFormatDescriptionMetadataSpecificationKey_DataType as String: dataType ] var desc: CMMetadataFormatDescription? let status CMMetadataFormatDescriptionCreateWithMetadataSpecifications( allocator: kCFAllocatorDefault, metadataType: kCMMetadataFormatDescriptionMetadataType, specifications: [spec] as CFArray, formatDescriptionOut: desc) guard status noErr, let result desc else { throw LivePhotoError.metadataFormatFailed } return result } private func metadataItem(key: String, dataType: String, value: NSCopying NSObjectProtocol) - AVMutableMetadataItem { let item AVMutableMetadataItem() item.identifier AVMetadataItem.identifier(forKey: key, keySpace: .quickTimeMetadata) item.dataType dataType item.value value item.extendedLanguageTag und // 不写这一行部分播放器解不出来 return item } func composePairedVideo(source: URL, assetIdentifier: String, output: URL) throws { let asset AVURLAsset(url: source) guard let videoTrack asset.tracks(withMediaType: .video).first, let formatHint videoTrack.formatDescriptions.first as? CMFormatDescription else { throw LivePhotoError.noVideoTrack } let duration asset.duration try? FileManager.default.removeItem(at: output) let reader try AVAssetReader(asset: asset) let trackOutput AVAssetReaderTrackOutput(track: videoTrack, outputSettings: nil) // nil 即直通 reader.add(trackOutput) let writer try AVAssetWriter(outputURL: output, fileType: .mov) let videoInput AVAssetWriterInput(mediaType: .video, outputSettings: nil, sourceFormatDescription: formatHint) videoInput.expectsMediaDataInRealTime false writer.add(videoInput) let idInput AVAssetWriterInput(mediaType: .metadata, outputSettings: nil, sourceFormatDescription: try metadataFormatDescription( identifier: mdta/com.apple.quicktime.content.identifier, dataType: com.apple.metadata.datatype.utf-8)) writer.add(idInput) let idAdaptor AVAssetWriterInputMetadataAdaptor(assetWriter p a hrefhttps://download.csdn.net/download/lyh1083908486/87521908 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询