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

SwiftUI视图保存为图片时内容缩小且出现异常黑背景问题

问题描述

视图在iPhone上显示时无黑色背景,但保存为图片后内容被缩小,上方出现黑色背景。已固定视图宽高,但问题仍存在。

显示效果

  • iPhone上的显示效果:iPhone上的视图显示效果
  • 导出为图片后的效果:导出后的图片效果

完整代码

import Foundation
import Photos
import SwiftUI
import UIKit
struct MyImageView: View {
    @State private var image: UIImage?
    @State var viewWidth = 0.0
    @State var viewHeight = 0.0
    @ObservedObject var imageSaver = ImageSaver()

    var imageUrl = "https://pic.netbian.com/uploads/allimg/160709/215802-1468072682c2c8.jpg"
    
    func calculateSize() {
        let maxWidth = UIScreen.main.bounds.width
        let maxHeight = UIScreen.main.bounds.height - 100
        if maxHeight/maxWidth >= image!.size.height/image!.size.width {
            viewWidth = maxWidth
            viewHeight = maxWidth * image!.size.height/image!.size.width
        } else {
            viewHeight = maxHeight
            viewWidth = maxHeight/(image!.size.height/image!.size.width)
        }
    }

    private func loadImage() {
        URLSession.shared.dataTask(with: URL(string: imageUrl)!) { data, _, error in
            guard let data = data, error == nil else {
                print("Error loading image: \(error?.localizedDescription ?? "Unknown error")")
                return
            }

            if let uiImage = UIImage(data: data) {
                DispatchQueue.main.async {
                    self.image = uiImage
                    calculateSize()
                }
            } else {
                print("Failed to create image from data")
            }
        }.resume()
    }

    var myView: some View {
        ZStack {
            if self.image != nil {
                Image(uiImage: self.image!)
                    .resizable()
                    .frame(width: viewWidth, height: viewHeight)
            }
            Text("hello girl").font(.title).foregroundColor(.red)
        }.frame(width: viewWidth, height: viewHeight)
            .background(Color.black)
            .onAppear {
                loadImage()
            }
    }

    var body: some View {
        VStack {
            myView
            Button("save image") {
                myView.snapshot { image in
                    if let image = image {
                        imageSaver.saveSnapshot(compressedImage: image)
                    } else {
                        print("Failed to capture snapshot image.")
                    }
                }
            }
        }
    }
}

extension View {
    func snapshot(completion: @escaping (UIImage?) -> Void) {
        let controller = UIHostingController(rootView: self)
        let view = controller.view
        view?.translatesAutoresizingMaskIntoConstraints = false
        let targetSize = controller.view.intrinsicContentSize
        view?.bounds = CGRect(origin: .zero, size: targetSize)
        view?.backgroundColor = .black
        view?.layoutIfNeeded()
        let renderer = UIGraphicsImageRenderer(size: targetSize)
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            let image: UIImage = renderer.image { _ in
                controller.view.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
            }
            controller.view.removeFromSuperview()
            completion(image)
        }
    }
}

class ImageSaver: NSObject, ObservableObject {
    @Published var isPresented = false
    @Published var title: String = ""
    @Published var message: String = ""

    func saveSnapshot(compressedImage: UIImage) {
        PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
            switch status {
            case .authorized:
                UIImageWriteToSavedPhotosAlbum(
                    compressedImage, self, #selector(self.saveError), nil
                )
            case .denied:
                self.title = NSLocalizedString("access_denied", comment: "")
                self.message =
                    NSLocalizedString("snapshot_saver_1", comment: "")
                self.isPresented = true
                print("access denied")
            case .limited:
                self.title = NSLocalizedString("snapshot_saver_2", comment: "")
                self.message =
                    NSLocalizedString("snapshot_saver_1", comment: "")
                self.isPresented = true
                print("limited access only")
            case .notDetermined:
                print("access not determined")
            case .restricted:
                print("access restricted")
            @unknown default:
                break
            }
        }
    }

    @objc func saveError(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
        isPresented = true
        if error != nil {
            title = "failed"
            message = "save image failed"

        } else {
            title = "success"
            message = "save image success"
        }
    }
}

#Preview {
    MyImageView()
}
解决方案

问题根源有两点:

  1. myView的onAppear在快照时不会触发,导致图片未加载、宽高参数为初始值0,快照用错误尺寸生成黑边和缩小内容。
  2. 快照方法依赖intrinsicContentSize获取尺寸,加上延迟1秒的做法不可靠,容易出现布局不匹配。

修复步骤

1. 迁移加载逻辑,脱离onAppear依赖

把图片加载和尺寸计算从myView移到主视图的onAppear,确保快照前已完成数据准备:

var myView: some View {
    ZStack {
        if self.image != nil {
            Image(uiImage: self.image!)
                .resizable()
                .frame(width: viewWidth, height: viewHeight)
        }
        Text("hello girl").font(.title).foregroundColor(.red)
    }
    .frame(width: viewWidth, height: viewHeight)
    .background(Color.black)
}

var body: some View {
    VStack {
        myView
        Button("save image") {
            // 后续修改内容见步骤3
        }
    }
    .onAppear {
        loadImage() // 移到此处
    }
}

2. 优化快照方法,使用已知尺寸

直接传入已计算好的宽高作为快照尺寸,去掉不可靠的延迟:

extension View {
    func snapshot(size: CGSize, completion: @escaping (UIImage?) -> Void) {
        let controller = UIHostingController(rootView: self)
        let view = controller.view
        view?.frame = CGRect(origin: .zero, size: size)
        view?.backgroundColor = .black
        
        // 强制完成布局
        view?.layoutIfNeeded()
        
        let renderer = UIGraphicsImageRenderer(size: size)
        let image = renderer.image { context in
            view?.drawHierarchy(in: view!.bounds, afterScreenUpdates: true)
        }
        
        controller.view.removeFromSuperview()
        completion(image)
    }
}

3. 调用快照时传入正确尺寸

修改按钮点击事件,确保尺寸就绪后再生成快照:

@State private var isImageLoaded = false

// 更新loadImage方法
private func loadImage() {
    guard !isImageLoaded else { return }
    URLSession.shared.dataTask(with: URL(string: imageUrl)!) { data, _, error in
        guard let data = data, error == nil else {
            print("Error loading image: \(error?.localizedDescription ?? "Unknown error")")
            return
        }
        if let uiImage = UIImage(data: data) {
            DispatchQueue.main.async {
                self.image = uiImage
                calculateSize()
                self.isImageLoaded = true
            }
        } else {
            print("Failed to create image from data")
        }
    }.resume()
}

// 更新按钮
Button("save image") {
    let size = CGSize(width: viewWidth, height: viewHeight)
    myView.snapshot(size: size) { image in
        if let image = image {
            imageSaver.saveSnapshot(compressedImage: image)
        } else {
            print("Failed to capture snapshot image.")
        }
    }
}
.disabled(!isImageLoaded)

修改后,快照尺寸与显示尺寸完全匹配,不会再出现黑边和内容缩小的问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 01:50:02