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

JHa*_*ack 1 view swiftui

此线程类似

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

paw*_*222 7

解决方案

这是一个可能的解决方案,它使用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())
    }
}
Run Code Online (Sandbox Code Playgroud)

您还需要kontiki在这里asImage提出的扩展的修改版本(设置是必要的,因为新设备可以拥有或扩展):UIGraphicsImageRendererFormat2x3x

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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法

假设您有一些测试视图:

var testView: some View {
    ZStack {
        Color.blue
        Circle()
            .fill(Color.red)
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以将此视图转换为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)
}
Run Code Online (Sandbox Code Playgroud)

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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


kon*_*iki 6

尽管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))
                .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)
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 嗨@dfd,恐怕我们已经处于实验领域了;-)据我所知,所有 SwiftUI 视图都只是包装的 UIKit 视图。列表由 UITableView 支持,TextField 由 UITextField 支持,等等。这就是为什么我认为这种方法会起作用。我预计不会出现任何递归问题,但可能需要更多测试。 (2认同)

kum*_*ang 6

按照kontiki 的回答,这里是 Preferences 方式

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

Run Code Online (Sandbox Code Playgroud)