Snapshot of a SwiftUI View

I'm wondering if anyone has figured out how to convert a SwiftUI View into an image?


I've tried encapsulating a SwiftUI View into a UIHostingController and using the following code:


import SwiftUI

extension UIView {
    
    func image() -> UIImage {
        let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
        let image = renderer.image { context in
            self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
        }
        return image
    }
}

extension UIHostingController {
    
    func image() -> Image? {

        guard let view = self.view else {
            return nil
        }
        
        let image = view.image()
        return Image(uiImage: image)
    }
}

struct RasterizedView<Content: View>: View {
    var controller: UIHostingController<Content>
    
    init(_ content: Content) {
        self.controller = UIHostingController(rootView: content)
    }
    
    var body: some View {
        controller.image()
    }
}

#if DEBUG
struct RasterizedViewTest_Previews: PreviewProvider {
    static var previews: some View {
        RasterizedView(Text("TEST"))
    }
}
#endif


This code displays nothing. I went into the debugger and looked at the UIHostingController. The view always has width of 0 and a height of 0.


I forced the view to load by adding self.loadView() as a first step in UIHostingController.image(), which results in the view having a non-zero size. However, I encountered a new problem: during runtime I receive an error that looks like this:


2019-08-17 17:41:08.737684-0400 Form[88020:27394836] [Snapshotting] View (0x7feb3200a420, UIView) drawing with afterScreenUpdates:YES inside CoreAnimation commit is not supported.


Does anyone have any experience or advice that might help me solve this problem?

Post not yet marked as solved Up vote post of PRG Down vote post of PRG
8.7k views

Replies

I'm struggling with this too. I've almost managed to get it working but the rendered image is incorrect.


The NavigationView items bleed over into the below view (i.e. they are correct in the app but the rendered image has the navigation title and barbutton items shifted down and overapping the view just below the navigation view.


In addition if I try to capture the whole screen and extract the specific subview, the reported view size is incorrect - i.e. the attached GeomertyReader provides the size of the view however when that size is used, the extracted image also includes a region around the view.

I battled this for a while. I have a specific view that I needed to capture which was sometimes larger than the screen.


My workaround in the end was to put the view I wanted to capture in a UIHostingController which I then put in the main view of a UIScrollView (this gives me flexibility on size)


then



func image(with view: UIView) -> UIImage? {

UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)

defer { UIGraphicsEndImageContext() }

if let context = UIGraphicsGetCurrentContext() {

view.layer.render(in: context)

if let image = UIGraphicsGetImageFromCurrentImageContext() {

return image

}

return nil

}

return nil

}

Can you share how you have managed to capture a SwiftUI view? I have tried some solutions but none of them worked.

Could you share your code?

How to take a screenshot of List in a ScrollView of SwiftUI?
I'd recommend combining these two approaches, worked like magic for me.

type the following in youtube search(Apple doesn't let me post the youtube link here):
"Hack screen capture to take photo of web info easily on your app | SwiftUI"
Use the background modifier shown in the video, it's a great idea.

Use the View and UIView extensions from this source:
https://gist.github.com/SatoTakeshiX/39e111aa93f896e58c74fdc13d19ba0a


Have you solved this? I'm trying to get it work with WebView which is causing a little bit problems because I think it should wait the view to load before taking the picture. Right now it's just white. Please let me know if you have solution.
Code Block
// Convert any SwiftUI View to UIImage
import SwiftUI
struct ContentView: View {
let imageSize: CGSize = CGSize(width: 1000, height: 1000)
var body: some View {
testView
.frame(width: 300, height: 300)
.onTapGesture {
// Capture high res image of swiftUI view
let highresImage = testView.asImage(size: imageSize)
// Now you can do what ever with the highresImage
}
}
var testView: some View {
ZStack {
Color.blue
Circle()
.fill(Color.red)
}
}
}
extension UIView {
func asImage() -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 1
return UIGraphicsImageRenderer(size: self.layer.frame.size, format: format).image { context in
self.drawHierarchy(in: self.layer.bounds, afterScreenUpdates: true)
}
}
}
extension View {
func asImage(size: CGSize) -> UIImage {
let controller = UIHostingController(rootView: self)
controller.view.bounds = CGRect(origin: .zero, size: size)
let image = controller.view.asImage()
return image
}
}


  • Man, you are my saviour.

  • This actually worked for me as some of the other answers did not. However when I’m getting the screenshot, the image on screen has been rotated slightly but the screenshot does not show this effect. Any idea of how to get the screenshot to show exactly how the image is placed on its parent view?

  • This was the simplest solution that worked for me. However size of image sometimes can be problematic. I am trying to find out a solution to get screen size so my screen captures were better.

Add a Comment
Simple. Add an extension to view.

Code Block Swift
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}

Then use body.snapshot() (assuming you use var body: Some View for your View) to take a UIImage snapshot of the View.

Code Block Swift
struct ContentView: View {
var textView: some View {
Text("Hello, SwiftUI")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.clipShape(Capsule())
}
var body: some View {
VStack {
textView
Button("Save to image") {
let image = textView.snapshot()
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
}
}
}


  • Any idea why there is a size limitation of 2730 for phone devices that has nativeScale of 3?

    if you set: let targetSize = CGSize(width: 3000, height: 3000) SnapShot will return an image with blank background for iPhones that have nativeScale of 3. No problem with other nativeScales and no problem with iPads.

  • On the iPad Pro (which seems to be 2x), the limit is 4096 which seems closer to an actual limit than 2730.

    Apple probably hardcoded an 8192x8192 pixel limit for rendering views.

Add a Comment
Code Block extension View {
  func snapshot() -> UIImage {
    let controller = UIHostingController(rootView: self)
    let view = controller.view
    let targetSize = controller.view.intrinsicContentSize
    view?.bounds = CGRect(origin: .zero, size: targetSize)
    view?.backgroundColor = .systemBackground
    let renderer = UIGraphicsImageRenderer(size: targetSize)
    return renderer.image { _ in
      view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
    }
  }
}

I think it can help I use this in swiftui it can be used in swift too

edit: I read all replies after posting
  • Does anyone know if it's possible to convert a swiftui view to an NSImage like is being done here for UIImage?

Add a Comment