Snapshotting a SwiftUI View isn’t as convenient as for UIKit UIView, but it worked well for iOS 13 & 14. I encountered the padding issue while working on Reverse Meme Search during iOS 15 release, the exported images suddenly had some weird padding.

If you just want the code, check this GitHub Gist.

Exported Memes

1. The initial code

Let’s first review the code that was working well before iOS 15.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
extension View {
    
    func snapshot() -> UIImage? {
        
        let controller = UIHostingController(rootView: self)
        guard let view = controller.view else { return nil }
        
        let targetSize = view.intrinsicContentSize
        if targetSize.width <= 0 || targetSize.height <= 0 { return nil }
        
        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)
        }
        
    }
    
}

You can find some alternatives, but the overall idea is there. We wrap the SwiftUI View within an UIHostingController, and we render it using UIGraphicsImageRenderer.

2. The fix

I couldn’t find the root cause leading to the bug, but I assume it’s a change within how safe area is managed in iOS 15.

Leading us to modify the code with this:

1
2
3
4
5
// Note: since iOS 15 it seems these two modifiers are required.
let controller = UIHostingController(
    rootView: self.ignoresSafeArea()
        .fixedSize(horizontal: true, vertical: true)
)

Applying these two modifiers seems to do the trick to avoid the weird padding:

  • Ignore the safe area (ignoresSafeArea).
  • Force the view to a fixed size (fixedSize).

The whole code (helper class) would be this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
extension View {
    
    func snapshot() -> UIImage? {
        
        // Note: since iOS 15 it seems these two modifiers are required.
        let controller = UIHostingController(
            rootView: self.ignoresSafeArea()
                .fixedSize(horizontal: true, vertical: true)
        )
        guard let view = controller.view else { return nil }
        
        let targetSize = view.intrinsicContentSize
        if targetSize.width <= 0 || targetSize.height <= 0 { return nil }
        
        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)
        }
        
    }
    
}

Happy coding!