【问题标题】:How to convert a View (not UIView) to an image?如何将视图(不是 UIView)转换为图像?
【发布时间】:2019-12-03 15:31:05
【问题描述】:

与此线程类似:How to convert a UIView to an image

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

【问题讨论】:

  • 可能是不可能的。请记住,在UIKit(完全不同的堆栈)中,您不会将UIView 转换为“图像”,而是将其转换为UIImage。由于 (a) 那是 100% UIKit 和 (b) SwiftUI 距离 GM 版本 1 还需要几个月的时间,我认为最好的办法是弄清楚如何使用 UIKitUIViewControllerRepresentable。 (也许启动一个函数?这就是为什么我建议UIViewController。)

标签: swift view swiftui


【解决方案1】:

虽然 SwiftUI 没有提供将视图转换为图像的直接方法,但您仍然可以做到。这有点小技巧,但效果很好。

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

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

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

祝你好运!

这是代码(在 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))
                .onTapGesture { 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))
                .onTapGesture { 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)
    }
}

【讨论】:

  • 非常不错的 hack。一个问题 - 使用UIKitCoreGraphics(不是链接问题的一部分),您可以拍摄UIView 的“屏幕截图”,其中包括它的渲染子视图。这能做到吗?还是需要...好吧,因为它是GeometryReader,这是父视图,您是否必须确保防止...反向递归?
  • 嗨@dfd 恐怕我们还处于实验领域;-) 就我所见,所有 SwiftUI 视图都只是包装好的 UIKit 视图。列表由 UITableView 支持,TextField 由 UITextField 支持,等等。这就是为什么我认为这种方法可行的原因。我预计不会出现任何递归问题,但可能需要进行更多测试。
  • 这行得通。但是我如何扩展它以获得整个视图(不仅仅是当前的可视区域)?
  • 恐怕这种方法只适用于可见区域。 :-(
  • @kontiki 我看到您的代码正在渲染 rootViewController 的一部分。有什么方法可以捕获 topViewController 吗? (例如模态表?)
【解决方案2】:

按照kontiki answer,这里是首选项方式

import SwiftUI

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

    var body: some View {
        VStack {
            HStack {
                VStack {
                    Text("LEFT")
                    Text("VIEW")
                }
                .padding(20)
                .background(Color.green)
                .border(Color.blue, width: 5)
                .getRect($rect1)
                .onTapGesture {
                    self.uiImage =  self.rect1.uiImage
                }

                VStack {
                    Text("RIGHT")
                    Text("VIEW")
                }
                .padding(40)
                .background(Color.yellow)
                .border(Color.green, width: 5)
                .getRect($rect2)
                .onTapGesture {
                    self.uiImage =  self.rect2.uiImage
                }
            }

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

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

extension CGRect {
    var uiImage: UIImage? {
        UIApplication.shared.windows
            .filter{ $0.isKeyWindow }
            .first?.rootViewController?.view
            .asImage(rect: self)
    }
}

extension View {
    func getRect(_ rect: Binding<CGRect>) -> some View {
        self.modifier(GetRect(rect: rect))
    }
}

struct GetRect: ViewModifier {

    @Binding var rect: CGRect

    var measureRect: some View {
        GeometryReader { proxy in
            Rectangle().fill(Color.clear)
                .preference(key: RectPreferenceKey.self, value:  proxy.frame(in: .global))
        }
    }

    func body(content: Content) -> some View {
        content
            .background(measureRect)
            .onPreferenceChange(RectPreferenceKey.self) { (rect) in
                if let rect = rect {
                    self.rect = rect
                }
            }

    }
}

extension GetRect {
    struct RectPreferenceKey: PreferenceKey {
        static func reduce(value: inout CGRect?, nextValue: () -> CGRect?) {
            value = nextValue()
        }

        typealias Value = CGRect?

        static var defaultValue: CGRect? = nil
    }
}

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

【讨论】:

    【解决方案3】:

    当您可以将不在屏幕上的 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(被复制的那个)。我将所有内容都放入一个滚动视图中,这样即使不在屏幕上,也可以看到图像复制得很好。

    【讨论】:

    • 这似乎只有在您实际显示MyUIViewController 时才有效。例如,如果您想将图像保存为 png,因此不显示 UIView,则永远不会调用 makeUIViewController。有什么想法可以强制它渲染吗?
    • 你好,我不知道。我需要一个解决方案,将视图添加到滚动视图后将其保存为图像。即使视图本身尚未显示并且必须向下滚动才能看到它,我也需要它来工作。幸运的是,我的代码可以很好地满足我的需求,而且我还没有寻找其他解决方案。问候Libor。
    • 感谢您的澄清。我在 tvOS Top Shelf 扩展中尝试它,我想创建一个图像以使用 swiftUI 进行布局显示,但是虽然我可以使它与 UILabels 一起使用,但无论我如何尝试,我都无法让 SwiftUIHostingViewContainer 呈现。
    • 如果我创建一个新项目并使用它,它可以工作,但是当我尝试在我已经拥有的更大项目中使用它时,它不会复制图像,有什么想法吗?
    • Gexhange,您需要更具体一点。我只能猜测可能出了什么问题。我在整个项目中都使用它,目前工作正常。尝试单步执行并查看是否调用了 func copyImage()->UIImage {...}。无论如何,这个功能可能会在新的 SwiftUI 中可用:)。问候Libor。
    【解决方案4】:

    解决方案

    这是一个可能的解决方案,它使用插入到rootViewController 的背景中的UIHostingController

    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 提议的 hereasImage 扩展的修改版本(设置 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)
            }
        }
    }
    

    【讨论】:

    • 这一行let imageData = $0 究竟是如何将视图转换为数据的?
    • @Duck 首先将testView 传递给convertViewToData。然后此函数调用作为参数传递的完成处理程序:completion: @escaping (Data?) -&gt; Void。换句话说,let imageData = $0 只是将convertViewToData 的结果赋给一个变量。
    猜你喜欢
    • 2015-08-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多