Exporting SwiftUI Views as an Image

A feature that some developers may want in an app is to be able to export their view as an image. Perhaps you have a recipe app that displays the ingredients and instructions to cook a particular dish, and you want to provide the option to export it as an image for your users to share with others. There are a few established solutions for this with UIKit, however it was only recently that I learned how to do this in SwiftUI.

My use-case for this with Plate-It was that I wanted my users to be able to share an image of their plates without having to screenshot multiple pages. This feature would render all of the plates in a grid similar to how it is in the app, and then be saved to the user’s camera roll. Below is an example - and yes that’s my entire license plate collection!

Implementation#

To start with, we need two extensions. One will be for View, which we will use to embed our View inside a UIHostingController. The other will then be for UIView, which will leverage UIGraphicsImageRenderer to create an image for what’s inside the Hosting Controller.

For larger images the rendering can take some time, so I prefer to use a completion handler to return the image back to our View. If you are only expecting to render smaller images, then you can change the saveAsImage() method to simply return the UIImage.

extension View {
    func saveAsImage(width: CGFloat, height: CGFloat, _ completion: @escaping (UIImage) -> Void) {
        let size = CGSize(width: width, height: height)
        
        let controller = UIHostingController(rootView: self.frame(width: width, height: height))
        controller.view.bounds = CGRect(origin: .zero, size: size)
        let image = controller.view.asImage()
        
        completion(image)
    }
}

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

Once we have this code in place, using it in our View is really easy. A few things to note, however:

  1. You need to set a frame for your View before using .saveAsImage(), otherwise you could end up with a heavily distorted image.
  2. By default, the image will take the background color depending on if the user has light or dark mode enabled - you’ll want to set your view’s .background() to override this.
  3. Don’t forget to set NSPhotoLibraryAddUsageDescription in your info.plist if you haven’t already!

In this sample app, we render a ZStack with a blue circle, an emoji on top, and a green background to fill the frame. This stack is clipped by a RoundedRectangle. You can see that we put our exportable view into it’s own View. This is because we need to use it twice - once to include it in our view’s body, and once inside the button’s action so that we can call the saveAsImage() method on it.

struct ContentView: View {
    
    var contentToSave: some View {
        ZStack {
            Circle()
                .fill(Color.blue)
                .frame(width: 150, height: 150)
                .padding()
            Text("🤯")
                .font(.largeTitle)
        }
            .background(Color.green.edgesIgnoringSafeArea(.all))
            .clipShape(RoundedRectangle(cornerRadius: 16))
    }
    
    var body: some View {
        VStack {
            contentToSave
            Button("Save Image") {
                contentToSave.saveAsImage(width: 200, height: 200) { image in
                    UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
                }
            }
        }
    }
}

Running the above code and tapping ‘Save Image’ will save the below image to your photo library. Of course you can extend this however you like, there’s so much you can do with this code!

Conclusion#

If this tutorial helped you in any way, I’d really appreciate a share on Twitter. You can reach out to me via Twitter if you have any questions, or to show me what you’ve been up to!