diff --git a/.github/workflows/build-xcframework.yml b/.github/workflows/build-xcframework.yml index fef508f0..c754c0e0 100644 --- a/.github/workflows/build-xcframework.yml +++ b/.github/workflows/build-xcframework.yml @@ -8,11 +8,21 @@ on: jobs: build: runs-on: macos-14 + strategy: + matrix: + xcode-version: [15.4, 16.1] steps: - name: Checkout code uses: actions/checkout@v2 - - name: Xcode select - run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer' + - name: Xcode select ${{ matrix.xcode-version }} + run: sudo xcode-select -s '/Applications/Xcode_${{ matrix.xcode-version }}.app/Contents/Developer' + - name: Get Swift Version + run: | + SWIFT_MAJOR_VERSION=$(swift --version 2>&1 | awk '/Apple Swift version/ { split($7, ver, "."); print ver[1]; exit }') + echo "Swift major version: $SWIFT_MAJOR_VERSION" + echo "SWIFT_VERSION=$SWIFT_MAJOR_VERSION.0" >> "$GITHUB_ENV" - name: Build xcframework run: sh build.sh + env: + SWIFT_VERSION: ${{ env.SWIFT_VERSION }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0871856..c278e205 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,12 +8,19 @@ on: jobs: build: runs-on: macos-14 - + strategy: + matrix: + xcode-version: [15.4, 16.1] steps: - name: Checkout code uses: actions/checkout@v2 - - name: Xcode select - run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer' + - name: Xcode select ${{ matrix.xcode-version }} + run: sudo xcode-select -s '/Applications/Xcode_${{ matrix.xcode-version }}.app/Contents/Developer' + - name: Get Swift Version + run: | + SWIFT_MAJOR_VERSION=$(swift --version 2>&1 | awk '/Apple Swift version/ { split($7, ver, "."); print ver[1]; exit }') + echo "Swift major version: $SWIFT_MAJOR_VERSION" + echo "SWIFT_VERSION=$SWIFT_MAJOR_VERSION.0" >> "$GITHUB_ENV" - name: Build TestApp run: cd Examples && xcodebuild build -scheme DemoApp -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -project DemoApp/DemoApp.xcodeproj - name: Build Snapshotting @@ -22,12 +29,20 @@ jobs: run: xcodebuild build -scheme SnapshottingTests -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' build-tvos: runs-on: macos-14 + strategy: + matrix: + xcode-version: [15.4, 16.1] steps: - name: Checkout code uses: actions/checkout@v2 - - name: Xcode select - run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer' + - name: Xcode select ${{ matrix.xcode-version }} + run: sudo xcode-select -s '/Applications/Xcode_${{ matrix.xcode-version }}.app/Contents/Developer' + - name: Get Swift Version + run: | + SWIFT_MAJOR_VERSION=$(swift --version 2>&1 | awk '/Apple Swift version/ { split($7, ver, "."); print ver[1]; exit }') + echo "Swift major version: $SWIFT_MAJOR_VERSION" + echo "SWIFT_VERSION=$SWIFT_MAJOR_VERSION.0" >> "$GITHUB_ENV" - name: Build PreviewGallery run: xcodebuild build -scheme PreviewGallery -sdk appletvsimulator -destination 'generic/platform=tvOS Simulator' - name: Build Snapshotting @@ -36,12 +51,20 @@ jobs: run: xcodebuild build -scheme SnapshottingTests -sdk appletvsimulator -destination 'generic/platform=tvOS Simulator' build-visionos: runs-on: macos-15 + strategy: + matrix: + xcode-version: [15.4, 16.1] steps: - name: Checkout code uses: actions/checkout@v2 - - name: Xcode select - run: sudo xcode-select -s '/Applications/Xcode_16.0.app/Contents/Developer' + - name: Xcode select ${{ matrix.xcode-version }} + run: sudo xcode-select -s '/Applications/Xcode_${{ matrix.xcode-version }}.app/Contents/Developer' + - name: Get Swift Version + run: | + SWIFT_MAJOR_VERSION=$(swift --version 2>&1 | awk '/Apple Swift version/ { split($7, ver, "."); print ver[1]; exit }') + echo "Swift major version: $SWIFT_MAJOR_VERSION" + echo "SWIFT_VERSION=$SWIFT_MAJOR_VERSION.0" >> "$GITHUB_ENV" - name: Build TestApp run: cd Examples && xcodebuild build -scheme DemoApp -sdk xrsimulator -destination 'generic/platform=visionOS Simulator' -project DemoApp/DemoApp.xcodeproj - name: Build Snapshotting @@ -50,12 +73,20 @@ jobs: run: xcodebuild build -scheme SnapshottingTests -sdk xrsimulator -destination 'generic/platform=visionOS Simulator' build-watchos: runs-on: macos-14 + strategy: + matrix: + xcode-version: [15.4, 16.1] steps: - name: Checkout code uses: actions/checkout@v2 - - name: Xcode select - run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer' + - name: Xcode select ${{ matrix.xcode-version }} + run: sudo xcode-select -s '/Applications/Xcode_${{ matrix.xcode-version }}.app/Contents/Developer' + - name: Get Swift Version + run: | + SWIFT_MAJOR_VERSION=$(swift --version 2>&1 | awk '/Apple Swift version/ { split($7, ver, "."); print ver[1]; exit }') + echo "Swift major version: $SWIFT_MAJOR_VERSION" + echo "SWIFT_VERSION=$SWIFT_MAJOR_VERSION.0" >> "$GITHUB_ENV" - name: Build Test Watch App 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 @@ -64,12 +95,20 @@ jobs: run: xcodebuild build -scheme SnapshottingTests -sdk watchsimulator -destination 'generic/platform=watchOS Simulator' build-macos: runs-on: macos-14 + strategy: + matrix: + xcode-version: [15.4, 16.1] steps: - name: Checkout code uses: actions/checkout@v2 - - name: Xcode select - run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer' + - name: Xcode select ${{ matrix.xcode-version }} + run: sudo xcode-select -s '/Applications/Xcode_${{ matrix.xcode-version }}.app/Contents/Developer' + - name: Get Swift Version + run: | + SWIFT_MAJOR_VERSION=$(swift --version 2>&1 | awk '/Apple Swift version/ { split($7, ver, "."); print ver[1]; exit }') + echo "Swift major version: $SWIFT_MAJOR_VERSION" + echo "SWIFT_VERSION=$SWIFT_MAJOR_VERSION.0" >> "$GITHUB_ENV" - name: Build PreviewGallery run: xcodebuild build -scheme PreviewGallery -sdk macosx -destination 'generic/platform=macOS' - name: Build Snapshotting @@ -78,11 +117,20 @@ jobs: run: xcodebuild build -scheme SnapshottingTests -sdk macosx -destination 'generic/platform=macOS' build-macos-catalyst: runs-on: macos-14 + strategy: + matrix: + xcode-version: [15.4, 16.1] + steps: - name: Checkout code uses: actions/checkout@v2 - - name: Xcode select - run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer' + - name: Xcode select ${{ matrix.xcode-version }} + run: sudo xcode-select -s '/Applications/Xcode_${{ matrix.xcode-version }}.app/Contents/Developer' + - name: Get Swift Version + run: | + SWIFT_MAJOR_VERSION=$(swift --version 2>&1 | awk '/Apple Swift version/ { split($7, ver, "."); print ver[1]; exit }') + echo "Swift major version: $SWIFT_MAJOR_VERSION" + echo "SWIFT_VERSION=$SWIFT_MAJOR_VERSION.0" >> "$GITHUB_ENV" - name: Build PreviewGallery run: xcodebuild build -scheme PreviewGallery -sdk macosx -destination 'platform=macOS,variant=Mac Catalyst' - name: Build Snapshotting diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0549022b..983866d9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -8,28 +8,41 @@ on: jobs: release: runs-on: macos-14 + strategy: + matrix: + xcode-version: [15.4, 16.1] steps: - name: Checkout code uses: actions/checkout@v2 - - name: Xcode select - run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer' + - name: Xcode select ${{ matrix.xcode-version }} + run: sudo xcode-select -s '/Applications/Xcode_${{ matrix.xcode-version }}.app/Contents/Developer' + - name: Get Swift Version + run: | + SWIFT_MAJOR_VERSION=$(swift --version 2>&1 | awk '/Apple Swift version/ { split($7, ver, "."); print ver[1]; exit }') + echo "Swift major version: $SWIFT_MAJOR_VERSION" + echo "SWIFT_MAJOR_VERSION=$SWIFT_MAJOR_VERSION" >> "$GITHUB_ENV" + echo "SWIFT_VERSION=$SWIFT_MAJOR_VERSION.0" >> "$GITHUB_ENV" + - name: Build xcframework + run: sh build.sh + env: + SWIFT_VERSION: ${{ env.SWIFT_VERSION }} - name: Build xcframework run: sh build.sh - name: Zip SnapshottingTests xcframework - run: zip -r SnapshottingTests.xcframework.zip SnapshottingTests.xcframework + run: zip -r SnapshottingTests_swift_${{ env.SWIFT_MAJOR_VERSION }}.xcframework.zip SnapshottingTests.xcframework - name: Zip PreviewGallery xcframework - run: zip -r PreviewGallery.xcframework.zip PreviewGallery.xcframework + run: zip -r PreviewGallery_swift_${{ env.SWIFT_MAJOR_VERSION }}.xcframework.zip PreviewGallery.xcframework - name: Zip preivews support - run: (cd PreviewsSupport && zip -r PreviewsSupport.xcframework.zip PreviewsSupport.xcframework) + run: (cd PreviewsSupport && zip -r PreviewsSupport_swift_${{ env.SWIFT_MAJOR_VERSION }}.xcframework.zip PreviewsSupport.xcframework) - name: Upload Artifact uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: files: | - PreviewGallery.xcframework.zip - SnapshottingTests.xcframework.zip - PreviewsSupport/PreviewsSupport.xcframework.zip + PreviewGallery_swift_${{ env.SWIFT_MAJOR_VERSION }}.xcframework.zip + SnapshottingTests_swift_${{ env.SWIFT_MAJOR_VERSION }}.xcframework.zip + PreviewsSupport_swift_${{ env.SWIFT_MAJOR_VERSION }}.xcframework.zip body: Release ${{ github.ref }} Automated release created by GitHub Actions. \ No newline at end of file diff --git a/Examples/DemoApp/DemoApp/TestViews/PreviewVariants.swift b/Examples/DemoApp/DemoApp/TestViews/PreviewVariants.swift index 7301612f..a31b4f8d 100644 --- a/Examples/DemoApp/DemoApp/TestViews/PreviewVariants.swift +++ b/Examples/DemoApp/DemoApp/TestViews/PreviewVariants.swift @@ -15,7 +15,7 @@ extension View { struct PreviewVariants: View { init( - modifiers: [NamedViewModifier] = .previewDefault, + modifiers: [NamedViewModifier], layout: PreviewLayout = .device, @ArrayBuilder views: () -> [PreviewView]) { @@ -23,6 +23,14 @@ struct PreviewVariants: View { self.layout = layout self.views = views() } + + @MainActor + init( + layout: PreviewLayout = .device, + @ArrayBuilder views: () -> [PreviewView]) + { + self.init(modifiers: .previewDefault, layout: layout, views: views) + } var body: some View { ForEach(modifiers) { modifier in @@ -73,6 +81,7 @@ extension NamedViewModifier { @available(watchOS, unavailable) @available(visionOS, unavailable) @available(tvOS, unavailable) + @MainActor static var accessibility: NamedViewModifier { .init(name: "Accessibility", value: { $0.emergeAccessibility(true) }) } @@ -84,6 +93,7 @@ extension NamedViewModifier { extension [NamedViewModifier] { /// The default named view modifiers in a ``PreviewVariants``. + @MainActor static var previewDefault: [NamedViewModifier] { #if os(iOS) if UserDefaults.standard.bool(forKey: "NSDoubleLocalizedStrings") { diff --git a/Package.swift b/Package.swift index 63cda1eb..34dd52ed 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/swhitty/FlyingFox.git", exact: "0.16.0"), - .package(url: "https://github.com/EmergeTools/AccessibilitySnapshot.git", exact: "1.0.2"), + .package(url: "https://github.com/EmergeTools/AccessibilitySnapshot.git", branch: "feature/swift_6_support"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/Sources/SnapshotPreferences/AccessibiltyPreference.swift b/Sources/SnapshotPreferences/AccessibiltyPreference.swift index 91917f88..a15def1f 100644 --- a/Sources/SnapshotPreferences/AccessibiltyPreference.swift +++ b/Sources/SnapshotPreferences/AccessibiltyPreference.swift @@ -15,7 +15,7 @@ struct AccessibilityPreferenceKey: PreferenceKey { } } - static var defaultValue: Bool? = nil + static let defaultValue: Bool? = nil } extension View { diff --git a/Sources/SnapshotPreferences/AppStoreSnapshotPreference.swift b/Sources/SnapshotPreferences/AppStoreSnapshotPreference.swift index 599b49c2..6699df8c 100644 --- a/Sources/SnapshotPreferences/AppStoreSnapshotPreference.swift +++ b/Sources/SnapshotPreferences/AppStoreSnapshotPreference.swift @@ -15,7 +15,7 @@ struct AppStoreSnapshotPreferenceKey: PreferenceKey { } } - static var defaultValue: Bool? = nil + static let defaultValue: Bool? = nil } extension View { diff --git a/Sources/SnapshotPreferences/EmergeModifierFinder.swift b/Sources/SnapshotPreferences/EmergeModifierFinder.swift index 0fa533cf..1f9d6c6a 100644 --- a/Sources/SnapshotPreferences/EmergeModifierFinder.swift +++ b/Sources/SnapshotPreferences/EmergeModifierFinder.swift @@ -13,10 +13,10 @@ import SnapshotSharedModels // The inserted test runner code finds these classes through ObjC runtime functions (NSClassFromString) // and Swift reflection (Mirror). -@objc(EmergeModifierState) -class EmergeModifierState: NSObject { +@objc(EmergeModifierState) @MainActor +final class EmergeModifierState: NSObject { - @objc + @MainActor @objc static let shared = EmergeModifierState() func reset() { @@ -35,23 +35,34 @@ class EmergeModifierState: NSObject { @objc(EmergeModifierFinder) class EmergeModifierFinder: NSObject { + @MainActor let finder: (any View) -> (any View) = { view in EmergeModifierState.shared.reset() return view .onPreferenceChange(ExpansionPreferenceKey.self, perform: { value in - EmergeModifierState.shared.expansionPreference = value + Task { @MainActor in + EmergeModifierState.shared.expansionPreference = value + } }) .onPreferenceChange(RenderingModePreferenceKey.self, perform: { value in - EmergeModifierState.shared.renderingMode = value + Task { @MainActor in + EmergeModifierState.shared.renderingMode = value + } }) .onPreferenceChange(PrecisionPreferenceKey.self, perform: { value in - EmergeModifierState.shared.precision = value + Task { @MainActor in + EmergeModifierState.shared.precision = value + } }) .onPreferenceChange(AccessibilityPreferenceKey.self, perform: { value in - EmergeModifierState.shared.accessibilityEnabled = value + Task { @MainActor in + EmergeModifierState.shared.accessibilityEnabled = value + } }) .onPreferenceChange(AppStoreSnapshotPreferenceKey.self, perform: { value in - EmergeModifierState.shared.appStoreSnapshot = value + Task { @MainActor in + EmergeModifierState.shared.appStoreSnapshot = value + } }) } } diff --git a/Sources/SnapshotPreferences/ExpansionPreference.swift b/Sources/SnapshotPreferences/ExpansionPreference.swift index 571609a7..f746b1a7 100644 --- a/Sources/SnapshotPreferences/ExpansionPreference.swift +++ b/Sources/SnapshotPreferences/ExpansionPreference.swift @@ -15,7 +15,7 @@ struct ExpansionPreferenceKey: PreferenceKey { } } - static var defaultValue: Bool? = nil + static let defaultValue: Bool? = nil } extension View { diff --git a/Sources/SnapshotPreferences/PrecisionPreference.swift b/Sources/SnapshotPreferences/PrecisionPreference.swift index a0e1237e..14839f78 100644 --- a/Sources/SnapshotPreferences/PrecisionPreference.swift +++ b/Sources/SnapshotPreferences/PrecisionPreference.swift @@ -13,7 +13,7 @@ struct PrecisionPreferenceKey: PreferenceKey { value = nextValue() } - static var defaultValue: Float? = nil + static let defaultValue: Float? = nil } extension View { diff --git a/Sources/SnapshotPreferences/RenderingModePreference.swift b/Sources/SnapshotPreferences/RenderingModePreference.swift index 0d94ac85..f96a2b6b 100644 --- a/Sources/SnapshotPreferences/RenderingModePreference.swift +++ b/Sources/SnapshotPreferences/RenderingModePreference.swift @@ -14,7 +14,7 @@ struct RenderingModePreferenceKey: PreferenceKey { value = nextValue() } - static var defaultValue: EmergeRenderingMode.RawValue? = nil + static let defaultValue: EmergeRenderingMode.RawValue? = nil } extension View { diff --git a/Sources/SnapshotPreviewsCore/ExpandingViewController.swift b/Sources/SnapshotPreviewsCore/ExpandingViewController.swift index 40c7e93e..097133b8 100644 --- a/Sources/SnapshotPreviewsCore/ExpandingViewController.swift +++ b/Sources/SnapshotPreviewsCore/ExpandingViewController.swift @@ -16,6 +16,7 @@ import SnapshotSharedModels public final class ExpandingViewController: UIHostingController, ScrollExpansionProviding { + @MainActor var supportsExpansion: Bool { rootView.supportsExpansion } @@ -112,15 +113,17 @@ public final class ExpandingViewController: UIHostingController= HeightExpansionTimeLimitInSeconds else { - return + let start = self.startTime, + Date().timeIntervalSince(start) >= self.HeightExpansionTimeLimitInSeconds else { + return } let timeoutError = RenderingError.expandingViewTimeout(CGSize(width: UIScreen.main.bounds.size.width, - height: firstScrollView?.visibleContentHeight ?? -1)) - NSLog("ExpandingViewController: Expanding Scroll View timed out. Current height is \(firstScrollView?.visibleContentHeight ?? -1)") - runCallback(timeoutError) + height: self.firstScrollView?.visibleContentHeight ?? -1)) + NSLog("ExpandingViewController: Expanding Scroll View timed out. Current height is \(self.firstScrollView?.visibleContentHeight ?? -1)") + self.runCallback(timeoutError) + } } } diff --git a/Sources/SnapshotPreviewsCore/ModifierFinder.swift b/Sources/SnapshotPreviewsCore/ModifierFinder.swift index 476aada7..e422c13d 100644 --- a/Sources/SnapshotPreviewsCore/ModifierFinder.swift +++ b/Sources/SnapshotPreviewsCore/ModifierFinder.swift @@ -9,14 +9,15 @@ import Foundation import SwiftUI import SnapshotSharedModels -private let modifierFinderClass = (NSClassFromString("EmergeModifierFinder") as? NSObject.Type)?.init() -private let finder = modifierFinderClass != nil ? Mirror(reflecting: modifierFinderClass!).descendant("finder") as? (any View) -> any View : nil -private let modifierState = NSClassFromString("EmergeModifierState") as? NSObject.Type -private let stateMirror = modifierState != nil ? Mirror( +@MainActor private let modifierFinderClass = (NSClassFromString("EmergeModifierFinder") as? NSObject.Type)?.init() +@MainActor private let finder = modifierFinderClass != nil ? Mirror(reflecting: modifierFinderClass!).descendant("finder") as? (any View) -> any View : nil +@MainActor private let modifierState = NSClassFromString("EmergeModifierState") as? NSObject.Type +@MainActor private let stateMirror = modifierState != nil ? Mirror( reflecting: modifierState! .perform(NSSelectorFromString("shared")) .takeUnretainedValue()) : nil +@MainActor public struct EmergeModifierView: View { private let internalView: AnyView diff --git a/Sources/SnapshotPreviewsCore/PreferredColorSchemeWrapper.swift b/Sources/SnapshotPreviewsCore/PreferredColorSchemeWrapper.swift index 35948787..b31954ee 100644 --- a/Sources/SnapshotPreviewsCore/PreferredColorSchemeWrapper.swift +++ b/Sources/SnapshotPreviewsCore/PreferredColorSchemeWrapper.swift @@ -24,8 +24,10 @@ public struct PreferredColorSchemeWrapper: View { public var body: some View { content .onPreferenceChange(PreferredColorSchemeKey.self, perform: { value in - preferredColorScheme = value - colorSchemeUpdater?(value) + Task { @MainActor in + preferredColorScheme = value + colorSchemeUpdater?(value) + } }) .environment(\.colorScheme, preferredColorScheme ?? colorScheme) .preferredColorScheme(nil) diff --git a/Sources/SnapshotPreviewsCore/ScrollExpansion.swift b/Sources/SnapshotPreviewsCore/ScrollExpansion.swift index 81c27811..d9282f47 100644 --- a/Sources/SnapshotPreviewsCore/ScrollExpansion.swift +++ b/Sources/SnapshotPreviewsCore/ScrollExpansion.swift @@ -15,23 +15,24 @@ import AppKit #endif protocol ContentHeightProviding { - var contentHeight: CGFloat { get } + @MainActor var contentHeight: CGFloat { get } - var visibleContentHeight: CGFloat { get } + @MainActor var visibleContentHeight: CGFloat { get } } protocol FirstScrollViewProviding { - var firstScrollView: ContentHeightProviding? { get } + @MainActor var firstScrollView: ContentHeightProviding? { get } } #if !os(watchOS) protocol ScrollExpansionProviding: AnyObject, FirstScrollViewProviding { - var previousHeight: CGFloat? { get set } - var heightAnchor: NSLayoutConstraint? { get } - var supportsExpansion: Bool { get } + @MainActor var previousHeight: CGFloat? { get set } + @MainActor var heightAnchor: NSLayoutConstraint? { get } + @MainActor var supportsExpansion: Bool { get } } extension ScrollExpansionProviding { + @MainActor func updateHeight(_ complete: (() -> Void)) { // If heightAnchor isn't set, this was a fixed size and we don't expand the scroll view guard let heightAnchor else { @@ -81,6 +82,7 @@ extension UIScrollView: ContentHeightProviding { } extension UIView: FirstScrollViewProviding { + @MainActor var firstScrollView: ContentHeightProviding? { var subviews = subviews while !subviews.isEmpty { @@ -100,6 +102,7 @@ extension UIView: FirstScrollViewProviding { } extension UIViewController: FirstScrollViewProviding { + @MainActor var firstScrollView: ContentHeightProviding? { view?.firstScrollView } diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index cb39f120..0799cc8a 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -9,8 +9,9 @@ public struct Preview: Identifiable { displayName = preview.displayName device = preview.device layout = preview.layout + let previewId = preview.id _view = { - ViewSelectorTree(SnapshotViewModel(index: preview.id)) { + ViewSelectorTree(SnapshotViewModel(index: previewId)) { P.previews } } @@ -18,6 +19,7 @@ public struct Preview: Identifiable { #if compiler(>=5.9) @available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) + @MainActor init?(preview: DeveloperToolsSupport.Preview) { previewId = "0" var orientation: InterfaceOrientation = .portrait @@ -51,12 +53,16 @@ public struct Preview: Identifiable { } else { #if canImport(UIKit) && !os(watchOS) if let source = source as? MakeUIViewProvider { - _view = { - return UIViewWrapper(source.makeView) + _view = { @MainActor @Sendable in + UIViewWrapper { + source.makeView() + } } } else if let source = source as? MakeViewControllerProvider { - _view = { - return UIViewControllerWrapper(source.makeViewController) + _view = { @MainActor @Sendable in + UIViewControllerWrapper { + source.makeViewController() + } } } else { return nil @@ -85,7 +91,7 @@ public struct Preview: Identifiable { // Wraps PreviewProvider or PreviewRegistry public struct PreviewType: Hashable, Identifiable { - init(typeName: String, previewProvider: A.Type) { + @MainActor init(typeName: String, previewProvider: A.Type) { self.typeName = typeName self.fileID = nil self.line = nil diff --git a/Sources/SnapshotPreviewsCore/UIKitRenderingStrategy.swift b/Sources/SnapshotPreviewsCore/UIKitRenderingStrategy.swift index 9f509a9a..236dc562 100644 --- a/Sources/SnapshotPreviewsCore/UIKitRenderingStrategy.swift +++ b/Sources/SnapshotPreviewsCore/UIKitRenderingStrategy.swift @@ -12,7 +12,7 @@ import SwiftUI public class UIKitRenderingStrategy: RenderingStrategy { - public init() { + @MainActor public init() { let windowScene = UIApplication.shared .connectedScenes .filter { $0.activationState == .foregroundActive } @@ -25,7 +25,7 @@ public class UIKitRenderingStrategy: RenderingStrategy { self.window = window } - private var windowScene: UIWindowScene? { + @MainActor private var windowScene: UIWindowScene? { window.windowScene } diff --git a/Sources/SnapshotPreviewsCore/UIViewWrapper.swift b/Sources/SnapshotPreviewsCore/UIViewWrapper.swift index 84fe71c0..cddf8104 100644 --- a/Sources/SnapshotPreviewsCore/UIViewWrapper.swift +++ b/Sources/SnapshotPreviewsCore/UIViewWrapper.swift @@ -13,7 +13,7 @@ import SwiftUI struct UIViewControllerWrapper: UIViewControllerRepresentable { let builder: @MainActor () -> UIViewController - init(_ builder: @escaping @MainActor () -> UIViewController) { + init(_ builder: @escaping @MainActor @Sendable () -> UIViewController) { self.builder = builder } @@ -28,7 +28,7 @@ struct UIViewWrapper: UIViewRepresentable { let builder: @MainActor () -> UIView - init(_ builder: @escaping @MainActor () -> UIView) { + init(_ builder: @escaping @MainActor @Sendable () -> UIView) { self.builder = builder } diff --git a/Sources/SnapshotPreviewsCore/View+Snapshot.swift b/Sources/SnapshotPreviewsCore/View+Snapshot.swift index e9d07ef3..c53f3c33 100644 --- a/Sources/SnapshotPreviewsCore/View+Snapshot.swift +++ b/Sources/SnapshotPreviewsCore/View+Snapshot.swift @@ -27,13 +27,16 @@ extension AccessibilityMarker: AccessibilityMark { return .frame(frame) case .path(let path): return .path(path) + @unknown default: + fatalError("Unknown shape") } } } -private var _colorScheme: ColorScheme? = nil +@MainActor private var _colorScheme: ColorScheme? = nil extension View { + @MainActor public func makeExpandingView(layout: PreviewLayout, window: UIWindow) -> ExpandingViewController { UIView.setAnimationsEnabled(false) var wrappedView: any View = self.transaction { transaction in @@ -189,6 +192,7 @@ extension View { } extension CGSize { + @MainActor var requiresCoreAnimationSnapshot: Bool { height >= UIScreen.main.bounds.size.height * 2 } @@ -209,6 +213,8 @@ extension UIView { layer.layerForSnapshot.render(in: context) return true } + case .some(_): + fatalError("Unsupported rendering mode: \(String(describing: mode))") } } } @@ -220,6 +226,8 @@ extension EmergeRenderingMode { return .renderLayerInContext case .uiView: return .drawHierarchyInRect + @unknown default: + fatalError("Unkown a11y rendering mode: \(self)") } } } diff --git a/Sources/SnapshottingSwift/Initializer.swift b/Sources/SnapshottingSwift/Initializer.swift index 8a4dfc20..4ab095df 100644 --- a/Sources/SnapshottingSwift/Initializer.swift +++ b/Sources/SnapshottingSwift/Initializer.swift @@ -11,22 +11,30 @@ import UIKit #endif @objc -public class Initializer: NSObject { +public final class Initializer: NSObject, Sendable { - @objc + @objc @MainActor static public let shared = Initializer() override init() { super.init() #if !canImport(UIKit) || os(watchOS) - snapshots = Snapshots() + Task { @MainActor [weak self] in + guard let self else { return } + self.snapshots = Snapshots() + } #else NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] notification in - self?.snapshots = Snapshots() + Task { @MainActor [weak self] in + guard let self else { return } + self.snapshots = Snapshots() + } } #endif } + + @MainActor var snapshots: Snapshots? } diff --git a/Sources/SnapshottingSwift/Snapshots.swift b/Sources/SnapshottingSwift/Snapshots.swift index 56a1f428..360b8cde 100644 --- a/Sources/SnapshottingSwift/Snapshots.swift +++ b/Sources/SnapshottingSwift/Snapshots.swift @@ -23,10 +23,11 @@ extension SnapshotError: LocalizedError { } } +@MainActor class Snapshots { let server = HTTPServer(address: .loopback(port: 38824)) - public init() { + @MainActor public init() { #if canImport(UIKit) && !os(watchOS) && !os(visionOS) && !os(tvOS) renderingStrategy = UIKitRenderingStrategy() #elseif canImport(AppKit) && !targetEnvironment(macCatalyst) @@ -54,8 +55,8 @@ class Snapshots { static let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] static let resultsDir = documentsURL.appendingPathComponent("EMGSnapshots") - func startServer() async throws { - await server.appendRoute("GET /display/*") { [weak self] request in + @MainActor func startServer() async throws { + await server.appendRoute("GET /display/*") { @MainActor [weak self] request in let pathComponents = request.path.components(separatedBy: "/") guard let self, pathComponents.count > 3 else { return HTTPResponse(statusCode: .badRequest) @@ -94,7 +95,7 @@ class Snapshots { await server.appendRoute("GET /file") { request in await Self.writeClassNames() - return HTTPResponse(statusCode: .ok, body: Self.resultsDir.path.data(using: .utf8)!) + return await HTTPResponse(statusCode: .ok, body: Self.resultsDir.path.data(using: .utf8)!) } try await server.start() diff --git a/Sources/SnapshottingTests/AccessibilityPreviewTest.swift b/Sources/SnapshottingTests/AccessibilityPreviewTest.swift index 2ef258a2..ff9e2b03 100644 --- a/Sources/SnapshottingTests/AccessibilityPreviewTest.swift +++ b/Sources/SnapshottingTests/AccessibilityPreviewTest.swift @@ -24,7 +24,7 @@ open class AccessibilityPreviewTest: PreviewBaseTest { /// Override this method to provide a custom XCUIApplication instance if needed. /// /// - Returns: An instance of XCUIApplication. - open class func getApp() -> XCUIApplication { + @MainActor open class func getApp() -> XCUIApplication { XCUIApplication() } @@ -65,6 +65,8 @@ open class AccessibilityPreviewTest: PreviewBaseTest { /// - Returns: A Boolean value indicating whether the issue was handled. Return `true` if the issue was handled, `false` otherwise. @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) open func handle(issue: XCUIAccessibilityAuditIssue) -> Bool { false } + + nonisolated(unsafe) static var resultPath: String? private static func getDylibPath(dylibName: String) -> String? { let count = _dyld_image_count() @@ -109,17 +111,13 @@ open class AccessibilityPreviewTest: PreviewBaseTest { preconditionFailure("Invalid URL") } - var resultPath: String? let request = URLRequest(url: url) let group = DispatchGroup() group.enter() - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let data = data, let stringData = String(data: data, encoding: .utf8) { - resultPath = stringData - } + getResultsPath(request: request) { stringData in + resultPath = stringData group.leave() } - task.resume() guard group.wait(timeout: .now().advanced(by: .seconds(25))) == .success else { preconditionFailure("test timed out") } @@ -150,11 +148,12 @@ open class AccessibilityPreviewTest: PreviewBaseTest { let request = URLRequest(url: url) var resultData: Data? - let task = URLSession.shared.dataTask(with: request) { data, response, error in - resultData = data - expectation.fulfill() + getResultData(request: request) { data in + DispatchQueue.main.async { + resultData = data + expectation.fulfill() + } } - task.resume() waitForExpectations(timeout: 5) { error in if let error = error { @@ -193,4 +192,22 @@ open class AccessibilityPreviewTest: PreviewBaseTest { return self?.handle(issue: issue) ?? false } } + + private class func getResultsPath(request: URLRequest, completion: @escaping @Sendable (String?) -> Void) { + let task = URLSession.shared.dataTask(with: request) { data, response, error in + var result: String? = nil + if let data = data, let stringData = String(data: data, encoding: .utf8) { + result = stringData + } + completion(result) + } + task.resume() + } + + private func getResultData(request: URLRequest, completion: @escaping @Sendable (Data?) -> Void) { + let task = URLSession.shared.dataTask(with: request) { data, response, error in + completion(data) + } + task.resume() + } } diff --git a/Sources/SnapshottingTests/PreviewBaseTest.swift b/Sources/SnapshottingTests/PreviewBaseTest.swift index e2a01cc5..a8a0f141 100644 --- a/Sources/SnapshottingTests/PreviewBaseTest.swift +++ b/Sources/SnapshottingTests/PreviewBaseTest.swift @@ -20,14 +20,14 @@ struct DiscoveredPreviewAndIndex { let index: Int } -var previews: [DiscoveredPreviewAndIndex] = [] +@MainActor var previews: [DiscoveredPreviewAndIndex] = [] @objc(EMGPreviewBaseTest) open class PreviewBaseTest: XCTestCase { - static var signatureCreator: NSObject? + @MainActor static var signatureCreator: NSObject? - @objc + @objc @MainActor static func swizzle(_ signatureCreator: NSObject) { self.signatureCreator = signatureCreator let originalSelector = NSSelectorFromString("testInvocations") diff --git a/Sources/SnapshottingTests/PreviewLayoutTest.swift b/Sources/SnapshottingTests/PreviewLayoutTest.swift index 7947c2f2..ab7db446 100644 --- a/Sources/SnapshottingTests/PreviewLayoutTest.swift +++ b/Sources/SnapshottingTests/PreviewLayoutTest.swift @@ -32,7 +32,7 @@ open class PreviewLayoutTest: PreviewBaseTest, PreviewFilters { nil } - static private var previews: [PreviewType] = [] + @MainActor static private var previews: [PreviewType] = [] /// Discovers all relevant previews based on inclusion and exclusion filters. Subclasses should NOT override this method. /// diff --git a/Sources/SnapshottingTests/SnapshotTest.swift b/Sources/SnapshottingTests/SnapshotTest.swift index f87d9f80..a356bd39 100644 --- a/Sources/SnapshottingTests/SnapshotTest.swift +++ b/Sources/SnapshottingTests/SnapshotTest.swift @@ -36,7 +36,7 @@ open class SnapshotTest: PreviewBaseTest, PreviewFilters { /// /// This method selects between UIKit, AppKit, and SwiftUI rendering strategies depending on the available frameworks and OS version. /// - Returns: A `RenderingStrategy` object suitable for the current environment. - private static func getRenderingStrategy() -> RenderingStrategy { + @MainActor private static func getRenderingStrategy() -> RenderingStrategy { #if canImport(UIKit) && !os(watchOS) && !os(visionOS) && !os(tvOS) return UIKitRenderingStrategy() #elseif canImport(AppKit) && !targetEnvironment(macCatalyst) @@ -49,9 +49,9 @@ open class SnapshotTest: PreviewBaseTest, PreviewFilters { } #endif } - private static let renderingStrategy = getRenderingStrategy() + @MainActor private static let renderingStrategy = getRenderingStrategy() - static private var previews: [SnapshotPreviewsCore.PreviewType] = [] + @MainActor static private var previews: [SnapshotPreviewsCore.PreviewType] = [] /// Discovers all relevant previews based on inclusion and exclusion filters. Subclasses should NOT override this method. /// diff --git a/build.sh b/build.sh index a657ac81..ae0d0c9f 100755 --- a/build.sh +++ b/build.sh @@ -26,7 +26,8 @@ build_framework() { -destination "$destination" \ BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ INSTALL_PATH='Library/Frameworks' \ - OTHER_SWIFT_FLAGS=-no-verify-emitted-module-interface + OTHER_SWIFT_FLAGS=-no-verify-emitted-module-interface \ + SWIFT_VERSION=${SWIFT_VERSION:-5.0} \ FRAMEWORK_MODULES_PATH="$XCODEBUILD_ARCHIVE_PATH/Products/Library/Frameworks/$scheme.framework/Modules" mkdir -p "$FRAMEWORK_MODULES_PATH"