简介面向iOS开发者的Swift实况图Live Photo制作demo围绕PHLivePhoto对象的生成、处理与预览系统演示从JPEG静态图与MOV视频素材合成动态实况图的完整流程可帮助解决实况图功能集成中的常见问题。工程包共21个文件压缩后大小4.37MB包含Swift源码、plist配置、JSON数据以及Xcode工程配置等主要内容并附有可用的jpeg和mp4测试素材便于在工程中直接运行验证。已有1323人学习下载demo将PHLivePhotoContent创建、PHLivePhotoRequestOptions参数设置、PHImageManager请求等步骤封装成清晰案例配合storyboard界面和swift源码可快速理解各环节调用关系。通过运行这个demo开发者既能掌握实况图制作的编码要点也能在此基础上扩展出iOS动态壁纸等交互玩法为照片类App增加动态展示能力。 最近我把一个做了一阵子的Live Photo实况照片生成需求整理成了独立Demo技术栈很常规Swift加系统自带的Photos、AVFoundation框架没有引入任何第三方依赖。Demo要解决的核心问题很直接把一张静态图片和一段视频素材在App内通过代码合成为Live Photo并写入系统相册同时支持从相册已有的Live Photo里反向提取图片和视频素材方便做二次加工或重新合成。如果你需要在iOS上做实况图编辑、动态壁纸、社交分享或者做一个自定义拍摄的相机App这份代码可以直接拿去改。写这篇博文时我会把素材格式要求、权限处理、资源关联方式、编码注意点这些文档里不细说的地方一并讲清楚希望能帮你少踩几个坑。1. 这个Demo解决的是什么问题1.1 Live Photo的底层原理比你想的简单用户拍摄实况照片时系统会在按下快门前后各记录约1.5秒形成一段3秒左右的视频同时保留一张静态主图。所以Live Photo本质上就是“1张图片 1段视频”的资源组合。系统相册展示时默认显示那张静态图用户长按时播放视频并同步放声音。这个原理意味着只要我们能提供一张合适的图片和一段合适的视频系统并不关心它们是不是相机拍的照样能打成一个Live Photo。Demo做的事情就是往系统里喂素材让系统帮我们打包。这也是整个需求能落地的理论基础。1.2 Demo的边界能做的和不能做的开始之前先明确边界避免大家被网上一些旧方案带偏。这个Demo负责的是把任意源素材转成系统要求的Live Photo格式、保存到系统相册并确保能长按播放、从已有Live Photo里反向导出素材。不负责的是拍照和滤镜、音频混剪这些可以当作后续扩展方向也不兼容Android安卓那边虽然有Motion Photo但是另一套技术体系。还有一点必须提前说模拟器上无法完整验证Live Photo的展示效果。代码能跑、能保存但照片App在模拟器上通常会把Live Photo显示成普通图片所以要测试效果务必准备一台真机。2. 策划与素材准备动手写代码前想清楚几件事2.1 关键规则图片和视频是怎么“配对”的写代码之前先把素材规则理清。Live Photo的两个文件之间并没有强制要求文件名一致系统判断一段资源是不是Live Photo关键看两个点保存时是否在同一个PHAssetCreationRequest里用了正确的资源类型以及photo资源是否设置了Live Photo对应的UTType标识符也就是com.apple.live-photo。实际开发时很多人在这里栽跟头。如果保存视频时把资源类型写成了普通的PHAssetResourceType.video而不是.pairedVideo系统会认为你要保存两个独立资源最终相册里是一个普通图片加一个普通视频完全不是Live Photo。这个细节后面代码里会重点标出。2.2 素材格式与规格表我把自己在测试中比较稳妥的素材规格整理成了表照着准备基本不会出问题。项目推荐规格说明静态图格式JPEG / HEIC不要用PNG个别系统版本会出现关联异常视频容器.mov / .mp4建议用系统相机同款MOV视频编码H.264 / HEVC不要用带Alpha通道的HEVC变种视频时长3秒左右前后各1.5秒是最接近真实Live Photo的参数视频尺寸与图片一致不一致虽然能保存但播放画面会裁切或留边文件名建议前缀相同如IMG_1234.jpg、IMG_1234.mov便于排查视频尺寸如果和图片不一致系统不会报错但长按播放时可能看到画面缩放、位移等奇怪观感。Demo里我统一把视频分辨率压到和图片一致。2.3 没有现成视频从单张图片生成3秒视频很多时候我们手上只有一张静态图需要先把它变成一段视频才能继续合成Live Photo。我用AVAssetWriter做一个简化的生成器把UIImage转成CVPixelBuffer以30fps写入90帧就得到一段3秒视频。func makeStillImageVideo(from image: UIImage, outputURL: URL, duration: TimeInterval 3.0, fps: Int32 30) { try? FileManager.default.removeItem(at: outputURL) let writer try! AVAssetWriter(outputURL: outputURL, fileType: .mov) let width image.size.width let height image.size.height let input AVAssetWriterInput( mediaType: .video, outputSettings: [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: Int(width), AVVideoHeightKey: Int(height) ]) let adaptor AVAssetWriterInputPixelBufferAdaptor( assetWriterInput: input, sourcePixelBufferAttributes: [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA, kCVPixelBufferWidthKey as String: Int(width), kCVPixelBufferHeightKey as String: Int(height) ]) writer.add(input) writer.startWriting() writer.startSession(atSourceTime: .zero) input.requestMediaDataWhenReady(on: .global(qos: .userInitiated)) { let totalFrames Int(duration * Double(fps)) for index in 0..totalFrames { while !input.isReadyForMoreMediaData { usleep(1000) } let time CMTime(value: CMTimeValue(index), timescale: fps) if let buffer pixelBufferFromImage(image) { adaptor.append(buffer, withPresentationTime: time) } } input.markAsFinished() writer.finishWriting { } } }pixelBufferFromImage是一个把UIImage转换成CVPixelBuffer的辅助函数实现思路是创建CVPixelBuffer后拿到对应的CGContext再把image的CGImage画进去。这个函数建议单独抽出来做复用后面处理任意图片素材都用得上。为什么这里选H.264而不是HEVC主要考虑兼容性。JPEG图加H.264视频在旧设备和第三方播放器上表现最稳定HEVC保存的视频在iOS 11以上能正常播放但在一些老机型上解码慢转码过程也更容易遇到问题。Demo场景优先求稳所以选了H.264。3. 核心实现把素材保存为Live Photo并写入相册3.1 权限申请addOnly就够了写入相册之前需要申请权限。从iOS 14开始建议用requestAuthorization(for: .addOnly)来申请“仅添加”权限。这个模式的弹窗文案对用户更友好而且不会触发“完整照片授权”那种敏感要求。PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in if status .authorized || status .limited { // 可以保存 } else { // 提示用户去设置页开启权限 } }limited是iOS 14里“允许部分照片”的授权状态。对addOnly场景来说只要不是denied或restricted一般就能正常写入相册。实际处理时我会把denied和restricted统一归为“无权限”引导用户去系统设置里调整。3.2 核心保存代码PHAssetCreationRequest的完整用法这是整个Demo最关键的方法。它把图片和视频在同一个事务里写入系统相册并标记为Live Photo。func saveAsLivePhoto(videoURL: URL, imageURL: URL, completion: escaping (Bool, Error?) - Void) { PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in guard status .authorized || status .limited else { DispatchQueue.main.async { completion(false, nil) } return } PHPhotoLibrary.shared().performChanges { let request PHAssetCreationRequest.forAsset() let imageOptions PHAssetResourceCreationOptions() imageOptions.uniformTypeIdentifier UTType.livePhoto.identifier request.addResource(with: .photo, fileURL: imageURL, options: imageOptions) let videoOptions PHAssetResourceCreationOptions() videoOptions.uniformTypeIdentifier UTType.livePhoto.identifier request.addResource(with: .pairedVideo, fileURL: videoURL, options: videoOptions) } completionHandler: { success, error in DispatchQueue.main.async { completion(success, error) } } } }这里有三个必须注意的地方。第一两个资源必须在同一个performChanges闭包里添加否则系统无法把图片和视频关联成一对。第二资源类型必须是PHAssetResourceType.photo和PHAssetResourceType.pairedVideo写成.video就错了。第三uniformTypeIdentifier要显式设置为UTType.livePhoto.identifier系统靠它识别这是Live Photo资源。如果这里不设置或设置成public.jpeg结果就是普通照片加普通视频。3.3 从已有Live Photo中反向提取素材App里经常有这种需求用户从相册选一张既有Live Photo然后提取里面的图片和视频来做编辑。实现思路是拿到PHAsset后先判断是不是Live Photo再通过内容编辑输入获取文件URL。func extractLivePhotoAssets(from asset: PHAsset, completion: escaping (URL?, URL?) - Void) { guard asset.mediaSubtypes.contains(.photoLive) else { completion(nil, nil) return } let options PHContentEditingInputRequestOptions() asset.requestContentEditingInput(with: options) { input, _ in guard let input input else { completion(nil, nil) return } let imageURL input.fullSizeImageURL let videoURL (input.audiovisualAsset as? AVURLAsset)?.url completion(imageURL, videoURL) } }fullSizeImageURL拿到的是系统从原始资源解码出来的图片文件audiovisualAsset在Live Photo场景下对应配对的视频。如果想拿到二进制层面更接近原始文件的资源可以改用PHAssetResourceManager按PHAssetResourceType.pairedVideo逐个导出两种方式我用下来都能跑通前者代码量更少后者可控性更强。3.4 跑通Demo的完整流程按我上面的代码整个流程是这样的准备一张JPEG图片放到App的沙盒tmp目录或bundle里。调用makeStillImageVideo从图片生成3秒视频或者从相册选一段现成视频素材。调用saveAsLivePhoto等待保存完成的回调。打开系统相册找到新生成的照片长按播放确认有动画和声音。如果相册里出现了两个独立文件优先检查资源类型是否写成了.video。这个流程跑通一次之后后面加滤镜、压缩、多图连拍转Live Photo都只是在此基础上叠加功能。4. 常见问题与避坑实录4.1 高频问题速查表开发过程中我遇到过不少幺蛾子整理成表格方便大家定位。现象可能原因解决办法保存成功但相册里是静态照片模拟器不支持展示资源类型写错换真机测试改用.pairedVideo相册里出现图片和视频两个文件视频资源类型用了.video改成.pairedVideo长按播放只有画面没有声音视频素材本身没有音轨合成视频时追加一段音频轨相册打开闪退或缩略图黑屏主图用了PNG视频编码用了带Alpha的HEVC换成JPEG/HEIC H.264视频播放一帧后卡住视频时长太短或关键帧间隔太长时长保持在1秒以上3秒最佳保存后视频方向不对原视频带旋转元数据用AVAssetExportSession重压一遍再保存4.2 三个印象深刻的坑第一个坑是资源类型。我第一次写这段代码时视频部分用的是.video保存完全不报错但相册里就是两个独立文件。后来查系统日志才发现PHAssetResourceType里有一个专门的pairedVideo类型。这个坑特别隐蔽因为没有任何报错提示。第二个坑是模拟器。模拟器上保存代码不会失败但照片App展示Live Photo依赖真机的解码链路模拟器几乎都会显示成普通照片。不少新手在这里浪费时间所以一定要从一开始就在真机上测试。第三个坑是视频方向。用相册视频做素材时如果视频带旋转元数据保存后长按播放可能方向不对。解决办法是先重压一遍把视频统一转成H.264并修正方向再进入保存流程。5. 这套能力可以放在什么业务里5.1 社交与内容社区现在不少社交App支持发送实况照片。如果是用户自己拍的Live Photo可以直接走系统分享面板如果用户只有普通视频可以在端上用这套方案先把视频转成Live Photo再进入发布链路动态内容的视觉冲击力比普通图片强很多。5.2 相机与影像工具相机类App可以拍完照后立刻把连拍关键帧与视频配对成Live Photo。图片编辑工具也可以让用户在加完滤镜后重新和原视频配对输出一张滤镜版的Live Photo。这类功能用户感知很强是很好的差异化卖点。5.3 保险、租赁等商用留证场景一些需要现场留证或确认“拍摄时设备状态”的场景也可以引导用户拍摄Live Photo再在端上提取视频帧和音频信息作为辅助材料。相比普通照片这类动态素材能多记录一点环境信息对后续审核和存档都有帮助。5.4 后续扩展方向这个Demo本身只是基础链路。按我自己的经验后续可以扩展的方向包括视频先压缩再入相册、自动从视频里挑选关键帧做主图、连拍多图转Live Photo、配合Core Image做实时滤镜等。核心的PHAssetCreationRequest链路稳定后扩展功能基本就是往上堆。最后再说两句大实话。Live Photo在iOS开发里算是偏小众的功能苹果文档虽然完整但没有一个开箱即用的官方示例可以参考很多细节都是试出来的。我把这套Demo整理出来后团队里新同学从零跑通大概用了不到半天。如果你手头正好要做类似功能建议先别纠结滤镜、压缩这些附加能力把PHAssetCreationRequest那一段吃透核心链路一旦稳定后面加什么都顺手。本文还有配套的精品资源点击获取