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

Swift中使用ContextMenu保存图片至相机胶卷出现黑图问题

问题

我正在尝试用ContextMenu实现将图片保存到相机胶卷的功能,当前显示的图片是SwiftUI的Image类型,所以写了个转UIImage的函数,再用UIImageWriteToSavedPhotosAlbum()保存。代码能运行,但保存后的图片是全黑的,不是实际内容。

转换代码:

extension Image {
    func asUIImage() -> UIImage? {
        // Create a UIView
        let uiView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        
        // Create a hosting controller with the SwiftUI Image
        let hostingController = UIHostingController(rootView: self)
        
        // Assign the frame to the hosting controller
        hostingController.view.frame = uiView.bounds
        
        // Add the hosting controller's view to the UIView
        uiView.addSubview(hostingController.view)
        
        // Make the snapshot of the hosting controller's view
        UIGraphicsBeginImageContextWithOptions(uiView.bounds.size, uiView.isOpaque, 0.0)
        defer { UIGraphicsEndImageContext() }
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        uiView.layer.render(in: context)
        let uiImage = UIGraphicsGetImageFromCurrentImageContext()
        
        return uiImage
    }
}

核心调用代码:

Button(action: {
    // Action to save the photo
    if let uiImage = image.asUIImage() {
        UIGraphicsBeginImageContextWithOptions(uiView.bounds.size, uiView.isOpaque, 0.0)
        defer { UIGraphicsEndImageContext() }
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        uiView.layer.render(in: context)
        let uiImage = UIGraphicsGetImageFromCurrentImageContext()
        
        return uiImage
    }
}) {
    Text("Save Photo")
    Image(systemName: "square.and.arrow.down")
}

解决方案

你的转换方法存在两个核心问题:

  • HostingController的View未完成渲染:直接添加View后就截图,SwiftUI的Image还没来得及绘制内容,导致截图全黑。
  • 固定尺寸不匹配原图片:硬编码100x100的尺寸,既浪费原图片分辨率,也会导致布局错位。

推荐两种修复方案:

方案一:直接获取AsyncImage的原始UIImage(最优)

跳过SwiftUI Image转截图的步骤,直接从AsyncImage的成功回调中提取原始下载的UIImage,避免渲染问题:

  1. 新增扩展方法提取原始UIImage:
extension Image {
    // 仅适用于由UIImage初始化的SwiftUI Image(比如AsyncImage返回的成功结果)
    func getOriginalUIImage() -> UIImage? {
        guard let imageProvider = self.imageProvider else { return nil }
        return (imageProvider as? UIImageProvider)?.uiImage
    }
}
  1. 修改AsyncImage的调用逻辑:
AsyncImage(url: media[index].link) { phase in
    switch phase {
    case .success(let image):
        // 提前获取原始UIImage
        let originalImage = image.getOriginalUIImage()
        image
            .resizable()
            .aspectRatio(contentMode: .fill)
            .contentShape(Rectangle())
            .onTapGesture {
                selectedImageIndex = index
                showFullImage = true
            }
            .contextMenu {
                // ...其他按钮
                Button(action: {
                    if let uiImage = originalImage {
                        UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil)
                        print("Saved successfully!")
                    }
                }) {
                    Text("Save Photo")
                    Image(systemName: "square.and.arrow.down")
                }
            }
    // ...failure、empty分支逻辑不变
    }
}

方案二:修复截图渲染逻辑(仅当必须用SwiftUI Image转截图时)

如果因为业务限制必须用截图方式,需要强制View完成布局渲染,同时使用正确的尺寸:

extension Image {
    func asUIImage(targetSize: CGSize) -> UIImage? {
        let containerView = UIView(frame: CGRect(origin: .zero, size: targetSize))
        let hostingController = UIHostingController(rootView: self)
        
        hostingController.view.frame = containerView.bounds
        containerView.addSubview(hostingController.view)
        
        // 强制触发布局和渲染
        hostingController.view.layoutIfNeeded()
        containerView.layoutIfNeeded()
        
        // 使用更可靠的UIGraphicsImageRenderer截图
        let renderer = UIGraphicsImageRenderer(size: targetSize)
        return renderer.image { context in
            containerView.drawHierarchy(in: containerView.bounds, afterScreenUpdates: true)
        }
    }
}

调用时传入实际显示的尺寸(比如从GeometryReader中获取):

// 在GeometryReader的闭包内调用
if let uiImage = image.asUIImage(targetSize: geo.size) {
    UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil)
}

额外注意事项

  • 务必在Info.plist中添加NSPhotoLibraryAddUsageDescription字段,填写相机胶卷访问权限的说明,否则保存操作会静默失败。
  • 方案一的质量和性能更优,直接使用原始图片,避免截图带来的分辨率损失和渲染延迟。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 19:22:49