如何在 ViewController 中将 SwiftUI View body 转换为 UIImage

How to convert SwiftUI View body to UIImage in ViewController

我正在处理这个转换并尝试了很多解决方案(扩展和方法),因为有很多与此相关的问题和答案,但没有任何帮助,就像我尝试了以下解决方案但没有帮助

尝试过的解决方案

以下摘自

extension View {
    func asImage() -> UIImage {
        let controller = UIHostingController(rootView: self)

        // locate far out of screen
        controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
        UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)

        let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
        controller.view.bounds = CGRect(origin: .zero, size: size)
        controller.view.sizeToFit()

        let image = controller.view.asImage()
        controller.view.removeFromSuperview()
        return image
    }
}

extension UIView {
    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { rendererContext in
// [!!] Uncomment to clip resulting image
//             rendererContext.cgContext.addPath(
//                UIBezierPath(roundedRect: bounds, cornerRadius: 20).cgPath)
//            rendererContext.cgContext.clip()

// As commented by @MaxIsom below in some cases might be needed
// to make this asynchronously, so uncomment below DispatchQueue
// if you'd same met crash
//            DispatchQueue.main.async {
                 layer.render(in: rendererContext.cgContext)
//            }
        }
    }
}

这个解决方案有帮助,但我不想将图像添加为 superView 的子视图

  func extractView(){
        let hostView = UIHostingController(rootView: ContentView())
        hostView.view.translatesAutoresizingMaskIntoConstraints = false
        let constraints = [
            hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
            hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor),
            hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor),
        ]
        self.view.addSubview(hostView.view)
        self.view.addConstraints(constraints)
    }

我想做什么???

我有一个扩展 swiftUI View 的结构,我在其中有一个设计。现在我想将 swiftUI View 转换为故事板 ViewController 内的 UIImage,当我的屏幕加载和 viewDidLoad() 函数调用时,系统会更新故事板

中 UIImageView 的图像

这是我的 SwiftUI 代码


import SwiftUI

struct ContentView: View {
    var body: some View {
        
        ZStack(alignment: .center){
            Rectangle()
                .frame(width: 200, height: 75)
                .cornerRadius(10)
                .foregroundColor(.white)
            Circle()
                .stroke(lineWidth:5)
                .foregroundColor(.red)
                .frame(width: 75, height: 75, alignment: .leading)
                .background(
                    Image("tempimage")
                        .resizable()
                )
        }
        
        
    }
}

可以 这样做...但不能在 viewDidLoad() -- 您必须至少等到 viewDidLayoutSubviews().

而且,必须将视图添加到视图层次结构中——但可以在我们生成图像后立即将其删除,因此它永远不会出现“on-screen”。“

注意:此处所有“结果”图片均使用:

  • 一个240 x 200图像视图
  • .contentMode = .center
  • 绿色背景,以便我们可以看到框架

我们给 UIImage 从 SwiftUI ContentView 生成黄色背景,因为我们需要解决一些布局问题。

因此,要生成图像并将其设置为 UIImageView,我们可以这样做:

// we will generate the image in viewDidLayoutSubview()
//  but that can be (and usually is) called more than once
//  so we'll use this to make sure we only generate the image once
var firstTime: Bool = true

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    // we only want this to run once
    if firstTime {
        firstTime = false
        if let img = imageFromContentView() {
            imgView.image = img
        }
    }

}

使用这个 imageFromContentView() 函数:

func imageFromContentView() -> UIImage? {
    
    let swiftUIView = UIHostingController(rootView: ContentView())
    
    // add as chlld controller
    addChild(swiftUIView)
    
    // make sure we can get its view (safely unwrap its view)
    guard let v = swiftUIView.view else {
        swiftUIView.willMove(toParent: nil)
        swiftUIView.removeFromParent()
        return nil
    }
    
    view.addSubview(v)
    swiftUIView.didMove(toParent: self)
    
    // size the view to its content
    v.sizeToFit()
    
    // force it to layout its subviews
    v.setNeedsLayout()
    v.layoutIfNeeded()
    
    // if we want to see the background
    v.backgroundColor = .systemYellow
    
    // get it as a UIImage
    let img = v.asImage()
    
    // we're done with it, so get rid of it
    v.removeFromSuperview()
    swiftUIView.willMove(toParent: nil)
    swiftUIView.removeFromParent()
    
    return img
    
}

结果 #1:

请注意顶部的 20 磅黄带,内容未垂直居中...这是因为 UIHostingController 应用了安全区域布局指南。

解决这个问题的几个选项...

如果我们添加这一行:

    view.addSubview(v)
    swiftUIView.didMove(toParent: self)

    // add same bottom safe area inset as top
    swiftUIView.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: v.safeAreaInsets.top, right: 0)
    
    // size the view to its content
    v.sizeToFit()

我们得到这个结果:

渲染图像现在有 20 磅的顶部和底部“安全区域”插图。

如果我们不想任何安全区域插入,我们可以使用这个扩展:

// extension to remove safe area from UIHostingController
//  source: 
extension UIHostingController {
    convenience public init(rootView: Content, ignoreSafeArea: Bool) {
        self.init(rootView: rootView)
        
        if ignoreSafeArea {
            disableSafeArea()
        }
    }
    
    func disableSafeArea() {
        guard let viewClass = object_getClass(view) else { return }
        
        let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
        if let viewSubclass = NSClassFromString(viewSubclassName) {
            object_setClass(view, viewSubclass)
        }
        else {
            guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
            guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
            
            if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
                let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
                    return .zero
                }
                class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
            }
            
            objc_registerClassPair(viewSubclass)
            object_setClass(view, viewSubclass)
        }
    }
}

并将函数的第一行更改为:

let swiftUIView = UIHostingController(rootView: ContentView(), ignoreSafeArea: true)

我们得到这个结果:

因为 SwiftUI ContentView 布局使用 zStack 其内容(“环”)超出其垂直边界,所以环的顶部和底部被“截断”。

我们可以通过更改 ContentView 中的框架来解决此问题:

或者增加加载视图的框架高度,例如:

    // size the view to its content
    v.sizeToFit()
    
    // for this explicit example, the "ring" extends vertically
    //  outside the bounds of the zStack
    //  so we'll add 10-pts height
    v.frame.size.height += 10.0
    


这是一个完整的实现(使用您未修改的 ContentView):

class ViewController: UIViewController {

    let imgView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        imgView.contentMode = .center
        
        imgView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(imgView)

        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            // let's put the imageView 40-pts from Top
            imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            // centered horizontally
            imgView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            // width: 240
            imgView.widthAnchor.constraint(equalToConstant: 240.0),
            // height: 200
            imgView.heightAnchor.constraint(equalToConstant: 200.0),
        ])

        // show the image view background so we
        //  can see its frame
        imgView.backgroundColor = .systemGreen

    }

    // we will generate the image in viewDidLayoutSubview()
    //  but that can be (and usually is) called more than once
    //  so we'll use this to make sure we only generate the image once
    var firstTime: Bool = true
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        // we only want this to run once
        if firstTime {
            firstTime = false
            if let img = imageFromContentView() {
                imgView.image = img
            }
        }
    
    }

    func imageFromContentView() -> UIImage? {
        
        let swiftUIView = UIHostingController(rootView: ContentView(), ignoreSafeArea: true)
        
        // add as chlld controller
        addChild(swiftUIView)
        
        // make sure we can get its view (safely unwrap its view)
        guard let v = swiftUIView.view else {
            swiftUIView.willMove(toParent: nil)
            swiftUIView.removeFromParent()
            return nil
        }
        
        view.addSubview(v)
        swiftUIView.didMove(toParent: self)
        
        // size the view to its content
        v.sizeToFit()
        
        // for this explicit example, the "ring" extends vertically
        //  outside the bounds of the zStack
        //  so we'll add 10-pts height
        v.frame.size.height += 10.0
        
        // force it to layout its subviews
        v.setNeedsLayout()
        v.layoutIfNeeded()
        
        // if we want to see the background
        v.backgroundColor = .systemYellow
        
        // get it as a UIImage
        let img = v.asImage()
        
        // we're done with it, so get rid of it
        v.removeFromSuperview()
        swiftUIView.willMove(toParent: nil)
        swiftUIView.removeFromParent()
        
        return img
        
    }
}

// extension to remove safe area from UIHostingController
//  source: 
extension UIHostingController {
    convenience public init(rootView: Content, ignoreSafeArea: Bool) {
        self.init(rootView: rootView)
        
        if ignoreSafeArea {
            disableSafeArea()
        }
    }
    
    func disableSafeArea() {
        guard let viewClass = object_getClass(view) else { return }
        
        let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
        if let viewSubclass = NSClassFromString(viewSubclassName) {
            object_setClass(view, viewSubclass)
        }
        else {
            guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
            guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
            
            if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
                let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
                    return .zero
                }
                class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
            }
            
            objc_registerClassPair(viewSubclass)
            object_setClass(view, viewSubclass)
        }
    }
}

extension UIView {
    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(size: frame.size)
        return renderer.image { context in
            layer.render(in: context.cgContext)
        }
    }
}