2

I know that you can turn an iOS SwiftUI View into an image by following these steps, but this uses UIKit things in the SwiftUI extension which you cannot use in SwiftUI for macOS.

Does anyone know how to snapshot/screenshot a macOS SwiftUI View?

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
WilliamD47
  • 129
  • 6

2 Answers2

6

In macOS same approach can be used. NSHostingController is analog of UIHostingController. Also to get it drawn the view should be added to some window:

extension View {
    func snapshot() -> NSImage? {
        let controller = NSHostingController(rootView: self)
        let targetSize = controller.view.intrinsicContentSize
        let contentRect = NSRect(origin: .zero, size: targetSize)
        
        let window = NSWindow(
            contentRect: contentRect,
            styleMask: [.borderless],
            backing: .buffered,
            defer: false
        )
        window.contentView = controller.view
        
        guard
            let bitmapRep = controller.view.bitmapImageRepForCachingDisplay(in: contentRect)
        else { return nil }
        
        controller.view.cacheDisplay(in: contentRect, to: bitmapRep)
        let image = NSImage(size: bitmapRep.size)
        image.addRepresentation(bitmapRep)
        return image
    }
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • Thank you so much! I've tried so many different code snippets to try to get this to work but this one finally did something. However, I needed to put everything from `controller.view.cacheDisplay` onward into a `DispatchQueue.main.async` block and return the result in a completion handler. I also hardcoded the targetSize for now because my view wasn't returning the right `intrinsicContentSize`, but that's an unrelated issue. – Angela Nov 04 '22 at 21:31
  • Okay, it turns out if you set the size of the view explicitly with `.frame(...)`, then not only does `intrinsicContentSize` work, but you can also call `window.contentView?.layout()` (without it crashing for lack of defined size) to make it work without using `DispatchQueue.main.async`. – Angela Nov 07 '22 at 22:15
0

SwiftUI native snapshot technique for macOS 13.0+

Use SwiftUI's ImageRenderer object to rasterize a macOS View into a NSImage.

Here's the code:

import SwiftUI

@MainActor final class BitmapImage : ObservableObject {
    
    @Published var images: [String: NSImage] = [:]

    init() {
        let view = SampleView()
        let renderer = ImageRenderer(content: view)
        
        #if os(macOS)
            renderer.scale = NSApplication.shared
                                          .mainWindow?
                                          .backingScaleFactor ?? 2.0
        
            if let nsImage = renderer.nsImage {
                images["1"] = nsImage
            }
        #endif
    }
}

struct SampleView : View {
    var body: some View {
        ZStack {
            Image("picture")
            VStack {
                Divider()
                Text("Lorem ipsum dolor sit amet")
                    .foregroundColor(.white)
                    .font(.custom("", size: 72))
                Spacer()
            }
        }
    }
}

enter image description here

@available(macOS 13.0, *) struct ContentView : View {
    
    @StateObject private var bitmapImage = BitmapImage()
    
    var body: some View {
        ZStack {
            if let nsImage = bitmapImage.images["1"] {
                Image(nsImage: nsImage)
                    .resizable()
                    .frame(width: 500)      // "Squeezed" Image test
            }
        }
    }
}
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220