Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Render SwiftUI View as an UIImage

I'm trying to render the SwiftUI View as an UIImage then let the user choose to save to the camera roll or send to others via email.

As an example, I want to render a list of 50 rows into a UIImage.

struct MyList: View {
    var body: some View {
        List {
            ForEach(0 ..< 50, id:\.self) {
                Text("row \($0)")
            }
        }
    }
}

Searched over the internet for the past few weeks still with no luck. I have tried 2 different approaches.

1. UIHostingController (source here)

let hosting = UIHostingController(rootView: Text("TEST"))
hosting.view.frame = // Calculate the content size here //
let snapshot = hosting.view.snapshot        // output: an empty snapshot of the size
print(hosting.view.subviews.count)          // output: 0

I've tried layoutSubviews(), setNeedsLayout(), layoutIfNeeded(), loadView(), but all still resulted 0 subviews.

2. UIWindow.rootViewController (source here)

var vc = UIApplication.shared.windows[0].rootViewController
vc = vc.visibleController          // loop through the view controller stack to find the top most view controller
let snapshot = vc.view.snapshot    // output: a snapshot of the top most view controller

This almost yields the output I have wanted. However, the snapshot I get is literally a screenshot, i.e. with a Navigation bar, Tab bar and fixed size (same as screen size). What I need to capture is simply the view's content without those bars, and may sometimes be larger than the screen (my view is a long list in this example).

Tried to query vc.view.subviews to look for the table view I want, but returning an unhelpful [<_TtGC7SwiftUI16PlatformViewHostGVS_42PlatformViewControllerRepresentableAdaptorGVS_16BridgedSplitViewVVS_22_VariadicView_Children7ElementGVS_5GroupGVS_19_ConditionalContentS4_GVS_17_UnaryViewAdaptorVS_9EmptyView______: 0x7fb061cc4770; frame = (0 0; 414 842); anchorPoint = (0, 0); tintColor = UIExtendedSRGBColorSpace 0 0.478431 1 1; layer = <CALayer: 0x600002d8caa0>>].

Any help is much appreciated.

like image 947
Anthony Avatar asked Dec 02 '19 02:12

Anthony


2 Answers

I am wondering why people say your question is not clear. It actually makes perfect sense and it's one of the things I've been looking to do myself.

I finally found a way to do things the same way you wish them to be done, it's convoluted but it does work.

The idea is to abuse your second possibility (UIWindow.rootViewController). That one is fun because you take your window, you figure out what you wish to draw, and you draw it.

But the problem is this is your main window, and you ask to draw your main window.

The way I finally got it working is by doing a UIViewControllerRepresentable, which gives me a UIViewController in my SwiftUI application. Then, inside it, I put a single UIHostingController, which gives me a SwiftUI view inside the UIView. In other words, I do a bridge.

In the update function, I can then call a dispatch async to view.layer.render(context) to draw the image.

Of interest, the UIViewControllerRepresentable will actually be full screen. So I recommend doing a .scale(...) so your entire list fits in the screen (the render command will not mind it being smaller) and it will be centered. So if you know the size of your image in advance, you can do a HStack { VStack { YourStuff Spacer() } Spacer() } so it's rendered top-left.

Good luck!

like image 112
Michel Donais Avatar answered Oct 24 '22 22:10

Michel Donais


Would something like this work for you?

import SwiftUI

extension UIView {
    func takeScreenshot() -> UIImage {
        // Begin context
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.main.scale)
        // Draw view in that context
        drawHierarchy(in: self.bounds, afterScreenUpdates: true)
        // And finally, get image
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        if (image != nil) {
            UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil);
            return image!
        }

        return UIImage()
    }
}

struct ContentView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        let someView = UIView(frame: UIScreen.main.bounds)
        _ = someView.takeScreenshot()
        return someView
    }

    func updateUIView(_ view: UIView, context: Context) {

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
like image 33
fulvio Avatar answered Oct 24 '22 22:10

fulvio