NEW: My new book Pro SwiftUI is out now – level up your SwiftUI skills today! >>

How to convert a SwiftUI view to an image

Paul Hudson    @twostraws   

Updated for Xcode 14.2

Improved in iOS 16

SwiftUI’s ImageRenderer class is able to render any SwiftUI view hierarchy into an image, which can then be saved, shared, or reused somehow else. At its simplest, the code needed is this:

let renderer = ImageRenderer(content: Text("Hello, world!"))

if let uiImage = renderer.uiImage {
    // use the rendered image somehow
}

Download this as an Xcode project

However, there are four key points to be aware of:

  1. If you don’t specify otherwise, your image will be rendered at 1x scale – that will look fuzzy on 2x or 3x resolution screens.
  2. You must not attempt to use ImageRenderer off the main actor, which might mean marking your rendering code with @MainActor.
  3. You can put the SwiftUI views you want to render right into the ImageRenderer(content:) initializer if you want, but I nearly always find separating them out into a dedicated view results in much cleaner code.
  4. Unlike the older UIGraphicsImageRenderer there is no easy way to read PNG or JPEG data directly from ImageRenderer, so as you can see in the code we need to read its resulting UIImage then call the pngData() method of that. This makes the code more complex for cross-platform users, but when I suggested to Apple that they could make it better they told me to use the (ancient) ImageI/O framework. (Spoiler: don’t use it, it will just lead to pain.)

Let’s look at a second example that is more realistic – this automatically uses the correct image scale for the device, uses @MainActor to ensure the rendering code is safe to call, carves out the view to render into its own struct, and then lets users share the result using ShareLink:

// An example view to render
struct RenderView: View {
    let text: String

    var body: some View {
        Text(text)
            .font(.largeTitle)
            .foregroundColor(.white)
            .padding()
            .background(.blue)
            .clipShape(Capsule())
    }
}

struct ContentView: View {
    @State private var text = "Your text here"
    @State private var renderedImage = Image(systemName: "photo")
    @Environment(\.displayScale) var displayScale

    var body: some View {
        VStack {
            renderedImage

            ShareLink("Export", item: renderedImage, preview: SharePreview(Text("Shared image"), image: renderedImage))

            TextField("Enter some text", text: $text)
                .textFieldStyle(.roundedBorder)
                .padding()
        }
        .onChange(of: text) { _ in render() }
        .onAppear { render() }
    }

    @MainActor func render() {
        let renderer = ImageRenderer(content: RenderView(text: text))

        // make sure and use the correct display scale for this device
        renderer.scale = displayScale

        if let uiImage = renderer.uiImage {
            renderedImage = Image(uiImage: uiImage)
        }
    }
}

Download this as an Xcode project

As you can see, that calls render() when the view is shown and also whenever text changes.

If you need to target iOS 15 and below, then SwiftUI’s views don’t have a built-in function to render a view as an image – we need to write one ourselves. The key here is to wrap the view using UIHostingController, then render its view hierarchy into a UIGraphicsImageRenderer.

This is best done using an extension on View, so you can call it naturally. This should wrap the view in a hosting controller, adjust the size of the hosting controller’s view to be the intrinsic content size of the SwiftUI view, clear any background color to keep the rendered image clean, then render the view into an image and send it back.

Here’s how that looks in code:

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)
        }
    }
}

To use that extension in SwiftUI, you should create your view as a property so you can reference it on demand – for example, in response to a button action.

For example, this renders a styled text view to an image, then saves it to the user’s photo album:

struct ContentView: View {
    var textView: some View {
        Text("Hello, SwiftUI")
            .padding()
            .background(.blue)
            .foregroundColor(.white)
            .clipShape(Capsule())
    }

    var body: some View {
        VStack {
            textView

            Button("Save to image") {
                let image = textView.snapshot()

                UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
            }
        }
    }
}

Important: In order to call UIImageWriteToSavedPhotosAlbum() you must add the NSPhotoLibraryAddUsageDescription key to your Info.plist and explain to the user why you want to write images. If you fail to do this your app will crash when you attempt to write out the image.

Hacking with Swift is sponsored by Essential Developer

SPONSORED From March 20th to 26th, you can join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer!

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Similar solutions…

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.3/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.