如何将swiftui视图主体转换为uiimage在viewController中

发布于 2025-01-18 12:35:03 字数 3490 浏览 3 评论 0 原文

我正在研究这种转换,并尝试了许多解决方案(扩展和方法),因为与此相关的问题和答案太多,但是没有什么能像我尝试过的解决方案那样帮助

尝试过的解决方案 <

< href =“ https://stackoverflow.com/a/64005395/15023395”> https://stackoverflow.com/a/a/64005395/15023395

a /41288197/1502395"> https://stackoverflow.com/a/a/41288197/15023395

以下取自

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)
//            }
        }
    }
}

此解决方案有所帮助,但我不想将image添加为Supperiew 的子视图

  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)
    }

我想做什么? Strong>

我有一个扩展SwiftUi视图的结构,并且我的设计中有一个设计。现在,我想将SwiftUi视图转换为故事板的ViewController内部的UIImage,当我的屏幕加载并 viewDidload()函数调用时,System UiimageView在故事板中的映像

这是我的SwiftUi代码< /strong>


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()
                )
        }
        
        
    }
}

I am working on this convertion and tried many solutions (extensions and methods) as there are so many questions and answers related to this but nothing helped like I have tried following solutions but didn't helped

Tried Solutions

https://stackoverflow.com/a/64005395/15023395

https://stackoverflow.com/a/41288197/15023395

Below is taken from https://stackoverflow.com/a/59333377/12299030

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)
//            }
        }
    }
}

This solution helped but I don't want to add image as subview of 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)
    }

What I want to do ???

I have a struct which extends swiftUI View and I have a design in it. Now I want to convert that swiftUI View into UIImage inside ViewController of storyboard that when my screen loads and viewDidLoad() function calls then system updates image of UIImageView in story board

Here is my SwiftUI code


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()
                )
        }
        
        
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

话少情深 2025-01-25 12:35:03

can 执行此操作...但不是 viewDidload() - 您必须至少等到 view> viewdidlayoutsubviews()

而且,必须将视图添加到视图层次结构中 - 但是一旦我们生成图像,就可以将其删除,以使其从未看到“屏幕上”。

注意:此处的所有“结果”图像使用:

  • 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
        }
    }

}

使用此 image> image fromContentView() func:

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: https://stackoverflow.com/a/70339424/6257435
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)
        }
    }
}

并将其在我们的Func中的第一行更改为:

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

我们得到此结果:

​SwiftUi contentView 布局使用 ZSTACK ,其中其内容(“ ring”)超过其垂直边界,戒指的顶部和底部被“夹住”。

我们可以通过更改 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: https://stackoverflow.com/a/70339424/6257435
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)
        }
    }
}

You can do this... but not in viewDidLoad() -- you have to wait at least until viewDidLayoutSubviews().

And, the view must be added to the view hierarchy -- but it can be removed as soon as we generate the image so it's never seen "on-screen."

Note: all "result" images here use:

  • a 240 x 200 image view
  • .contentMode = .center
  • green background so we can see the frame

and we give the UIImage generate from the SwiftUI ContentView a yellow background, because we will need to address some layout quirks.

So, to generate the image and set it to a UIImageView, we can do this:

// 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
        }
    }

}

using this imageFromContentView() func:

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
    
}

Result #1:

enter image description here

Notice the 20-pt yellow band at the top, and the content is not vertically centered... that's because the UIHostingController applies a safe area layout guide.

Couple options to get around that...

If we add this line:

    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()

we get this result:

enter image description here

the rendered image now has 20-pts Top and Bottom "safe area" insets.

If we don't want any safe area insets, we can use this extension:

// extension to remove safe area from UIHostingController
//  source: https://stackoverflow.com/a/70339424/6257435
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)
        }
    }
}

and change the first line in our func to:

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

and we get this result:

enter image description here

Because the SwiftUI ContentView layout is using a zStack where its content (the "ring") exceeds its vertical bounds, the top and bottom of the ring is "clipped."

We can fix that either by changing the framing in ContentView:

enter image description here

or by increasing the frame height of the loaded view, like this for example:

    // 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
    

enter image description here


Here's a complete implementation (using your unmodified 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: https://stackoverflow.com/a/70339424/6257435
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)
        }
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文