11

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.

Anthony
  • 747
  • 1
  • 8
  • 23
  • You seem fixated on 50 images. Are you talking about 50 images on a *single* screen? That's likely no possible. Look at it this way - a `List` *is* equatable to a `UITableView`. What kind of device displays 50 cells at once/ –  Dec 03 '19 at 00:52
  • @dfd Not 50 images, but 50 rows. 50 is just an example. Let me put it like this: I want to create one jpeg containing all these 50 rows, so that it can be saved to camera roll or send to others by email. – Anthony Dec 03 '19 at 01:56

2 Answers2

7

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()
    }
}
Anjali Shah
  • 720
  • 7
  • 21
gotnull
  • 26,454
  • 22
  • 137
  • 203
  • Thanks for the quick reply. Will try it when I get home later. Will keep you updated. – Anthony Dec 02 '19 at 18:10
  • Some questions. 1. How can I put contents inside ContentView? 2. ContentView is bounded by the screen size. What if I need to take a snapshot of a long list of items, e.g. a List with 50 rows? – Anthony Dec 03 '19 at 00:32
  • 1
    @Anthony I'd ask another question if you want more functionality to your original question. If this answer has helped consider accepting it. – gotnull Dec 03 '19 at 00:44
  • Sorry if I haven't been expressing my question clearly before. What I need is to generate an UIImage of a SwiftUI View with contents extending beyond the screen size. This has always been my question. I guess there is no point to generate a blank screenshot from an empty ContentView. I just added an example to better illustrate my need. – Anthony Dec 03 '19 at 01:02
  • @Anthony Again, I'd recommend you ask another question. The comments sections are not meant to be used for added functionality on top of existing answers. – gotnull Dec 03 '19 at 01:25
  • 2
    With all respect, your answer is generating a blank white image with a sizing same the the screen size. While I've been asking for a way to generate an image of a view with contents exceeding the screen size. This is what I was asking all along, even in the original version of my question before I added the example. – Anthony Dec 03 '19 at 02:06
  • 1
    Anyway, thanks for your help. I'll mark this answer as correct and ask for another question with more clarity. Thank you again. :) – Anthony Dec 03 '19 at 02:07
  • 4
    This answer is not useful — He was asking how to render a SwiftUI view to an Image, this answer shows how to render a UIView to an image. This answer should not be marked as accepted. – sahandnayebaziz Aug 08 '20 at 19:04
  • its not working when view has remote image – Kodr.F Apr 28 '21 at 10:21
3

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!

Michel Donais
  • 474
  • 4
  • 13