You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将生成的GIF保存至相册?仅首次保存成功问题排查

解决GIF首次保存相册成功、后续失败(PHPhotosErrorDomain error -1)的问题

问题根源

  1. 异步时序错误:generateGifFromImages方法是异步执行GIF生成逻辑,但按钮点击代码中调用完该方法后立刻触发相册保存操作,此时GIF文件可能还未完全写入磁盘,导致相册读取失败。
  2. 参数未生效:generateGifFromImages方法未使用传入的colorSpace参数,始终生成灰度图,不符合预期。
  3. 强制解包风险:原代码中使用try!强制解包,可能导致未知崩溃。

解决方案

1. 修改GIF生成方法,添加完成回调

调整GIFFromImages的generateGifFromImages方法,添加completion闭包通知生成完成,同时修复colorSpace参数未使用的问题:

import UIKit
import MobileCoreServices

public class GIFFromImages {
    public enum colorSpace {
        case rgb
        case gray
    }
    public init () {}
}

extension GIFFromImages {
    public func makeFileURL(filename: String) -> URL {
        let gifDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let gifFileURL = gifDirectory.appendingPathComponent(filename)
        return gifFileURL
    }
    
    // 添加completion回调,通知GIF生成状态
    public func generateGifFromImages(images: [UIImage], fileURL: URL, colorSpace: colorSpace, delayTime: Double, loopCount: Int, completion: @escaping (Bool) -> Void) {
        let gifGroup = DispatchGroup()
        var tempImages: [UIImage] = []
        
        for image in images {
            gifGroup.enter()
            let imageWidth = UIScreen.main.bounds.width
            let imageHeight = UIScreen.main.bounds.height
            let imageRect = CGRect(x:0, y:0, width: imageWidth, height: imageHeight)
            let imageBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
            
            // 根据传入参数选择颜色空间
            let cgColorSpace: CGColorSpace
            switch colorSpace {
            case .rgb:
                cgColorSpace = CGColorSpaceCreateDeviceRGB()
            case .gray:
                cgColorSpace = CGColorSpaceCreateDeviceGray()
            }
            
            let context = CGContext(data: nil, width: Int(imageWidth), height: Int(imageHeight), bitsPerComponent: 8, bytesPerRow: 0, space: cgColorSpace, bitmapInfo: imageBitmapInfo.rawValue)
            
            if let cgImg = image.cgImage {
                context?.setFillColor(UIColor.white.cgColor)
                context?.fill(imageRect)
                context?.draw(cgImg, in: imageRect)
                
                if let makeImg = context?.makeImage() {
                    tempImages.append(UIImage(cgImage: makeImg))
                }
            }
            gifGroup.leave()
        }
        
        gifGroup.notify(queue: .main) {
            let gifFileProperties: CFDictionary = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: loopCount]] as CFDictionary
            let gifFrameProperties: CFDictionary = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: delayTime]] as CFDictionary
            var isSuccess = false
            
            if let url = fileURL as CFURL?, let destination = CGImageDestinationCreateWithURL(url, kUTTypeGIF, tempImages.count, nil) {
                CGImageDestinationSetProperties(destination, gifFileProperties)
                for image in tempImages {
                    if let cgImage = image.cgImage {
                        CGImageDestinationAddImage(destination, cgImage, gifFrameProperties)
                    }
                }
                isSuccess = CGImageDestinationFinalize(destination)
                if !isSuccess {
                    print("Failed to finalize the image destination")
                }
            }
            completion(isSuccess)
        }
    }
}

2. 调整按钮点击逻辑,确保生成完成后再保存相册

将相册保存逻辑放到GIF生成完成的回调中,同时修复强制解包问题:

Button {
    print("Saving animation titled: \(animation.title)")
    
    var images: [UIImage] = []
    for frame in animation.frames {
        do {
            let image = try PKDrawing(data: frame.frameData).generateThumbnail(scale: 1)
            images.append(image)
        } catch {
            print("Failed to generate thumbnail: \(error.localizedDescription)")
            return
        }
    }
    
    let timestamp = Date().timeIntervalSince1970
    let timestampString = String(format: "%.0f", timestamp)
    let gifURL = gifManager.makeFileURL(filename: "\(timestampString).gif")
    
    // 等待GIF生成完成后再执行保存
    gifManager.generateGifFromImages(images: images, fileURL: gifURL, colorSpace: .rgb, delayTime: (1.0/Double(animation.framesPerSecond)), loopCount: 0) { [weak self] success in
        guard success else {
            print("Failed to generate GIF file")
            return
        }
        
        guard FileManager.default.fileExists(atPath: gifURL.path) else {
            print("Error: GIF file does not exist at:", gifURL.path)
            return
        }
        print("GIF file exists at:", gifURL.path)
        
        // 保存到相册
        PHPhotoLibrary.shared().performChanges({
            // 优先使用Data方式创建资源,避免URL读取时机问题
            if let gifData = try? Data(contentsOf: gifURL) {
                let creationRequest = PHAssetCreationRequest.forAsset()
                creationRequest.addResource(with: .photo, data: gifData, options: nil)
            } else {
                PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: gifURL)
            }
        }) { success, error in
            if success {
                print("GIF saved successfully.")
            } else {
                print("Error saving GIF:", error?.localizedDescription ?? "Unknown error")
            }
            self?.close()
        }
    }
    
} label: {
    ZStack {
        Rectangle()
            .frame(width: 400, height: 100)
            .foregroundColor(.accentColor)
            .cornerRadius(30)
        Text("Save GIF to Photos")
            .foregroundStyle(.white)
            .font(.title)
            .bold()
    } 
}

关键说明

  • 通过completion回调确保GIF完全写入磁盘后再执行保存,解决异步时序问题。
  • 修复colorSpace参数,生成符合预期的颜色空间GIF。
  • 替换try!为do-catch,避免崩溃风险。
  • 提供两种相册保存方式,优先使用Data方式避免URL读取权限或时机问题。

内容的提问来源于stack exchange,提问作者Zoe Sosa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 05:54:57