Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic macOS support #3

Merged
merged 5 commits into from
Aug 17, 2023
Merged

Add basic macOS support #3

merged 5 commits into from
Aug 17, 2023

Conversation

jflan-dd
Copy link
Contributor

Add macOS support to Package.swift and loosen hard UIImage requirement to allow any snapshotting strategy that takes AnyView/ModifiedView as input.

@jflan-dd
Copy link
Contributor Author

@mackoj This should be a more flexible solution to #1

}

/// PreviewSnapshots assertion using `named` parameter
func test_namedAssertion() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was just added in the last PR, but this test felt redundant now that every test is using the named argument

@mackoj
Copy link

mackoj commented Aug 16, 2023

Thanks for making this.

@jflan-dd jflan-dd merged commit 7f5a753 into main Aug 17, 2023
@jflan-dd jflan-dd deleted the jflan/macOS-support branch August 17, 2023 14:52
@andrewtheis
Copy link

This doesn't appear to work, this code:

func test_snapshots() {
    MacOSContentView_Previews.snapshots.assertSnapshots(as: .image)
}

Results in compiler error:

Generic parameter 'Format' could not be inferred

Am I missing something?

@jflan-dd @mackoj

@jflan-dd
Copy link
Contributor Author

jflan-dd commented Jan 16, 2024

@andrewtheis The main swift-snapshot-testing library doesn't ship with an NSImage version of .image out of the box. I defined one locally in tests within this project, but it felt weird to include it as part of the package since the this library is otherwise strategy agnostic.

You could try including this strategy in your project.

extension Snapshotting where Value: SwiftUI.View, Format == NSImage {
    static var image: Self {
        Snapshotting<NSView, NSImage>.image(size: .init(width: 400, height: 400)).pullback { view in
            let view = NSHostingView(rootView: view)
            view.wantsLayer = true
            view.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
            return view
        }
    }
}

@andrewtheis
Copy link

@jflan-dd makes sense. This is code I used, which also correctly scales the image based on the backing scale factor - necessary if you're using Xcode Cloud, which runs at a 1px = 1pt resolution. Hope this helps anyone else that stumbles upon this!

#if os(macOS)
import AppKit
import Cocoa

extension Snapshotting where Value == NSView, Format == NSImage {
    
    public static func scaleFactorAdjustedImage(
        precision: Float = 1,
        perceptualPrecision: Float = 1,
        size: CGSize
    ) -> Snapshotting {
        return SimplySnapshotting.image(
            precision: precision, perceptualPrecision: perceptualPrecision
        ).asyncPullback { view in
            Async { callback in
                view.frame.size = size
                guard view.frame.width > 0, view.frame.height > 0 else {
                    fatalError("View not renderable to image at size \(view.frame.size)")
                }
                
                let bitmapRep = view.bitmapImageRepForCachingDisplay(in: view.bounds)!
                view.cacheDisplay(in: view.bounds, to: bitmapRep)
                
                // Use the scaling function to get the scaled image
                let image = adjustForBackingScaleFactor(bitmapRep, in: view)
                callback(image)
            }
        }
    }
    
    // Function to scale the image based on the window's scale factor
    static func adjustForBackingScaleFactor(_ bitmapRep: NSBitmapImageRep, in view: NSView) -> NSImage {
        let scaleFactor = 1 / (NSScreen.main?.backingScaleFactor ?? view.window?.backingScaleFactor ?? 1)
        let scaledSize = NSSize(width: bitmapRep.size.width * scaleFactor, height: bitmapRep.size.height * scaleFactor)
        
        guard scaleFactor < 1, let scaledBitmapRep = bitmapRep.copy() as? NSBitmapImageRep else {
            return NSImage(size: bitmapRep.size, flipped: false) { rect in
                bitmapRep.draw(in: rect)
            }
        }
        
        scaledBitmapRep.size = scaledSize
        return NSImage(size: scaledSize, flipped: false) { rect in
            scaledBitmapRep.draw(in: rect)
        }
    }
}

#endif

You'll need some additional code to wrap it in a NSHostingController and get the sizeThatFits

@jflan-dd
Copy link
Contributor Author

@andrewtheis Thanks for sharing this!
As a small nit-pick, is it possible to use plain .pullback instead of .asyncPullback since I don't see any asynchronous work happening via a callback?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants