diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0bd51a0e..c0871856 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -35,13 +35,13 @@ jobs:
- name: Build SnapshottingTests
run: xcodebuild build -scheme SnapshottingTests -sdk appletvsimulator -destination 'generic/platform=tvOS Simulator'
build-visionos:
- runs-on: macos-14
+ runs-on: macos-15
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Xcode select
- run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer'
+ run: sudo xcode-select -s '/Applications/Xcode_16.0.app/Contents/Developer'
- name: Build TestApp
run: cd Examples && xcodebuild build -scheme DemoApp -sdk xrsimulator -destination 'generic/platform=visionOS Simulator' -project DemoApp/DemoApp.xcodeproj
- name: Build Snapshotting
@@ -57,7 +57,7 @@ jobs:
- name: Xcode select
run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer'
- name: Build Test Watch App
- run: cd Examples && xcodebuild build -scheme 'Demo Watch App' -sdk watchsimulator -destination 'platform=watchOS Simulator,name=Apple Watch Series 9 (41mm)' -project DemoApp/DemoApp.xcodeproj CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
+ run: cd Examples && xcodebuild build -scheme 'Demo Watch App' -sdk watchsimulator -destination 'generic/platform=watchOS Simulator' -project DemoApp/DemoApp.xcodeproj CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
- name: Build Snapshotting
run: xcodebuild build -scheme Snapshotting -sdk watchsimulator -destination 'generic/platform=watchOS Simulator'
- name: Build SnapshottingTests
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 04567fe1..0549022b 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -16,8 +16,10 @@ jobs:
run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer'
- name: Build xcframework
run: sh build.sh
- - name: Zip xcframework
+ - name: Zip SnapshottingTests xcframework
run: zip -r SnapshottingTests.xcframework.zip SnapshottingTests.xcframework
+ - name: Zip PreviewGallery xcframework
+ run: zip -r PreviewGallery.xcframework.zip PreviewGallery.xcframework
- name: Zip preivews support
run: (cd PreviewsSupport && zip -r PreviewsSupport.xcframework.zip PreviewsSupport.xcframework)
- name: Upload Artifact
@@ -25,6 +27,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
+ PreviewGallery.xcframework.zip
SnapshottingTests.xcframework.zip
PreviewsSupport/PreviewsSupport.xcframework.zip
body:
diff --git a/Package.swift b/Package.swift
index a0509b26..26f5cbbe 100644
--- a/Package.swift
+++ b/Package.swift
@@ -10,6 +10,7 @@ let package = Package(
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "PreviewGallery",
+ type: .static, // Replace this to build dynamic
targets: ["PreviewGallery"]),
// Test library to import in your XCTest target.
// This is the only library that depends on XCTest.framework
diff --git a/README.md b/README.md
index 03b69054..c363c5bb 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
# 📸 SnapshotPreviews
-[](https://swiftpackageindex.com/EmergeTools/SnapshotPreviews-iOS)
-[](https://swiftpackageindex.com/EmergeTools/SnapshotPreviews-iOS)
+[](https://swiftpackageindex.com/EmergeTools/SnapshotPreviews)
+[](https://swiftpackageindex.com/EmergeTools/SnapshotPreviews)
+[](https://www.emergetools.com/app/example/ios/snapshotpreviews-ios.PreviewGallery/release?utm_campaign=badge-data)
+[](https://www.emergetools.com/app/example/ios/snapshotpreviews-ios.SnapshottingTests/release?utm_campaign=badge-data)
An all-in-one snapshot testing solution built on Xcode previews. Automatic browsable gallery of previews, and no-code snapshot generation with XCTest. Supports SwiftUI and UIKit previews using `PreviewProvider` or `#Preview` and works on all Apple platforms (iOS/macOS/watchOS/tvOS/visionOS).
@@ -16,7 +18,7 @@ An all-in-one snapshot testing solution built on Xcode previews. Automatic brows
`PreviewGallery` is an interactive UI built on top of snapshot extraction. It turns your Xcode previews into a gallery of components and features you can access from your application, for example in an internal settings screen. **Xcode is not required to view the previews.** You can use it to preview individual components (buttons/rows/icons/etc) or even entire interactive features.
-
+
The public API of PreviewGallery is a single SwiftUI `View` named `PreviewGallery`. Displaying this view gives you access to the full gallery. For example, you could add a button to open the gallery like this:
@@ -65,7 +67,7 @@ Note that there are no test functions; they are automatically added at runtime b
> [!NOTE]
> When you use Preview macros (`#Preview("Display Name")`) the name of the snapshot uses the file path and the name, for example: "MyModule/MyFile.swift:Display Name"
-
+
The [EmergeTools snapshot testing service](https://docs.emergetools.com/docs/snapshot-testing) generates snapshots and diffs them in the cloud to control for sources of flakiness, store images outside of git, and optimize test performance. `SnapshotTest` is for locally debugging these snapshot tests. You can also use `PreviewTest` to get code coverage of all previews in your unit test without generating PNGs. This will validate that previews do not crash (such as a missing @EnvironmentObject) but runs faster because it does not render the views to images.
@@ -101,10 +103,10 @@ See the demo app for a full example.
# Installation
-Add the package dependency to your Xcode project using the URL of this repository (https://github.com/EmergeTools/SnapshotPreviews-iOS).
+Add the package dependency to your Xcode project using the URL of this repository (https://github.com/EmergeTools/SnapshotPreviews).
-
+
Link your app to `PreviewGallery` and (optionally) to `SnapshotPreferences` to customize the behavior of snapshot generation.
@@ -150,7 +152,7 @@ Check `ProcessInfo.isRunningPeviews` to disable behavior you don’t want in pre
> [!TIP]
> Using PreviewVariants greatly simplifies snapshot testing, by ensuring a consistent set of variants and that every view is provided a name.
-Using multiple variants of the same view can ensure test coverage of all the ways users interact with your UI. Most are provided by SwiftUI, eg: `.dynamicTypeSize(.xxxLarge)`. There is one built into the package: `.emergeAccessibility(true)`. This function adds a visualization of voice over elements to your snapshot. You can automatically add variants using the [`PreviewVariants` View](https://github.com/EmergeTools/SnapshotPreviews-iOS/blob/main/DemoApp/DemoApp/TestViews/PreviewVariants.swift) that is demonstrated in the example app. It adds RTL, landscape, accessibility, dark mode and large text variants. You can use it like this:
+Using multiple variants of the same view can ensure test coverage of all the ways users interact with your UI. Most are provided by SwiftUI, eg: `.dynamicTypeSize(.xxxLarge)`. There is one built into the package: `.emergeAccessibility(true)`. This function adds a visualization of voice over elements to your snapshot. You can automatically add variants using the [`PreviewVariants` View](https://github.com/EmergeTools/SnapshotPreviews/blob/main/Examples/DemoApp/DemoApp/TestViews/PreviewVariants.swift) that is demonstrated in the example app. It adds RTL, landscape, accessibility, dark mode and large text variants. You can use it like this:
```swift
struct MyView_Previews: PreviewProvider {
@@ -172,7 +174,7 @@ struct MyView_Previews: PreviewProvider {
# Star History
-[](https://star-history.com/#EmergeTools/SnapshotPreviews-iOS&Date)
+[](https://star-history.com/#EmergeTools/SnapshotPreviews&Date)
# Related Reading
- [How to use VariadicView, SwiftUI's Private View API](https://www.emergetools.com/blog/posts/how-to-use-variadic-view): VariadicView is a core part of how multiple images are rendered for one PreviewProvider.
diff --git a/Sources/SnapshotPreferences/AppStoreSnapshotPreference.swift b/Sources/SnapshotPreferences/AppStoreSnapshotPreference.swift
new file mode 100644
index 00000000..599b49c2
--- /dev/null
+++ b/Sources/SnapshotPreferences/AppStoreSnapshotPreference.swift
@@ -0,0 +1,48 @@
+//
+// AppStoreSnapshotPreference.swift
+//
+//
+// Created by Trevor Elkins on 09/30/24.
+//
+
+import Foundation
+import SwiftUI
+
+struct AppStoreSnapshotPreferenceKey: PreferenceKey {
+ static func reduce(value: inout Bool?, nextValue: () -> Bool?) {
+ if value == nil {
+ value = nextValue()
+ }
+ }
+
+ static var defaultValue: Bool? = nil
+}
+
+extension View {
+ /// Marks a snapshot for use with our App Store screenshot editing tool. This should ideally be used with a
+ /// full-size preview matching one of our supported devices.
+ ///
+ /// - Note: This method is only available on iOS. It is unavailable on macOS, watchOS, visionOS, and tvOS.
+ ///
+ /// - Parameter enabled: A Boolean value that determines whether the snapshot is for an App Store screenshot.
+ /// If `nil`, the effect will default to `false`.
+ ///
+ /// - Returns: A view with the app store snapshot preference applied.
+ ///
+ /// # Example
+ /// ```swift
+ /// struct ContentView: View {
+ /// var body: some View {
+ /// Text("My App Store listing!")
+ /// .emergeAppStoreSnapshot(true)
+ /// }
+ /// }
+ /// ```
+ @available(macOS, unavailable)
+ @available(watchOS, unavailable)
+ @available(visionOS, unavailable)
+ @available(tvOS, unavailable)
+ public func emergeAppStoreSnapshot(_ enabled: Bool?) -> some View {
+ preference(key: AppStoreSnapshotPreferenceKey.self, value: enabled)
+ }
+}
diff --git a/Sources/SnapshotPreferences/EmergeModifierFinder.swift b/Sources/SnapshotPreferences/EmergeModifierFinder.swift
index 3e6ea8d7..0fa533cf 100644
--- a/Sources/SnapshotPreferences/EmergeModifierFinder.swift
+++ b/Sources/SnapshotPreferences/EmergeModifierFinder.swift
@@ -30,6 +30,7 @@ class EmergeModifierState: NSObject {
var renderingMode: EmergeRenderingMode.RawValue?
var precision: Float?
var accessibilityEnabled: Bool?
+ var appStoreSnapshot: Bool?
}
@objc(EmergeModifierFinder)
@@ -49,5 +50,8 @@ class EmergeModifierFinder: NSObject {
.onPreferenceChange(AccessibilityPreferenceKey.self, perform: { value in
EmergeModifierState.shared.accessibilityEnabled = value
})
+ .onPreferenceChange(AppStoreSnapshotPreferenceKey.self, perform: { value in
+ EmergeModifierState.shared.appStoreSnapshot = value
+ })
}
}
diff --git a/Sources/SnapshotPreviewsCore/AppKitRenderingStrategy.swift b/Sources/SnapshotPreviewsCore/AppKitRenderingStrategy.swift
index 6c11aafc..6444b434 100644
--- a/Sources/SnapshotPreviewsCore/AppKitRenderingStrategy.swift
+++ b/Sources/SnapshotPreviewsCore/AppKitRenderingStrategy.swift
@@ -47,16 +47,17 @@ public class AppKitRenderingStrategy: RenderingStrategy {
window.contentViewController = NSViewController()
window.setContentSize(AppKitContainer.defaultSize)
window.contentViewController = vc
- vc.rendered = { [weak vc] mode, precision, accessibilityEnabled in
+ vc.rendered = { [weak vc] mode, precision, accessibilityEnabled, appStoreSnapshot in
DispatchQueue.main.async {
let image = vc?.view.snapshot()
completion(
SnapshotResult(
- image: image != nil ? .success(image!) : .failure(SwiftUIRenderingError.renderingError),
+ image: image != nil ? .success(image!) : .failure(RenderingError.failedRendering(vc?.view.bounds.size ?? .zero)),
precision: precision,
accessibilityEnabled: accessibilityEnabled,
accessibilityMarkers: nil,
- colorScheme: _colorScheme))
+ colorScheme: _colorScheme,
+ appStoreSnapshot: appStoreSnapshot))
}
}
}
@@ -70,7 +71,7 @@ final class AppKitContainer: NSHostingController, ScrollExpa
var heightAnchor: NSLayoutConstraint?
var previousHeight: CGFloat?
- public var rendered: ((EmergeRenderingMode?, Float?, Bool?) -> Void)? {
+ public var rendered: ((EmergeRenderingMode?, Float?, Bool?, Bool?) -> Void)? {
didSet { didCall = false }
}
@@ -120,7 +121,7 @@ final class AppKitContainer: NSHostingController, ScrollExpa
guard !didCall else { return }
didCall = true
- rendered?(rootView.emergeRenderingMode, rootView.precision, rootView.accessibilityEnabled)
+ rendered?(rootView.emergeRenderingMode, rootView.precision, rootView.accessibilityEnabled, rootView.appStoreSnapshot)
}
override func updateViewConstraints() {
diff --git a/Sources/SnapshotPreviewsCore/ConformanceLookup.swift b/Sources/SnapshotPreviewsCore/ConformanceLookup.swift
index 1d13d2dd..bc07c647 100644
--- a/Sources/SnapshotPreviewsCore/ConformanceLookup.swift
+++ b/Sources/SnapshotPreviewsCore/ConformanceLookup.swift
@@ -11,6 +11,13 @@ import MachO
private func getTypeName(descriptor: UnsafePointer) -> String? {
let flags = descriptor.pointee.flags
var parentName: String? = nil
+ if descriptor.pointee.parent != 0 {
+ let parent = UnsafeRawPointer(descriptor).advanced(by: MemoryLayout.offset(of: \.parent)!).advanced(by: Int(descriptor.pointee.parent))
+ if abs(descriptor.pointee.parent) % 2 == 1 {
+ return nil
+ }
+ parentName = getTypeName(descriptor: parent.assumingMemoryBound(to: TargetModuleContextDescriptor.self))
+ }
switch flags.kind {
case .Module, .Enum, .Struct, .Class:
let name = UnsafeRawPointer(descriptor)
@@ -18,19 +25,12 @@ private func getTypeName(descriptor: UnsafePointer.offset(of: \.parent)!).advanced(by: Int(descriptor.pointee.parent))
- if abs(descriptor.pointee.parent) % 2 == 1 {
- return nil
- }
- parentName = getTypeName(descriptor: parent.assumingMemoryBound(to: TargetModuleContextDescriptor.self))
- }
if let parentName = parentName {
return "\(parentName).\(typeName)"
}
return typeName
default:
- return nil
+ return parentName
}
}
diff --git a/Sources/SnapshotPreviewsCore/ExpandingViewController.swift b/Sources/SnapshotPreviewsCore/ExpandingViewController.swift
index c0e613a4..40c7e93e 100644
--- a/Sources/SnapshotPreviewsCore/ExpandingViewController.swift
+++ b/Sources/SnapshotPreviewsCore/ExpandingViewController.swift
@@ -31,7 +31,7 @@ public final class ExpandingViewController: UIHostingController Void)? {
+ public var expansionSettled: ((EmergeRenderingMode?, Float?, Bool?, Bool?, Error?) -> Void)? {
didSet { didCall = false }
}
@@ -78,7 +78,7 @@ public final class ExpandingViewController: UIHostingController AnyView
+
+ init(_ modifier: M) {
+ let type = type(of: modifier)
+ let hash = String(describing: type)
+
+ _body = { content in
+ let cachedContext = PreviewModifierContextCache.contextCache[hash]
+ guard let typedContext = cachedContext as? M.Context else {
+ fatalError("Context type mismatch, expected: \(String(describing: M.Context.self)), got: \(String(describing: cachedContext.self))")
+ }
+ return AnyView(modifier.body(content: content, context: typedContext))
+ }
+ }
+
+ func body(content: PreviewModifier.Content, context: Void) -> AnyView {
+ return _body(content)
+ }
+}
diff --git a/Sources/SnapshotPreviewsCore/PreviewModifier/PreviewModifierContextCache.swift b/Sources/SnapshotPreviewsCore/PreviewModifier/PreviewModifierContextCache.swift
new file mode 100644
index 00000000..3287543d
--- /dev/null
+++ b/Sources/SnapshotPreviewsCore/PreviewModifier/PreviewModifierContextCache.swift
@@ -0,0 +1,13 @@
+//
+// PreviewModifierContextCache.swift
+// SnapshotPreviews
+//
+// Created by Itay Brenner on 26/9/24.
+//
+
+import SwiftUI
+
+@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, *)
+struct PreviewModifierContextCache {
+ static var contextCache: [String: Any] = [:]
+}
diff --git a/Sources/SnapshotPreviewsCore/PreviewModifier/View+PreviewModifier.swift b/Sources/SnapshotPreviewsCore/PreviewModifier/View+PreviewModifier.swift
new file mode 100644
index 00000000..7a03284a
--- /dev/null
+++ b/Sources/SnapshotPreviewsCore/PreviewModifier/View+PreviewModifier.swift
@@ -0,0 +1,21 @@
+//
+// View+PreviewModifier.swift
+// SnapshotPreviews
+//
+// Created by Itay Brenner on 25/9/24.
+//
+
+import SwiftUI
+import PreviewsSupport
+
+extension View {
+ @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, *)
+ func applyPreviewModifiers(_ modifiers: [any PreviewModifier]) -> some View {
+ var currentView: AnyView = AnyView(self)
+ for modifier in modifiers {
+ let viewModifier = PreviewModifierSupport.toViewModifier(modifier: AnyPreviewModifier(modifier))
+ currentView = AnyView(currentView.modifier(viewModifier))
+ }
+ return currentView
+ }
+}
diff --git a/Sources/SnapshotPreviewsCore/RenderingStrategy.swift b/Sources/SnapshotPreviewsCore/RenderingStrategy.swift
index 05f29fa3..0d7a7c94 100644
--- a/Sources/SnapshotPreviewsCore/RenderingStrategy.swift
+++ b/Sources/SnapshotPreviewsCore/RenderingStrategy.swift
@@ -41,13 +41,15 @@ public struct SnapshotResult {
precision: Float?,
accessibilityEnabled: Bool?,
accessibilityMarkers: [AccessibilityMark]?,
- colorScheme: ColorScheme?)
+ colorScheme: ColorScheme?,
+ appStoreSnapshot: Bool?)
{
self.image = image
self.precision = precision
self.accessibilityEnabled = accessibilityEnabled
self.accessibilityMarkers = accessibilityMarkers
self.colorScheme = colorScheme
+ self.appStoreSnapshot = appStoreSnapshot
}
public let image: Result
@@ -55,12 +57,17 @@ public struct SnapshotResult {
public let accessibilityEnabled: Bool?
public let accessibilityMarkers: [AccessibilityMark]?
public let colorScheme: ColorScheme?
+ public let appStoreSnapshot: Bool?
}
public protocol RenderingStrategy {
@MainActor func render(
preview: SnapshotPreviewsCore.Preview,
completion: @escaping (SnapshotResult) -> Void)
+
+ @MainActor func preparePreview(
+ preview: SnapshotPreviewsCore.Preview
+ ) async
}
private let testHandler: NSObject.Type? = NSClassFromString("EMGTestHandler") as? NSObject.Type
@@ -69,5 +76,13 @@ extension RenderingStrategy {
static func setup() {
testHandler?.perform(NSSelectorFromString("setup"))
}
+
+ @MainActor public func preparePreview(
+ preview: SnapshotPreviewsCore.Preview
+ ) async {
+ if #available(iOS 18.0, *) {
+ await preview.loadPreviewModifiers()
+ }
+ }
}
diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift
index dba656ef..8ea3a384 100644
--- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift
+++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift
@@ -14,6 +14,7 @@ public struct Preview: Identifiable {
P.previews
}
}
+ previewModifiers = []
}
#if compiler(>=5.9)
@@ -39,6 +40,16 @@ public struct Preview: Identifiable {
}
}
}
+ let previewModifiersArrays = traits.compactMap({ trait in
+ if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, *),
+ let value = Mirror(reflecting: trait).descendant("value"),
+ let value = value as? [(any PreviewModifier)] {
+ return value
+ }
+ return nil
+ }).flatMap { $0 }
+ self.previewModifiers = previewModifiersArrays
+
self.orientation = orientation
self.layout = layout
displayName = preview.descendant("displayName") as? String
@@ -77,9 +88,34 @@ public struct Preview: Identifiable {
public let index: Int
public let device: PreviewDevice?
public let layout: PreviewLayout
+ public let previewModifiers: [Any]
private let _view: @MainActor () -> any View
- @MainActor public func view() -> any View {
- _view()
+ @MainActor
+ public func view() -> any View {
+ if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, *) {
+ return _view().applyPreviewModifiers(previewModifiers as! [any PreviewModifier])
+ }
+ return _view()
+ }
+
+ @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, *)
+ @MainActor
+ func loadPreviewModifiers() async {
+ for previewModifier in previewModifiers {
+ guard let modifier = previewModifier as? (any PreviewModifier) else {
+ continue
+ }
+
+ let type = type(of: modifier)
+ let hash = String(describing: type)
+
+ guard PreviewModifierContextCache.contextCache[hash] == nil else {
+ continue
+ }
+
+ let context = try! await type.makeSharedContext()
+ PreviewModifierContextCache.contextCache[hash] = context
+ }
}
}
diff --git a/Sources/SnapshotPreviewsCore/SwiftUIRenderingStrategy.swift b/Sources/SnapshotPreviewsCore/SwiftUIRenderingStrategy.swift
index 185be759..aefc0c2d 100644
--- a/Sources/SnapshotPreviewsCore/SwiftUIRenderingStrategy.swift
+++ b/Sources/SnapshotPreviewsCore/SwiftUIRenderingStrategy.swift
@@ -8,10 +8,6 @@
import Foundation
import SwiftUI
-enum SwiftUIRenderingError: Error {
- case renderingError
-}
-
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, visionOS 1.0, *)
public class SwiftUIRenderingStrategy: RenderingStrategy {
@@ -23,25 +19,29 @@ public class SwiftUIRenderingStrategy: RenderingStrategy {
preview: SnapshotPreviewsCore.Preview,
completion: @escaping (SnapshotResult) -> Void)
{
- Self.setup()
- var view = preview.view()
- colorScheme = nil
- view = PreferredColorSchemeWrapper {
- AnyView(view)
- } colorSchemeUpdater: { [weak self] scheme in
- self?.colorScheme = scheme
- }
- let wrappedView = EmergeModifierView(wrapped: view)
- let renderer = ImageRenderer(content: wrappedView)
- #if canImport(UIKit)
- let image = renderer.uiImage
- #else
- let image = renderer.nsImage
- #endif
- if let image {
- completion(SnapshotResult(image: .success(image), precision: wrappedView.precision, accessibilityEnabled: wrappedView.accessibilityEnabled, accessibilityMarkers: [], colorScheme: colorScheme))
- } else {
- completion(SnapshotResult(image: .failure(SwiftUIRenderingError.renderingError), precision: wrappedView.precision, accessibilityEnabled: wrappedView.accessibilityEnabled, accessibilityMarkers: [], colorScheme: colorScheme))
+ Task { @MainActor in
+ Self.setup()
+ await preparePreview(preview: preview)
+ var view = preview.view()
+ colorScheme = nil
+ view = PreferredColorSchemeWrapper {
+ AnyView(view)
+ } colorSchemeUpdater: { [weak self] scheme in
+ self?.colorScheme = scheme
+ }
+ let wrappedView = EmergeModifierView(wrapped: view)
+ let renderer = ImageRenderer(content: wrappedView)
+#if canImport(UIKit)
+ let image = renderer.uiImage
+#else
+ let image = renderer.nsImage
+#endif
+ if let image {
+ completion(SnapshotResult(image: .success(image), precision: wrappedView.precision, accessibilityEnabled: wrappedView.accessibilityEnabled, accessibilityMarkers: [], colorScheme: colorScheme, appStoreSnapshot: wrappedView.appStoreSnapshot))
+ completion(SnapshotResult(image: .success(image), precision: wrappedView.precision, accessibilityEnabled: wrappedView.accessibilityEnabled, accessibilityMarkers: [], colorScheme: colorScheme, appStoreSnapshot: wrappedView.appStoreSnapshot))
+ } else {
+ completion(SnapshotResult(image: .failure(RenderingError.failedRendering(.zero)), precision: wrappedView.precision, accessibilityEnabled: wrappedView.accessibilityEnabled, accessibilityMarkers: [], colorScheme: colorScheme, appStoreSnapshot: wrappedView.appStoreSnapshot))
+ }
}
}
}
diff --git a/Sources/SnapshotPreviewsCore/UIKitRenderingStrategy.swift b/Sources/SnapshotPreviewsCore/UIKitRenderingStrategy.swift
index 35bc42a6..d059d2f0 100644
--- a/Sources/SnapshotPreviewsCore/UIKitRenderingStrategy.swift
+++ b/Sources/SnapshotPreviewsCore/UIKitRenderingStrategy.swift
@@ -37,23 +37,26 @@ public class UIKitRenderingStrategy: RenderingStrategy {
preview: SnapshotPreviewsCore.Preview,
completion: @escaping (SnapshotResult) -> Void
) {
+ Task { @MainActor in
Self.setup()
geometryUpdateError = nil
let targetOrientation = preview.orientation.toInterfaceOrientation()
+ await preparePreview(preview: preview)
guard #available(iOS 16.0, *), windowScene!.interfaceOrientation != targetOrientation else {
- performRender(preview: preview, completion: completion)
- return
+ performRender(preview: preview, completion: completion)
+ return
}
-
+
windowScene!.requestGeometryUpdate(.iOS(interfaceOrientations: targetOrientation.toInterfaceOrientationMask())) { error in
- NSLog("Rotation error handler: \(error) \(self.windowScene!.interfaceOrientation)")
- DispatchQueue.main.async {
- self.geometryUpdateError = error
- }
+ NSLog("Rotation error handler: \(error) \(self.windowScene!.interfaceOrientation)")
+ DispatchQueue.main.async {
+ self.geometryUpdateError = error
+ }
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
- self?.waitForOrientationChange(targetOrientation: targetOrientation, preview: preview, attempts: 50, completion: completion)
+ self?.waitForOrientationChange(targetOrientation: targetOrientation, preview: preview, attempts: 50, completion: completion)
}
+ }
}
@MainActor private func waitForOrientationChange(
@@ -63,12 +66,12 @@ public class UIKitRenderingStrategy: RenderingStrategy {
completion: @escaping (SnapshotResult) -> Void
) {
if let geometryUpdateError {
- completion(SnapshotResult(image: .failure(geometryUpdateError), precision: nil, accessibilityEnabled: nil, accessibilityMarkers: nil, colorScheme: nil))
+ completion(SnapshotResult(image: .failure(geometryUpdateError), precision: nil, accessibilityEnabled: nil, accessibilityMarkers: nil, colorScheme: nil, appStoreSnapshot: nil))
return
}
guard attempts > 0 else {
let timeoutError = NSError(domain: "OrientationChangeTimeout", code: 0, userInfo: [NSLocalizedDescriptionKey: "Orientation change timed out"])
- completion(SnapshotResult(image: .failure(timeoutError), precision: nil, accessibilityEnabled: nil, accessibilityMarkers: nil, colorScheme: nil))
+ completion(SnapshotResult(image: .failure(timeoutError), precision: nil, accessibilityEnabled: nil, accessibilityMarkers: nil, colorScheme: nil, appStoreSnapshot: nil))
return
}
diff --git a/Sources/SnapshotPreviewsCore/View+Snapshot.swift b/Sources/SnapshotPreviewsCore/View+Snapshot.swift
index bb4892af..e9d07ef3 100644
--- a/Sources/SnapshotPreviewsCore/View+Snapshot.swift
+++ b/Sources/SnapshotPreviewsCore/View+Snapshot.swift
@@ -5,12 +5,7 @@
// Created by Noah Martin on 12/22/22.
//
-#if canImport(UIKit) && !os(visionOS) && !os(watchOS) && !os(tvOS)
-import Foundation
-import SwiftUI
-import UIKit
-import AccessibilitySnapshotCore
-import SnapshotSharedModels
+import CoreFoundation
public enum RenderingError: Error {
case failedRendering(CGSize)
@@ -18,6 +13,13 @@ public enum RenderingError: Error {
case expandingViewTimeout(CGSize)
}
+#if canImport(UIKit) && !os(visionOS) && !os(watchOS) && !os(tvOS)
+import Foundation
+import SwiftUI
+import UIKit
+import AccessibilitySnapshotCore
+import SnapshotSharedModels
+
extension AccessibilityMarker: AccessibilityMark {
public var accessibilityShape: MarkerShape {
switch shape {
@@ -58,14 +60,14 @@ extension View {
async: Bool,
completion: @escaping (SnapshotResult) -> Void)
{
- controller.expansionSettled = { [weak controller, weak window] renderingMode, precision, accessibilityEnabled, error in
+ controller.expansionSettled = { [weak controller, weak window] renderingMode, precision, accessibilityEnabled, appStoreSnapshot, error in
guard let controller, let window, let containerVC = controller.parent else {
return
}
if let error {
DispatchQueue.main.async {
- completion(SnapshotResult(image: .failure(error), precision: precision, accessibilityEnabled: accessibilityEnabled, accessibilityMarkers: nil, colorScheme: _colorScheme))
+ completion(SnapshotResult(image: .failure(error), precision: precision, accessibilityEnabled: accessibilityEnabled, accessibilityMarkers: nil, colorScheme: _colorScheme, appStoreSnapshot: appStoreSnapshot))
}
return
}
@@ -73,7 +75,7 @@ extension View {
if async {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let imageResult = Self.takeSnapshot(layout: layout, renderingMode: renderingMode, rootVC: containerVC, targetView: controller.view)
- completion(SnapshotResult(image: imageResult.mapError { $0 }, precision: precision, accessibilityEnabled: accessibilityEnabled, accessibilityMarkers: nil, colorScheme: _colorScheme))
+ completion(SnapshotResult(image: imageResult.mapError { $0 }, precision: precision, accessibilityEnabled: accessibilityEnabled, accessibilityMarkers: nil, colorScheme: _colorScheme, appStoreSnapshot: appStoreSnapshot))
}
} else {
DispatchQueue.main.async {
@@ -99,10 +101,10 @@ extension View {
a11yView.sizeToFit()
let result = Self.takeSnapshot(layout: .sizeThatFits, renderingMode: renderingMode, rootVC: containerVC, targetView: a11yView)
a11yView.removeFromSuperview()
- completion(SnapshotResult(image: result.mapError { $0 }, precision: precision, accessibilityEnabled: accessibilityEnabled, accessibilityMarkers: elements, colorScheme: _colorScheme))
+ completion(SnapshotResult(image: result.mapError { $0 }, precision: precision, accessibilityEnabled: accessibilityEnabled, accessibilityMarkers: elements, colorScheme: _colorScheme, appStoreSnapshot: appStoreSnapshot))
} else {
let imageResult = Self.takeSnapshot(layout: layout, renderingMode: renderingMode, rootVC: containerVC, targetView: controller.view)
- completion(SnapshotResult(image: imageResult.mapError { $0 }, precision: precision, accessibilityEnabled: accessibilityEnabled, accessibilityMarkers: nil, colorScheme: _colorScheme))
+ completion(SnapshotResult(image: imageResult.mapError { $0 }, precision: precision, accessibilityEnabled: accessibilityEnabled, accessibilityMarkers: nil, colorScheme: _colorScheme, appStoreSnapshot: appStoreSnapshot))
}
}
}
diff --git a/Sources/SnapshottingTests/SnapshotTest.swift b/Sources/SnapshottingTests/SnapshotTest.swift
index 401efdeb..f87d9f80 100644
--- a/Sources/SnapshottingTests/SnapshotTest.swift
+++ b/Sources/SnapshottingTests/SnapshotTest.swift
@@ -49,7 +49,7 @@ open class SnapshotTest: PreviewBaseTest, PreviewFilters {
}
#endif
}
- private let renderingStrategy = getRenderingStrategy()
+ private static let renderingStrategy = getRenderingStrategy()
static private var previews: [SnapshotPreviewsCore.PreviewType] = []
@@ -79,7 +79,7 @@ open class SnapshotTest: PreviewBaseTest, PreviewFilters {
var result: SnapshotResult? = nil
let expectation = XCTestExpectation()
- renderingStrategy.render(preview: preview) { snapshotResult in
+ Self.renderingStrategy.render(preview: preview) { snapshotResult in
result = snapshotResult
expectation.fulfill()
}
diff --git a/build.sh b/build.sh
index b4adb90b..a657ac81 100755
--- a/build.sh
+++ b/build.sh
@@ -45,10 +45,18 @@ build_framework() {
# Update the Package.swift to build the library as dynamic instead of static
sed -i '' '/Replace this/ s/.*/type: .dynamic,/' Package.swift
+# Build SnapshottingTests
build_framework "iphonesimulator" "generic/platform=iOS Simulator" "SnapshottingTests"
build_framework "iphoneos" "generic/platform=iOS" "SnapshottingTests"
+# Build PreviewGallery
+build_framework "iphonesimulator" "generic/platform=iOS Simulator" "PreviewGallery"
+build_framework "iphoneos" "generic/platform=iOS" "PreviewGallery"
+
echo "Builds completed successfully."
rm -rf "SnapshottingTests.xcframework"
xcodebuild -create-xcframework -framework SnapshottingTests-iphonesimulator.xcarchive/Products/Library/Frameworks/SnapshottingTests.framework -framework SnapshottingTests-iphoneos.xcarchive/Products/Library/Frameworks/SnapshottingTests.framework -output SnapshottingTests.xcframework
+
+rm -rf "PreviewGallery.xcframework"
+xcodebuild -create-xcframework -framework PreviewGallery-iphonesimulator.xcarchive/Products/Library/Frameworks/PreviewGallery.framework -framework PreviewGallery-iphoneos.xcarchive/Products/Library/Frameworks/PreviewGallery.framework -output PreviewGallery.xcframework
\ No newline at end of file