4

I have found numerous examples of converting UIView to UIImage, and they work perfectly for once the view has been laid out etc. Even in my view controller with many rows, some of which are net yet displaying on the screen, I can do the conversion. Unfortunately, for some of the views that are too far down in the table (and hence have not yet been "drawn"), doing the conversion produces a blank UIImage.

I've tried calling setNeedsDisplay and layoutIfNeeded, but these don't work. I've even tried to automatically scroll through the table, but perhaps I'm not doing in a way (using threads) that ensures that the scroll happens first, allowing the views to update, before the conversion takes place. I suspect this can't be done because I have found various questions asking this, and none have found a solution. Alternatively, can I just redraw my entire view in a UIImage, not requiring a UIView?

From Paul Hudson's website

Using any UIView that is not showing on the screen (say a row in a UITableview that is way down below the bottom of the screen.

let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
let image = renderer.image { ctx in
    view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
Joao Frasco
  • 53
  • 1
  • 3
  • Could you clarify the purpose of this? – Bence Pattogato Feb 14 '19 at 13:09
  • You can't do that, Its only applicable to views currently rendered ON (or OFF) screen. I cannot think of any easy way to do this. Maybe you can programmatically scroll the table top to bottom and create UIImage one by one each cell when appeared. It feels hacky but it could work. – RJE Feb 14 '19 at 13:25
  • Possible duplicate of [Convert UIWebview contents to a UIImage when the webview is larger than the screen](https://stackoverflow.com/questions/4475397/convert-uiwebview-contents-to-a-uiimage-when-the-webview-is-larger-than-the-scre) – DonMag Feb 14 '19 at 13:47
  • My app has tableviews with lots of charts over multiple rows (between 6 and 10 say). I want to get these out of the app to share with others. Taking snapshots is fine, but slow and cumbersome. For certain applications, I also want to crop the snapshot to only include the relevant part of the screenshot i.e. the uiview of interest. I therefore created a share button, and wanted to dump all 6 or 10 views into the share at once (to email, or airdrop, or add to photo library), but it would just provide blanks for those views not on the screen. – Joao Frasco Feb 14 '19 at 14:54

2 Answers2

9

You don't have to have a view in a window/on-screen to be able to render it into an image. I've done exactly this in PixelTest:

extension UIView {

    /// Creates an image from the view's contents, using its layer.
    ///
    /// - Returns: An image, or nil if an image couldn't be created.
    func image() -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        context.saveGState()
        layer.render(in: context)
        context.restoreGState()
        guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
        UIGraphicsEndImageContext()
        return image
    }

}

This will render a view's layer into an image as it currently looks if it was to be rendered on-screen. That is to say, if the view hasn't been laid out yet, then it won't look the way you expect. PixelTest does this by force-laying out the view beforehand when verifying a view for snapshot testing.

Kane Cheshire
  • 1,654
  • 17
  • 20
  • I don't believe it. It works! Thank you so much. Do you know that there are at least five questions like this on this website that still have not been closed i.e. no solution provided. You're a genius. – Joao Frasco Feb 14 '19 at 14:50
  • This method uses my xib's default height and width for the UIImage it generates. How can I make it use custom dimensions for my snapshot? – Kirill Jan 16 '20 at 00:23
6

You can also accomplish this using UIGraphicsImageRenderer.

extension UIView {

    func image() -> UIImage {
        let imageRenderer = UIGraphicsImageRenderer(bounds: bounds)
        if let format = imageRenderer.format as? UIGraphicsImageRendererFormat {
            format.opaque = true
        }
        let image = imageRenderer.image { context in
            return layer.render(in: context.cgContext)
        }
        return image
    }

}
Rodrigo Morbach
  • 378
  • 4
  • 16
  • Thanks. This also works, but now the background is white, whereas in the method above, I could choose opaque to be true (and got a black background). – Joao Frasco Feb 14 '19 at 16:33
  • You can use the `format` (`UIGraphicsImageRendererFormat`) property of `UIGraphicsImageRenderer` to change opaque value. – Rodrigo Morbach Feb 14 '19 at 17:57
  • Thanks. Works perfectly. How do I mark both answers correct. – Joao Frasco Feb 14 '19 at 18:15
  • This method uses my xib's default height and width for the UIImage it generates. How can I make it use custom dimensions for my snapshot? – Kirill Jan 16 '20 at 00:24
  • You can change the method signature and passes the bounds parameter: `function image(customDimensions: CGRect = bounds)` – Rodrigo Morbach Jan 16 '20 at 18:02
  • 1
    @RodrigoMorbach I tried your suggestion for the customDimensions, but my issue is while the output image is custom, the ```UIView``` remains the same original size. https://stackoverflow.com/q/70226387/14414215 – app4g Dec 04 '21 at 14:21