如何将视图(不是UIView)转换为图像?

时间:2019-07-25 11:06:21

标签: view swiftui

类似于this thread

我想将SwiftUI View而不是UIView转换为图像。

4 个答案:

答案 0 :(得分:5)

kontiki answer之后,这是“偏好设置”方式

groupby

答案 1 :(得分:3)

尽管SwiftUI不提供将视图转换为图像的直接方法,但您仍然可以做到。这有点hack,但是效果很好。

在下面的示例中,每当点击两个VStack时,代码就会捕获它们的图像。它们的内容将转换为UIImage(以后可以根据需要将其保存到文件中)。在这种情况下,我只在下面显示它。

请注意,可以改进代码,但是它提供了入门的基础。我使用GeometryReader来获取要捕获的VStack的坐标,但是可以使用Preferences进行改进以使其更健壮。如果需要了解更多信息,请查看提供的链接。

此外,为了将屏幕的区域转换为图像,我们确实需要UIView。该代码使用UIApplication.shared.windows[0].rootViewController.view来获取顶视图,但是根据您的情况,您可能需要从其他地方获取它。

祝你好运!

enter image description here

这是代码(在iPhone Xr模拟器Xcode 11 beta 4上测试):

import SwiftUI

extension UIView {
    func asImage(rect: CGRect) -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: rect)
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }
}

struct ContentView: View {
    @State private var rect1: CGRect = .zero
    @State private var rect2: CGRect = .zero
    @State private var uiimage: UIImage? = nil

    var body: some View {
        VStack {
            HStack {
                VStack {
                    Text("LEFT")
                    Text("VIEW")
                }
                .padding(20)
                .background(Color.green)
                .border(Color.blue, width: 5)
                .background(RectGetter(rect: $rect1))
                .tapAction { self.uiimage = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect1) }

                VStack {
                    Text("RIGHT")
                    Text("VIEW")
                }
                .padding(40)
                .background(Color.yellow)
                .border(Color.green, width: 5)
                .background(RectGetter(rect: $rect2))
                .tapAction { self.uiimage = UIApplication.shared.windows[0].rootViewController?.view.asImage(rect: self.rect2) }

            }

            if uiimage != nil {
                VStack {
                    Text("Captured Image")
                    Image(uiImage: self.uiimage!).padding(20).border(Color.black)
                }.padding(20)
            }

        }

    }
}

struct RectGetter: View {
    @Binding var rect: CGRect

    var body: some View {
        GeometryReader { proxy in
            self.createView(proxy: proxy)
        }
    }

    func createView(proxy: GeometryProxy) -> some View {
        DispatchQueue.main.async {
            self.rect = proxy.frame(in: .global)
        }

        return Rectangle().fill(Color.clear)
    }
}

答案 2 :(得分:2)

当您可以将不在屏幕上的SwiftUI视图保存到UIImage时,我想出了一个解决方案。 该解决方案看起来有些怪异,但效果很好。

首先创建一个类,用作UIHostingController和SwiftUI之间的连接。在此类中,定义一个函数,您可以调用该函数来复制“视图的”图像。完成此操作后,只需“发布”新值即可更新视图。

class Controller:ObservableObject {
     
    @Published var update=false
    
    var img:UIImage?
    
    var hostingController:MySwiftUIViewHostingController?
    
    init() {

    }
    
    func copyImage() {
        img=hostingController?.copyImage()
        update=true
    }
}

然后包装要通过UIHostingController复制的SwiftUI视图

class MySwiftUIViewHostingController: UIHostingController<TestView> {
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    func copyImage()->UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: self.view.bounds)
        
        return renderer.image(actions: { (c) in
            self.view.layer.render(in: c.cgContext)
        })
    }
    
}

copyImage()函数将控制器的视图作为UIImage返回

现在您需要显示UIHostingController:

struct MyUIViewController:UIViewControllerRepresentable {
    
    @ObservedObject var cntrl:Controller
    
    func makeUIViewController(context: Context) -> MySwiftUIViewHostingController {
        let controller=MySwiftUIViewHostingController(rootView: TestView())
        cntrl.hostingController=controller
        return controller
    }
    
    func updateUIViewController(_ uiViewController: MySwiftUIViewHostingController, context: Context) {
        
    }
    
}

其余部分如下:

struct TestView:View {
    
    var body: some View {
        VStack {
            Text("Title")
            Image("img2")
                .resizable()
                .aspectRatio(contentMode: .fill)
            Text("foot note")
        }
    }
}

import SwiftUI

struct ContentView: View {
    @ObservedObject var cntrl=Controller()
    var body: some View {
        ScrollView {
            VStack {
                HStack {
                    Image("img1")
                        .resizable()
                        .scaledToFit()
                        .border(Color.black, width: 2.0)
                        .onTapGesture(count: 2) {
                            print("tap registered")
                            self.cntrl.copyImage()
                    }
                    Image("img1")
                        .resizable()
                        .scaledToFit()
                        .border(Color.black, width: 2.0)
                }
                
                TextView()
                ImageCopy(cntrl: cntrl)
                    .border(Color.red, width: 2.0)
                TextView()
                TextView()
                TextView()
                TextView()
                TextView()
                MyUIViewController(cntrl: cntrl)
                    .aspectRatio(contentMode: .fit)
            }
            
        }
    }
}

struct ImageCopy:View {
    @ObservedObject var cntrl:Controller
    var body: some View {
        VStack {
            Image(uiImage: cntrl.img ?? UIImage())
                .resizable()
                .frame(width: 200, height: 200, alignment: .center)
        }
        
    }
}

struct TextView:View {
    
    var body: some View {
        VStack {
            Text("Bla Bla Bla Bla Bla ")
            Text("Bla Bla Bla Bla Bla ")
            Text("Bla Bla Bla Bla Bla ")
            Text("Bla Bla Bla Bla Bla ")
            Text("Bla Bla Bla Bla Bla ")
            
        }
        
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

您需要img1和img2(被复制的一个)。我将所有内容都放入了一个滚动视图中,以便即使不在屏幕上也能看到图像的复制效果。

答案 3 :(得分:1)

解决方案

以下是一种可能的解决方案,该解决方案使用插入到UIHostingController后台的rootViewController

func convertViewToData<V>(view: V, size: CGSize, completion: @escaping (Data?) -> Void) where V: View {
    guard let rootVC = UIApplication.shared.windows.first?.rootViewController else {
        completion(nil)
        return
    }
    let imageVC = UIHostingController(rootView: view.edgesIgnoringSafeArea(.all))
    imageVC.view.frame = CGRect(origin: .zero, size: size)
    DispatchQueue.main.async {
        rootVC.view.insertSubview(imageVC.view, at: 0)
        let uiImage = imageVC.view.asImage(size: size)
        imageVC.view.removeFromSuperview()
        completion(uiImage.pngData())
    }
}

您还需要kontiki建议here扩展的asImage扩展名(设置UIGraphicsImageRendererFormat是必要的,因为新设备可以具有2x3x规模):

extension UIView {
    func asImage(size: CGSize) -> UIImage {
        let format = UIGraphicsImageRendererFormat()
        format.scale = 1
        return UIGraphicsImageRenderer(size: size, format: format).image { context in
            layer.render(in: context.cgContext)
        }
    }
}

用法

假设您具有一些测试视图:

var testView: some View {
    ZStack {
        Color.blue
        Circle()
            .fill(Color.red)
    }
}

您可以将此视图转换为Data,该视图可用于返回Image(或UIImage):

convertViewToData(view: testView, size: CGSize(width: 300, height: 300)) {
    guard let imageData = $0, let uiImage = UIImage(data: imageData) else { return }
    return Image(uiImage: uiImage)
}

Data对象也可以保存到文件中,共享...


演示

struct ContentView: View {
    @State var imageData: Data?

    var body: some View {
        VStack {
            testView
                .frame(width: 50, height: 50)
            if let imageData = imageData, let uiImage = UIImage(data: imageData) {
                Image(uiImage: uiImage)
            }
        }
        .onAppear {
            convertViewToData(view: testView, size: .init(width: 300, height: 300)) {
                imageData = $0
            }
        }
    }

    var testView: some View {
        ZStack {
            Color.blue
            Circle()
                .fill(Color.red)
        }
    }
}