简介这是一份面向iOS开发者的Swift实况图制作实战demo聚焦Live Photo的合成、展示与预览全流程适合已掌握Swift基础、希望深入Photos框架与图像视频处理的中级开发者参考。资源包共21个文件以swift源码、plist配置、json数据、storyboard界面文件为主另含jpeg静态图与mp4视频素材压缩包约4.37MB目录结构清晰便于按模块对照学习。demo封装了从素材准备、PHLivePhotoContent合成、PHImageManager请求到PHLivePhotoView预览的完整链路并涉及进度监听与动态壁纸扩展思路读者可借此理解实况图三要素JPEG、MOV、元数据的组织方式与苹果Photos框架的调用细节。目前已有1332人学习下载适合需要快速上手实况图功能、提升应用交互体验的开发者参考借鉴。1. 从一张静态照片到会动的实况图这个 Swift Demo 能帮你省下多少试错时间你手里有一张普通的 JPEG 或 HEIC 照片想把它变成 iPhone 相册里那种按压会动、带声音的 Live Photo第一反应可能是去搜“iOS 实况图制作”。搜出来的结果要么是 App Store 里收费的工具要么是零散的 API 说明真正能跑起来的完整代码少之又少。这个swift-livePhoto-实况图制作demo就是冲着这个缺口来的它把「静态图 短视频 → Live Photo」的完整链路用 Swift 写成了一个可运行的工程适合正在做相册类、社交分享类 App 的 iOS 开发者也适合想理解 Live Photo 底层结构的同学。你不需要从 Photos framework 的文档里一点点拼凑demo 已经把关键步骤串好了。2. 先看清技术底座Live Photo 到底是什么结构2.1 一张图加一段视频靠 metadata 绑定Live Photo 在文件层面并不是一个单一格式它由两部分组成一张静态图片通常是 HEIC 或 JPEG和一段短视频MOV通常是 3 秒左右、带音轨。两者通过一个共享的 asset identifier 关联起来。系统相册在展示时会把视频的第一帧作为封面按压时播放视频内容。这个 demo 的核心工作就是拿到用户选中的图片和视频给它们打上相同的标识符然后写入相册。听起来简单但标识符的生成、metadata 的写入位置、视频编码参数都有讲究稍有不慎就会变成“两张独立资源”而不是一个 Live Photo。常见做法是用PHAssetCreationRequest的addResource(with:data:options:)方法分别以.photo和.pairedVideo类型添加两个资源并在options里指定同一个PHAssetResourceCreationOptions的originalFilename和统一的标识符。2.2 为什么不用 UIImageWriteToSavedPhotosAlbum很多新手第一反应是用UIImageWriteToSavedPhotosAlbum把图片存进去再用PHPhotoLibrary存视频结果相册里出现两个独立条目。原因在于这种方式没有建立配对关系。Live Photo 的配对信息必须通过PHAssetCreationRequest在创建阶段就写入事后无法补加。demo 里用的是PHPhotoLibrary.shared().performChanges闭包内创建PHAssetCreationRequest这是目前 iOS 上唯一可靠的 Live Photo 写入方式。下面这段代码展示了核心的创建逻辑// 创建 Live Photo 的核心写入逻辑 PHPhotoLibrary.shared().performChanges({ // 创建资源创建请求 let creationRequest PHAssetCreationRequest.forAsset() // 配置图片资源选项 let photoOptions PHAssetResourceCreationOptions() photoOptions.originalFilename live_photo_cover.heic // 添加静态图片资源类型为 .photo creationRequest.addResource(with: .photo, data: imageData, options: photoOptions) // 配置视频资源选项使用相同的标识符 let videoOptions PHAssetResourceCreationOptions() videoOptions.originalFilename live_photo_motion.mov // 添加配对视频资源类型为 .pairedVideo creationRequest.addResource(with: .pairedVideo, data: videoData, options: videoOptions) }) { success, error in if success { print(Live Photo 已成功写入相册) } else { print(写入失败: \(error?.localizedDescription ?? 未知错误)) } }这段代码的关键点在于两个addResource调用必须发生在同一个PHAssetCreationRequest实例上并且.photo和.pairedVideo的顺序不能颠倒。originalFilename虽然可以自定义但扩展名要和实际数据格式匹配否则系统可能拒绝识别。imageData和videoData分别是从原图导出的 HEIC/JPEG 数据和从视频文件读取的 Data。2.3 视频参数的门槛为什么你的 MOV 不被识别不是随便一段 MOV 都能当 Live Photo 的配对视频。系统对视频有隐性要求时长通常在 3 秒以内编码用 H.264 或 HEVC分辨率建议和图片一致或接近。如果视频太长或编码不对写入可能成功但按压无反应。demo 里对视频做了预处理用AVAssetExportSession重新导出为兼容格式。这一步在实际项目中很容易被忽略导致“代码没报错但效果不对”的玄学问题。我一般会在导出时把outputFileType设为.movvideoSettings里指定AVVideoCodecKey为AVVideoCodecType.h264并限制时长。3. 跑起来再说从 Xcode 配置到真机验证的完整路径3.1 工程结构与依赖检查拿到 demo 后先看目录结构。典型的 Swift Live Photo demo 会包含这几个部分AppDelegate.swift或SceneDelegate.swift负责生命周期ViewController.swift承载 UI 和交互可能还有一个LivePhotoMaker.swift或类似命名的工具类封装核心逻辑。资源文件夹里会有测试用的图片和视频。打开.xcodeproj后第一件事是检查Info.plist里有没有加相册权限描述。iOS 14 之后必须声明NSPhotoLibraryAddUsageDescription否则写入时会直接崩溃。demo 通常已经配好但如果你要集成到自己的项目这一步不能漏。!-- Info.plist 中必须添加的权限声明 -- keyNSPhotoLibraryAddUsageDescription/key string需要将制作好的实况图保存到您的相册/string keyNSPhotoLibraryUsageDescription/key string需要访问相册以选择图片和视频/stringNSPhotoLibraryAddUsageDescription是写入权限NSPhotoLibraryUsageDescription是读取权限。只加后者不加前者在保存时会触发PHPhotoLibrary的授权失败。两个都加上最稳妥。3.2 选图、选视频、生成、保存四步操作链路demo 的交互流程一般是这样界面上有两个按钮一个选图片一个选视频选完后点“生成实况图”最后保存到相册。选图用UIImagePickerController或PHPickerViewController后者是 iOS 14 之后推荐的方式不需要额外权限就能拿到用户选中的资源。// 使用 PHPickerViewController 选择图片和视频 func presentPicker(for type: PickType) { var config PHPickerConfiguration() // 根据选择类型过滤图片或视频 config.filter (type .image) ? .images : .videos config.selectionLimit 1 let picker PHPickerViewController(configuration: config) picker.delegate self present(picker, animated: true) } // PHPickerViewControllerDelegate 回调 func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) guard let result results.first else { return } // 通过 itemProvider 加载数据 let provider result.itemProvider if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { // 加载图片数据 provider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, error in // 处理图片数据 } } else if provider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { // 加载视频数据 provider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in // 处理视频文件 } } }PHPickerConfiguration的filter属性决定用户能选什么类型.images和.videos是互斥的所以 demo 里通常做两次选择。loadDataRepresentation拿到的是原始数据对于图片可以直接用对于视频loadFileRepresentation给的是一个临时 URL需要在这个回调结束前把文件拷贝到自己的临时目录否则 URL 会失效。3.3 真机验证模拟器为什么不行Live Photo 的写入和播放依赖相册 App 和系统底层能力iOS 模拟器虽然能跑代码但相册里不会正确显示 Live Photo 的按压效果。必须用真机测试。连接 iPhone 后在 Xcode 里选择设备运行授权相册权限走一遍完整流程。验证时注意看相册里的条目如果左上角有 LIVE 标识说明配对成功如果只是普通照片说明视频资源没被识别为.pairedVideo。按压或长按时应该能看到动态效果如果只有静态图回去检查视频编码和时长。提示第一次运行时如果保存失败先看控制台有没有PHPhotosError相关日志常见的是权限未授权或视频格式不兼容。4. 核心模块拆解图片处理、视频导出与元数据写入4.1 图片侧HEIC 与 JPEG 的选择Live Photo 的封面图推荐用 HEIC因为它在同等画质下体积更小而且 iOS 原生支持好。但如果你的图片源是 JPEG也可以直接用系统不会拒绝。demo 里如果是从UIImage转 Data要注意压缩质量参数。// 将 UIImage 转为 HEIC 或 JPEG 数据 func imageData(from image: UIImage, preferHEIC: Bool true) - Data? { if preferHEIC, let heicData image.heicData() { return heicData } // 回退到 JPEG压缩质量 0.9 return image.jpegData(compressionQuality: 0.9) }heicData()是 iOS 11 之后UIImage提供的方法但不是所有图片都能转成功所以要有 JPEG 回退。压缩质量 0.9 是平衡画质和体积的常用值再低会出现可见压缩痕迹。4.2 视频侧用 AVAssetExportSession 统一格式前面提到视频需要预处理核心是用AVAssetExportSession重新导出。demo 里通常会封装一个方法接收原始视频 URL输出符合 Live Photo 要求的 MOV 文件。// 将视频重新导出为 Live Photo 兼容格式 func exportVideoForLivePhoto(inputURL: URL, completion: escaping (URL?) - Void) { let asset AVAsset(url: inputURL) // 使用中等质量预设兼顾体积和兼容性 guard let exportSession AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetMediumQuality) else { completion(nil) return } // 输出到临时目录 let outputURL FileManager.default.temporaryDirectory .appendingPathComponent(live_photo_\(UUID().uuidString).mov) exportSession.outputURL outputURL exportSession.outputFileType .mov // 限制时长Live Photo 通常不超过 3 秒 exportSession.timeRange CMTimeRange(start: .zero, duration: CMTime(seconds: 3, preferredTimescale: 600)) exportSession.exportAsynchronously { DispatchQueue.main.async { if exportSession.status .completed { completion(outputURL) } else { completion(nil) } } } }AVAssetExportPresetMediumQuality是系统预设能保证 H.264 编码和合理的码率。timeRange截取前 3 秒避免视频过长导致系统不识别。导出完成后outputURL指向临时文件读取成 Data 后传给addResource。4.3 元数据写入标识符与配对逻辑虽然PHAssetCreationRequest会自动处理配对但有些场景下需要手动指定assetIdentifier。比如你想让图片和视频在文件层面就带有关联信息可以在写入前给视频的 metadata 里塞一个标识符。// 为视频添加 Live Photo 标识符可选增强兼容性 func addAssetIdentifier(to videoURL: URL, identifier: String) - URL? { let asset AVAsset(url: videoURL) guard let exportSession AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough) else { return nil } let outputURL FileManager.default.temporaryDirectory .appendingPathComponent(tagged_\(UUID().uuidString).mov) exportSession.outputURL outputURL exportSession.outputFileType .mov // 构建 metadata 项 let metadataItem AVMutableMetadataItem() metadataItem.key com.apple.quicktime.content.identifier as NSString metadataItem.keySpace .quickTimeMetadata metadataItem.value identifier as NSString exportSession.metadata [metadataItem] exportSession.exportAsynchronously { // 导出完成后返回新 URL } return outputURL }com.apple.quicktime.content.identifier是 QuickTime 元数据里用于 Live Photo 配对的键。AVAssetExportPresetPassthrough表示不重新编码只改元数据速度快但要求原视频格式已经兼容。如果原视频编码不对还是得用前面的MediumQuality预设重新编码。5. 避坑与排查那些让 Live Photo 变“死图”的常见问题5.1 保存成功但相册里没有 LIVE 标识现象代码回调success true相册里也能看到图片但左上角没有 LIVE 标记按压无反应。原因视频资源没有被识别为.pairedVideo或者图片和视频的标识符不一致。常见于手动拼接 Data 时顺序错误或者视频时长超过系统限制。解决确认addResource调用顺序是.photo在前、.pairedVideo在后检查视频时长是否超过 3 秒用AVAsset读取视频的duration属性验证。5.2 模拟器上正常真机上写入失败现象Xcode 模拟器跑流程没问题换到真机后performChanges报错。原因真机的相册权限更严格或者Info.plist里缺少NSPhotoLibraryAddUsageDescription。模拟器有时会放宽权限检查导致问题被掩盖。解决在真机设置里手动检查 App 的相册权限确认Info.plist两个权限键都有如果用户之前拒绝过权限需要引导去设置里重新开启。5.3 视频导出后没有声音现象Live Photo 按压时有画面但没声音。原因AVAssetExportSession的预设或audioSettings配置不对导致音轨被丢弃。或者原视频本身就没有音轨。解决用AVAssetExportPresetMediumQuality或HighestQuality预设它们默认保留音轨检查原视频的tracks(withMediaType: .audio)是否为空。5.4 图片和视频分辨率不匹配导致画面跳动现象按压时画面从封面图切换到视频第一帧出现明显跳变。原因图片分辨率和视频分辨率差异过大系统在切换时做了缩放。解决在导出视频时用AVMutableVideoComposition把视频渲染到和图片相同的尺寸或者选图时就把图片裁剪到视频的分辨率比例。5.5 多次保存后相册出现重复条目现象连续点保存按钮相册里出现多个相同的 Live Photo。原因没有做防重复提交或者每次保存都生成了新的标识符。解决在保存前禁用按钮保存完成后根据结果决定是否恢复如果需要覆盖先删除旧的PHAsset再写入新的。6. 二次开发把 Demo 改成你项目里能用的模块6.1 封装成独立工具类demo 里的代码通常散落在 ViewController 里实际项目要抽成LivePhotoMaker这样的工具类。对外暴露一个方法接收图片 Data 和视频 URL内部完成导出、配对、写入用闭包返回结果。// 封装后的调用方式 LivePhotoMaker.shared.make(from: imageData, videoURL: videoURL) { result in switch result { case .success(let assetIdentifier): print(生成成功: \(assetIdentifier)) case .failure(let error): print(生成失败: \(error)) } }这样你的业务层不需要关心PHAssetCreationRequest的细节也方便做单元测试和替换实现。6.2 支持从视频提取封面帧如果用户只选了一段视频想直接生成 Live Photo可以从视频里截取第一帧作为封面图。用AVAssetImageGenerator实现// 从视频提取第一帧作为封面 func extractFirstFrame(from videoURL: URL) - UIImage? { let asset AVAsset(url: videoURL) let generator AVAssetImageGenerator(asset: asset) generator.appliesPreferredTrackTransform true generator.requestedTimeToleranceBefore .zero generator.requestedTimeToleranceAfter .zero let time CMTime(seconds: 0, preferredTimescale: 600) guard let cgImage try? generator.copyCGImage(at: time, actualTime: nil) else { return nil } return UIImage(cgImage: cgImage) }appliesPreferredTrackTransform设为true能自动处理视频旋转避免封面图方向不对。requestedTimeTolerance设为.zero保证取到精确的第一帧。6.3 批量生成时的性能注意点如果要一次生成多个 Live Photo不要在循环里同步调用performChanges。PHPhotoLibrary的变更操作应该串行执行或者用performChangesAndWait在后台队列逐个处理。同时注意内存视频 Data 可能很大及时释放临时文件。改造点原 demo 做法生产环境建议代码组织逻辑在 ViewController抽成独立工具类视频处理直接使用原视频统一导出为 H.264 MOV错误处理print 日志定义错误类型回调给业务层权限申请使用时才申请启动时预申请引导用户授权临时文件不清理每次生成后删除临时文件6.4 一个我踩过的坑标识符不要用 UUID 字符串直接塞早期我图省事用UUID().uuidString生成标识符分别塞给图片和视频的 metadata。结果发现系统并不认这个标识符Live Photo 还是配不上。后来才明白PHAssetCreationRequest内部会自己管理配对关系手动塞的标识符只在某些跨 App 传递场景下有用写入相册时反而不需要。从那以后我每次做 Live Photo 写入都强制走一遍PHAssetCreationRequest的.photo.pairedVideo组合不再手动干预标识符。希望这个 demo 能帮你少走一段弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?