From 89f9003861e3c2b4f932b85a2792c253082351b1 Mon Sep 17 00:00:00 2001 From: Itay Date: Wed, 23 Apr 2025 16:31:27 -0300 Subject: [PATCH 01/11] Add `uniqueName` property and update attachment naming in SnapshotTest --- .../SnapshotPreviewsCore.swift | 28 +++++++++++++++++-- Sources/SnapshottingTests/SnapshotTest.swift | 6 +--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index cb39f12..991165b 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -91,12 +91,15 @@ public struct PreviewType: Hashable, Identifiable { self.line = nil self.previews = A._allPreviews.map { Preview(preview: $0, type: A.self) } self.platform = A.platform + + // type names are unique (Module.StructName_Previews) + self.uniqueName = typeName } #if compiler(>=5.9) @available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) @MainActor - init?(typeName: String, registry: A.Type) { + init?(typeName: String, registry: A.Type, uniqueName: String) { self.typeName = typeName self.fileID = A.fileID self.line = A.line @@ -105,6 +108,7 @@ public struct PreviewType: Hashable, Identifiable { } self.previews = [preview] self.platform = nil + self.uniqueName = typeName } #endif @@ -141,6 +145,7 @@ public struct PreviewType: Hashable, Identifiable { public let typeName: String public var previews: [Preview] public let platform: PreviewPlatform? + public var uniqueName: String = "" } // The enum provides a namespace @@ -205,8 +210,23 @@ public enum FindPreviews { shouldInclude: (String, String) -> Bool = { _, _ in true }, willAccess: (String) -> Void = { _ in }) -> [PreviewType] { - return getPreviewTypes() + let previewTypes = getPreviewTypes() .filter { shouldInclude($0.name, $0.proto) } + + // Helper to diff Previews created using #Preview macros + var previewCountForFileId: [String: Int] = [:] + +#if compiler(>=5.9) + if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { + for preview in previewTypes where preview.proto == "PreviewRegistry" { + willAccess(preview.name) + let registryType = unsafeBitCast(preview.accessor(), to: Any.Type.self) as! any PreviewRegistry.Type + previewCountForFileId[registryType.fileID, default: 0] += 1 + } + } +#endif + + return previewTypes .compactMap { conformance -> PreviewType? in let (name, accessor, proto) = conformance willAccess(name) @@ -218,7 +238,9 @@ public enum FindPreviews { #if compiler(>=5.9) if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { let previewRegistry = unsafeBitCast(accessor(), to: Any.Type.self) as! any PreviewRegistry.Type - return PreviewType(typeName: name, registry: previewRegistry) + let fileId = previewRegistry.fileID + let uniqueName = (previewCountForFileId[fileId] ?? 0) > 1 ? "\(fileId):\(previewRegistry.line)" : fileId + return PreviewType(typeName: name, registry: previewRegistry, uniqueName: uniqueName) } #endif return nil diff --git a/Sources/SnapshottingTests/SnapshotTest.swift b/Sources/SnapshottingTests/SnapshotTest.swift index f7a765d..9690f72 100644 --- a/Sources/SnapshottingTests/SnapshotTest.swift +++ b/Sources/SnapshottingTests/SnapshotTest.swift @@ -98,13 +98,9 @@ open class SnapshotTest: PreviewBaseTest, PreviewFilters { return } - var typeFileName = previewType.displayName - if let fileId = previewType.fileID, let lineNumber = previewType.line { - typeFileName = Self.previewCountForFileId[fileId]! > 1 ? "\(fileId):\(lineNumber)" : fileId - } do { let attachment = try XCTAttachment(image: result.image.get()) - attachment.name = "\(typeFileName)_\(preview.displayName ?? String(discoveredPreview.index))" + attachment.name = "\(previewType.uniqueName)_\(preview.displayName ?? String(discoveredPreview.index))" attachment.lifetime = .keepAlways add(attachment) } catch { From de00e214335ca3542c33501590f8cb5ea39beea8 Mon Sep 17 00:00:00 2001 From: Itay Date: Wed, 23 Apr 2025 16:54:16 -0300 Subject: [PATCH 02/11] Extract preview count calculation into a separate method --- .../SnapshotPreviewsCore.swift | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index 991165b..be380d4 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -214,17 +214,7 @@ public enum FindPreviews { .filter { shouldInclude($0.name, $0.proto) } // Helper to diff Previews created using #Preview macros - var previewCountForFileId: [String: Int] = [:] - -#if compiler(>=5.9) - if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { - for preview in previewTypes where preview.proto == "PreviewRegistry" { - willAccess(preview.name) - let registryType = unsafeBitCast(preview.accessor(), to: Any.Type.self) as! any PreviewRegistry.Type - previewCountForFileId[registryType.fileID, default: 0] += 1 - } - } -#endif + let previewCountForFileId = FindPreviews.calculatePreviewCountForFileId(previewTypes, willAccess) return previewTypes .compactMap { conformance -> PreviewType? in @@ -249,4 +239,21 @@ public enum FindPreviews { } } } + + @MainActor + private static func calculatePreviewCountForFileId(_ previewTypes: [LookupResult], _ willAccess: (String) -> Void) -> [String: Int] { + var previewCountForFileId: [String: Int] = [:] + +#if compiler(>=5.9) + if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { + for preview in previewTypes where preview.proto == "PreviewRegistry" { + willAccess(preview.name) + let registryType = unsafeBitCast(preview.accessor(), to: Any.Type.self) as! any PreviewRegistry.Type + previewCountForFileId[registryType.fileID, default: 0] += 1 + } + } +#endif + + return previewCountForFileId + } } From 82bc0441559097c67e4cc913380e05a5bcb37a08 Mon Sep 17 00:00:00 2001 From: Itay Date: Sat, 26 Apr 2025 19:16:45 -0300 Subject: [PATCH 03/11] Add uniqueName parameter to `Preview` and refactor PreviewType initialization. --- .../SnapshotPreviewsCore.swift | 207 ++++++++++++------ Sources/SnapshottingTests/SnapshotTest.swift | 2 +- 2 files changed, 145 insertions(+), 64 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index be380d4..6df9b46 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -2,7 +2,7 @@ import SwiftUI import PreviewsSupport public struct Preview: Identifiable { - init(preview: _Preview, type: P.Type) { + init(preview: _Preview, type: P.Type, uniqueName: String) { previewId = "\(preview.id)" index = preview.id orientation = preview.interfaceOrientation @@ -14,11 +14,12 @@ public struct Preview: Identifiable { P.previews } } + self.uniqueName = uniqueName } #if compiler(>=5.9) @available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) - init?(preview: DeveloperToolsSupport.Preview) { + init?(preview: DeveloperToolsSupport.Preview, uniqueName: String) { previewId = "0" var orientation: InterfaceOrientation = .portrait device = nil @@ -67,6 +68,7 @@ public struct Preview: Identifiable { } self._view = _view + self.uniqueName = uniqueName } #endif @@ -77,41 +79,22 @@ public struct Preview: Identifiable { public let index: Int public let device: PreviewDevice? public let layout: PreviewLayout + public let uniqueName: String private let _view: @MainActor () -> any View @MainActor public func view() -> any View { _view() } } -// Wraps PreviewProvider or PreviewRegistry public struct PreviewType: Hashable, Identifiable { - init(typeName: String, previewProvider: A.Type) { + init(typeName: String, fileId: String?, line: Int?, platform: PreviewPlatform?, previews: [Preview]) { self.typeName = typeName - self.fileID = nil - self.line = nil - self.previews = A._allPreviews.map { Preview(preview: $0, type: A.self) } - self.platform = A.platform - - // type names are unique (Module.StructName_Previews) - self.uniqueName = typeName + self.fileID = fileId + self.line = line + self.previews = previews + self.platform = platform } -#if compiler(>=5.9) - @available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) - @MainActor - init?(typeName: String, registry: A.Type, uniqueName: String) { - self.typeName = typeName - self.fileID = A.fileID - self.line = A.line - guard let internalPreview = try? A.makePreview(), let preview = Preview(preview: internalPreview) else { - return nil - } - self.previews = [preview] - self.platform = nil - self.uniqueName = typeName - } -#endif - public var module: String { String(typeName.split(separator: ".").first!) } @@ -145,7 +128,20 @@ public struct PreviewType: Hashable, Identifiable { public let typeName: String public var previews: [Preview] public let platform: PreviewPlatform? - public var uniqueName: String = "" +} + +private struct PreviewTypeInfo { + let name: String + let fileID: String? + let line: Int? + let previews: [InternalPreview] + let platform: PreviewPlatform? +} + +enum InternalPreview { + case previewProvider(_Preview, any SwiftUI.PreviewProvider.Type) + // Can't use DeveloperToolsSupport.Preview here because it's not available before iOS 17 + case previewRegistry(Any) } // The enum provides a namespace @@ -210,50 +206,135 @@ public enum FindPreviews { shouldInclude: (String, String) -> Bool = { _, _ in true }, willAccess: (String) -> Void = { _ in }) -> [PreviewType] { - let previewTypes = getPreviewTypes() + let rawPreviewTypes = getPreviewTypes() .filter { shouldInclude($0.name, $0.proto) } - // Helper to diff Previews created using #Preview macros - let previewCountForFileId = FindPreviews.calculatePreviewCountForFileId(previewTypes, willAccess) - - return previewTypes - .compactMap { conformance -> PreviewType? in - let (name, accessor, proto) = conformance - willAccess(name) - switch proto { - case "PreviewProvider": - let previewProvider = unsafeBitCast(accessor(), to: Any.Type.self) as! any PreviewProvider.Type - return PreviewType(typeName: name, previewProvider: previewProvider) - case "PreviewRegistry": - #if compiler(>=5.9) - if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { - let previewRegistry = unsafeBitCast(accessor(), to: Any.Type.self) as! any PreviewRegistry.Type - let fileId = previewRegistry.fileID - let uniqueName = (previewCountForFileId[fileId] ?? 0) > 1 ? "\(fileId):\(previewRegistry.line)" : fileId - return PreviewType(typeName: name, registry: previewRegistry, uniqueName: uniqueName) + let previewTypeInfos = rawPreviewTypes.compactMap { rawType -> PreviewTypeInfo? in + willAccess(rawType.name) + switch rawType.proto { + case "PreviewProvider": + let previewProvider = unsafeBitCast(rawType.accessor(), to: Any.Type.self) as! any PreviewProvider.Type + return PreviewTypeInfo( + name: rawType.name, + fileID: nil, + line: nil, + previews: previewProvider._allPreviews.map { .previewProvider($0, previewProvider.self) }, + platform: previewProvider.platform + ) + case "PreviewRegistry": + #if compiler(>=5.9) + if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { + let previewRegistry = unsafeBitCast(rawType.accessor(), to: Any.Type.self) as! any PreviewRegistry.Type + guard let internalPreview = try? previewRegistry.makePreview() else { + return nil } - #endif - return nil - default: - return nil + return PreviewTypeInfo( + name: rawType.name, + fileID: previewRegistry.fileID, + line: previewRegistry.line, + previews: [ .previewRegistry(internalPreview) ], + platform: nil + ) } + #endif + return nil + default: + return nil + } + } + + let previewCountForId = calculateIdToPreviewCount(previewTypeInfos) + + return generateFinalPreviewTypes( + previewTypeInfos: previewTypeInfos, + previewCountForId: previewCountForId + ) + } + + private static func calculateIdToPreviewCount(_ previewTypeInfos: [PreviewTypeInfo]) -> [String: Int] { + var previewCountForId: [String: Int] = [:] + for previewTypeInfo in previewTypeInfos { + for preview in previewTypeInfo.previews { + let possibleId = idForPreview(preview, previewTypeInfo) + previewCountForId[possibleId, default: 0] += 1 + } } + return previewCountForId } - @MainActor - private static func calculatePreviewCountForFileId(_ previewTypes: [LookupResult], _ willAccess: (String) -> Void) -> [String: Int] { - var previewCountForFileId: [String: Int] = [:] - + private static func idForPreview(_ preview: InternalPreview, _ previewTypeInfo: PreviewTypeInfo) -> String { + var id = previewTypeInfo.fileID ?? previewTypeInfo.name + let displayName = switch preview { + case .previewProvider(let internalPreview, _): + internalPreview.displayName + case .previewRegistry(let internalPreview): + Mirror(reflecting: internalPreview).descendant("displayName") as? String + } + if let displayName = displayName { + id += "_\(displayName)" + } + return id + } + + private static func generateFinalPreviewTypes( + previewTypeInfos: [PreviewTypeInfo], + previewCountForId: [String: Int] + ) -> [PreviewType] { + previewTypeInfos.map { previewTypeInfo in + let previews = previewTypeInfo.previews.compactMap { preview in + let possibleId = idForPreview(preview, previewTypeInfo) + let previewId = switch preview { + case .previewProvider(let internalPreview, _): + "\(internalPreview.id)" + case .previewRegistry(_): + "0" + } + let uniqueName = generateUniqueName( + possibleId: possibleId, + previewCount: previewCountForId[possibleId] ?? 0, + fileId: previewTypeInfo.fileID, + line: previewTypeInfo.line, + typeName: previewTypeInfo.name, + previewId: previewId + ) + switch preview { + case .previewProvider(let internalPreview, let previewType): + return Preview(preview: internalPreview, type: previewType, uniqueName: uniqueName) + case .previewRegistry(let anyValue): #if compiler(>=5.9) - if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { - for preview in previewTypes where preview.proto == "PreviewRegistry" { - willAccess(preview.name) - let registryType = unsafeBitCast(preview.accessor(), to: Any.Type.self) as! any PreviewRegistry.Type - previewCountForFileId[registryType.fileID, default: 0] += 1 + if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *), + let realPreview = anyValue as? DeveloperToolsSupport.Preview { + return Preview(preview: realPreview, uniqueName: uniqueName) + } +#endif + return nil + } } + return PreviewType( + typeName: previewTypeInfo.name, + fileId: previewTypeInfo.fileID, + line: previewTypeInfo.line, + platform: previewTypeInfo.platform, + previews: previews, + ) + } + } + + private static func generateUniqueName( + possibleId: String, + previewCount: Int, + fileId: String?, + line: Int?, + typeName: String, + previewId: String + ) -> String { + if previewCount == 1 { + return possibleId + } else if let fileId = fileId, let line = line { + return "\(fileId)_\(line)" + } else { + return "\(typeName)_\(previewId)" } -#endif - - return previewCountForFileId } } + diff --git a/Sources/SnapshottingTests/SnapshotTest.swift b/Sources/SnapshottingTests/SnapshotTest.swift index 9690f72..ff82817 100644 --- a/Sources/SnapshottingTests/SnapshotTest.swift +++ b/Sources/SnapshottingTests/SnapshotTest.swift @@ -100,7 +100,7 @@ open class SnapshotTest: PreviewBaseTest, PreviewFilters { do { let attachment = try XCTAttachment(image: result.image.get()) - attachment.name = "\(previewType.uniqueName)_\(preview.displayName ?? String(discoveredPreview.index))" + attachment.name = preview.uniqueName attachment.lifetime = .keepAlways add(attachment) } catch { From cd6a6a008851f4e7453d6aa8b604c07fb6b4abcd Mon Sep 17 00:00:00 2001 From: Itay Date: Sat, 26 Apr 2025 19:27:54 -0300 Subject: [PATCH 04/11] Code cleanup --- .../SnapshotPreviewsCore.swift | 87 ++++++++----------- 1 file changed, 35 insertions(+), 52 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index 6df9b46..20b79b7 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -87,12 +87,12 @@ public struct Preview: Identifiable { } public struct PreviewType: Hashable, Identifiable { - init(typeName: String, fileId: String?, line: Int?, platform: PreviewPlatform?, previews: [Preview]) { - self.typeName = typeName - self.fileID = fileId - self.line = line + fileprivate init(previewTypeInfo: PreviewTypeInfo, previews: [Preview]) { + self.typeName = previewTypeInfo.name + self.fileID = previewTypeInfo.fileID + self.line = previewTypeInfo.line self.previews = previews - self.platform = platform + self.platform = previewTypeInfo.platform } public var module: String { @@ -138,10 +138,28 @@ private struct PreviewTypeInfo { let platform: PreviewPlatform? } -enum InternalPreview { +private enum InternalPreview { case previewProvider(_Preview, any SwiftUI.PreviewProvider.Type) // Can't use DeveloperToolsSupport.Preview here because it's not available before iOS 17 case previewRegistry(Any) + + func getPreviewId() -> String { + switch self { + case .previewProvider(let internalPreview, _): + "\(internalPreview.id)" + case .previewRegistry(_): + "0" + } + } + + func getDisplayName() -> String? { + switch self { + case .previewProvider(let internalPreview, _): + internalPreview.displayName + case .previewRegistry(let internalPreview): + Mirror(reflecting: internalPreview).descendant("displayName") as? String + } + } } // The enum provides a namespace @@ -245,10 +263,7 @@ public enum FindPreviews { let previewCountForId = calculateIdToPreviewCount(previewTypeInfos) - return generateFinalPreviewTypes( - previewTypeInfos: previewTypeInfos, - previewCountForId: previewCountForId - ) + return generateFinalPreviewTypes(previewTypeInfos: previewTypeInfos, previewCountForId: previewCountForId) } private static func calculateIdToPreviewCount(_ previewTypeInfos: [PreviewTypeInfo]) -> [String: Int] { @@ -264,39 +279,20 @@ public enum FindPreviews { private static func idForPreview(_ preview: InternalPreview, _ previewTypeInfo: PreviewTypeInfo) -> String { var id = previewTypeInfo.fileID ?? previewTypeInfo.name - let displayName = switch preview { - case .previewProvider(let internalPreview, _): - internalPreview.displayName - case .previewRegistry(let internalPreview): - Mirror(reflecting: internalPreview).descendant("displayName") as? String - } - if let displayName = displayName { + if let displayName = preview.getDisplayName() { id += "_\(displayName)" } return id } - private static func generateFinalPreviewTypes( - previewTypeInfos: [PreviewTypeInfo], - previewCountForId: [String: Int] - ) -> [PreviewType] { + private static func generateFinalPreviewTypes(previewTypeInfos: [PreviewTypeInfo], previewCountForId: [String: Int]) -> [PreviewType] { previewTypeInfos.map { previewTypeInfo in let previews = previewTypeInfo.previews.compactMap { preview in let possibleId = idForPreview(preview, previewTypeInfo) - let previewId = switch preview { - case .previewProvider(let internalPreview, _): - "\(internalPreview.id)" - case .previewRegistry(_): - "0" - } - let uniqueName = generateUniqueName( - possibleId: possibleId, - previewCount: previewCountForId[possibleId] ?? 0, - fileId: previewTypeInfo.fileID, - line: previewTypeInfo.line, - typeName: previewTypeInfo.name, - previewId: previewId - ) + let previewId = preview.getPreviewId() + let previewCount = previewCountForId[possibleId] ?? 1 + let uniqueName = generateUniqueName(possibleId: possibleId, previewCount: previewCount, previewTypeInfo: previewTypeInfo, previewId: previewId) + switch preview { case .previewProvider(let internalPreview, let previewType): return Preview(preview: internalPreview, type: previewType, uniqueName: uniqueName) @@ -310,30 +306,17 @@ public enum FindPreviews { return nil } } - return PreviewType( - typeName: previewTypeInfo.name, - fileId: previewTypeInfo.fileID, - line: previewTypeInfo.line, - platform: previewTypeInfo.platform, - previews: previews, - ) + return PreviewType(previewTypeInfo: previewTypeInfo, previews: previews) } } - private static func generateUniqueName( - possibleId: String, - previewCount: Int, - fileId: String?, - line: Int?, - typeName: String, - previewId: String - ) -> String { + private static func generateUniqueName(possibleId: String, previewCount: Int, previewTypeInfo: PreviewTypeInfo, previewId: String) -> String { if previewCount == 1 { return possibleId - } else if let fileId = fileId, let line = line { + } else if let fileId = previewTypeInfo.fileID, let line = previewTypeInfo.line { return "\(fileId)_\(line)" } else { - return "\(typeName)_\(previewId)" + return "\(previewTypeInfo.name)_\(previewId)" } } } From 60c4f5395dcfb5f776475c30a1201148e851d8c1 Mon Sep 17 00:00:00 2001 From: Itay Date: Sat, 26 Apr 2025 19:32:03 -0300 Subject: [PATCH 05/11] Rename `idForPreview` to `possibleUniqueIdForPreview` --- .../SnapshotPreviewsCore.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index 20b79b7..d020521 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -270,25 +270,17 @@ public enum FindPreviews { var previewCountForId: [String: Int] = [:] for previewTypeInfo in previewTypeInfos { for preview in previewTypeInfo.previews { - let possibleId = idForPreview(preview, previewTypeInfo) + let possibleId = possibleUniqueIdForPreview(preview, previewTypeInfo) previewCountForId[possibleId, default: 0] += 1 } } return previewCountForId } - private static func idForPreview(_ preview: InternalPreview, _ previewTypeInfo: PreviewTypeInfo) -> String { - var id = previewTypeInfo.fileID ?? previewTypeInfo.name - if let displayName = preview.getDisplayName() { - id += "_\(displayName)" - } - return id - } - private static func generateFinalPreviewTypes(previewTypeInfos: [PreviewTypeInfo], previewCountForId: [String: Int]) -> [PreviewType] { previewTypeInfos.map { previewTypeInfo in let previews = previewTypeInfo.previews.compactMap { preview in - let possibleId = idForPreview(preview, previewTypeInfo) + let possibleId = possibleUniqueIdForPreview(preview, previewTypeInfo) let previewId = preview.getPreviewId() let previewCount = previewCountForId[possibleId] ?? 1 let uniqueName = generateUniqueName(possibleId: possibleId, previewCount: previewCount, previewTypeInfo: previewTypeInfo, previewId: previewId) @@ -310,6 +302,14 @@ public enum FindPreviews { } } + private static func possibleUniqueIdForPreview(_ preview: InternalPreview, _ previewTypeInfo: PreviewTypeInfo) -> String { + var id = previewTypeInfo.fileID ?? previewTypeInfo.name + if let displayName = preview.getDisplayName() { + id += "_\(displayName)" + } + return id + } + private static func generateUniqueName(possibleId: String, previewCount: Int, previewTypeInfo: PreviewTypeInfo, previewId: String) -> String { if previewCount == 1 { return possibleId From c03808e64ac9f735275402390d56bc7bdd462da9 Mon Sep 17 00:00:00 2001 From: Itay Date: Mon, 28 Apr 2025 14:24:08 -0300 Subject: [PATCH 06/11] Bump swift-tools-version --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index da0b2ea..f373c39 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription From 359f759098dc3283611c1d38fe0085b80faf512b Mon Sep 17 00:00:00 2001 From: Itay Date: Mon, 28 Apr 2025 14:25:18 -0300 Subject: [PATCH 07/11] Remove conditional compilation checks --- Examples/DemoApp/DemoModule/RowView.swift | 2 -- Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift | 6 ------ 2 files changed, 8 deletions(-) diff --git a/Examples/DemoApp/DemoModule/RowView.swift b/Examples/DemoApp/DemoModule/RowView.swift index 91e5dd6..5d20c99 100644 --- a/Examples/DemoApp/DemoModule/RowView.swift +++ b/Examples/DemoApp/DemoModule/RowView.swift @@ -45,7 +45,6 @@ public struct RowView: View { } } -#if compiler(>=5.9) #Preview("New test") { RowView( imageName: "product-image", @@ -53,7 +52,6 @@ public struct RowView: View { ratings: 4.2) .preferredColorScheme(.dark) } -#endif struct RowView_Previews: PreviewProvider { static var previews: some View { diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index d020521..0ba55f7 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -17,7 +17,6 @@ public struct Preview: Identifiable { self.uniqueName = uniqueName } -#if compiler(>=5.9) @available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) init?(preview: DeveloperToolsSupport.Preview, uniqueName: String) { previewId = "0" @@ -70,7 +69,6 @@ public struct Preview: Identifiable { self._view = _view self.uniqueName = uniqueName } -#endif public let id = UUID() public let previewId: String @@ -240,7 +238,6 @@ public enum FindPreviews { platform: previewProvider.platform ) case "PreviewRegistry": - #if compiler(>=5.9) if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { let previewRegistry = unsafeBitCast(rawType.accessor(), to: Any.Type.self) as! any PreviewRegistry.Type guard let internalPreview = try? previewRegistry.makePreview() else { @@ -254,7 +251,6 @@ public enum FindPreviews { platform: nil ) } - #endif return nil default: return nil @@ -289,12 +285,10 @@ public enum FindPreviews { case .previewProvider(let internalPreview, let previewType): return Preview(preview: internalPreview, type: previewType, uniqueName: uniqueName) case .previewRegistry(let anyValue): -#if compiler(>=5.9) if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *), let realPreview = anyValue as? DeveloperToolsSupport.Preview { return Preview(preview: realPreview, uniqueName: uniqueName) } -#endif return nil } } From 2957aa86fbacc640071f865bcbdc8c5de92a6218 Mon Sep 17 00:00:00 2001 From: Itay Date: Tue, 29 Apr 2025 11:42:50 -0300 Subject: [PATCH 08/11] Rename `PreviewType` to `PreviewTypeInfo` --- .../SnapshotPreviewsCore.swift | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index 0ba55f7..f9da50c 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -85,12 +85,12 @@ public struct Preview: Identifiable { } public struct PreviewType: Hashable, Identifiable { - fileprivate init(previewTypeInfo: PreviewTypeInfo, previews: [Preview]) { - self.typeName = previewTypeInfo.name - self.fileID = previewTypeInfo.fileID - self.line = previewTypeInfo.line + fileprivate init(previewInformation: PreviewInformation, previews: [Preview]) { + self.typeName = previewInformation.name + self.fileID = previewInformation.fileID + self.line = previewInformation.line self.previews = previews - self.platform = previewTypeInfo.platform + self.platform = previewInformation.platform } public var module: String { @@ -128,7 +128,7 @@ public struct PreviewType: Hashable, Identifiable { public let platform: PreviewPlatform? } -private struct PreviewTypeInfo { +private struct PreviewInformation { let name: String let fileID: String? let line: Int? @@ -225,12 +225,12 @@ public enum FindPreviews { let rawPreviewTypes = getPreviewTypes() .filter { shouldInclude($0.name, $0.proto) } - let previewTypeInfos = rawPreviewTypes.compactMap { rawType -> PreviewTypeInfo? in + let previewInfoArray = rawPreviewTypes.compactMap { rawType -> PreviewInformation? in willAccess(rawType.name) switch rawType.proto { case "PreviewProvider": let previewProvider = unsafeBitCast(rawType.accessor(), to: Any.Type.self) as! any PreviewProvider.Type - return PreviewTypeInfo( + return PreviewInformation( name: rawType.name, fileID: nil, line: nil, @@ -243,7 +243,7 @@ public enum FindPreviews { guard let internalPreview = try? previewRegistry.makePreview() else { return nil } - return PreviewTypeInfo( + return PreviewInformation( name: rawType.name, fileID: previewRegistry.fileID, line: previewRegistry.line, @@ -257,29 +257,29 @@ public enum FindPreviews { } } - let previewCountForId = calculateIdToPreviewCount(previewTypeInfos) + let previewCountForId = calculateIdToPreviewCount(previewInfoArray) - return generateFinalPreviewTypes(previewTypeInfos: previewTypeInfos, previewCountForId: previewCountForId) + return generateFinalPreviewTypes(previewInfoArray: previewInfoArray, previewCountForId: previewCountForId) } - private static func calculateIdToPreviewCount(_ previewTypeInfos: [PreviewTypeInfo]) -> [String: Int] { + private static func calculateIdToPreviewCount(_ previewInfoArray: [PreviewInformation]) -> [String: Int] { var previewCountForId: [String: Int] = [:] - for previewTypeInfo in previewTypeInfos { - for preview in previewTypeInfo.previews { - let possibleId = possibleUniqueIdForPreview(preview, previewTypeInfo) + for previewInformation in previewInfoArray { + for preview in previewInformation.previews { + let possibleId = possibleUniqueIdForPreview(preview, previewInformation) previewCountForId[possibleId, default: 0] += 1 } } return previewCountForId } - private static func generateFinalPreviewTypes(previewTypeInfos: [PreviewTypeInfo], previewCountForId: [String: Int]) -> [PreviewType] { - previewTypeInfos.map { previewTypeInfo in - let previews = previewTypeInfo.previews.compactMap { preview in - let possibleId = possibleUniqueIdForPreview(preview, previewTypeInfo) + private static func generateFinalPreviewTypes(previewInfoArray: [PreviewInformation], previewCountForId: [String: Int]) -> [PreviewType] { + previewInfoArray.map { previewInformation in + let previews = previewInformation.previews.compactMap { preview in + let possibleId = possibleUniqueIdForPreview(preview, previewInformation) let previewId = preview.getPreviewId() let previewCount = previewCountForId[possibleId] ?? 1 - let uniqueName = generateUniqueName(possibleId: possibleId, previewCount: previewCount, previewTypeInfo: previewTypeInfo, previewId: previewId) + let uniqueName = generateUniqueName(possibleId: possibleId, previewCount: previewCount, previewInformation: previewInformation, previewId: previewId) switch preview { case .previewProvider(let internalPreview, let previewType): @@ -292,25 +292,25 @@ public enum FindPreviews { return nil } } - return PreviewType(previewTypeInfo: previewTypeInfo, previews: previews) + return PreviewType(previewInformation: previewInformation, previews: previews) } } - private static func possibleUniqueIdForPreview(_ preview: InternalPreview, _ previewTypeInfo: PreviewTypeInfo) -> String { - var id = previewTypeInfo.fileID ?? previewTypeInfo.name + private static func possibleUniqueIdForPreview(_ preview: InternalPreview, _ previewInformation: PreviewInformation) -> String { + var id = previewInformation.fileID ?? previewInformation.name if let displayName = preview.getDisplayName() { id += "_\(displayName)" } return id } - private static func generateUniqueName(possibleId: String, previewCount: Int, previewTypeInfo: PreviewTypeInfo, previewId: String) -> String { + private static func generateUniqueName(possibleId: String, previewCount: Int, previewInformation: PreviewInformation, previewId: String) -> String { if previewCount == 1 { return possibleId - } else if let fileId = previewTypeInfo.fileID, let line = previewTypeInfo.line { + } else if let fileId = previewInformation.fileID, let line = previewInformation.line { return "\(fileId)_\(line)" } else { - return "\(previewTypeInfo.name)_\(previewId)" + return "\(previewInformation.name)_\(previewId)" } } } From 2bf9ed29e8c1c52f9683f9c9a8ecfbf2dd2b456c Mon Sep 17 00:00:00 2001 From: Itay Date: Tue, 29 Apr 2025 12:24:10 -0300 Subject: [PATCH 09/11] Add `DeveloperPreview` protocol and refactor code to use it instead of `DeveloperToolsSupport.Preview` --- .../SnapshotPreviewsCore.swift | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index f9da50c..6aefc85 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -1,6 +1,31 @@ import SwiftUI import PreviewsSupport +nonisolated protocol DeveloperPreview { + var displayName: String? { get } + var traits: [Any] { get } + var source: Any { get } +} + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) +extension DeveloperToolsSupport.Preview: DeveloperPreview { + private nonisolated var mirror: Mirror { + return Mirror(reflecting: self) + } + + var displayName: String? { + mirror.descendant("displayName") as? String + } + + var traits: [Any] { + mirror.descendant("traits") as! [Any] + } + + var source: Any { + mirror.descendant("source")! + } +} + public struct Preview: Identifiable { init(preview: _Preview, type: P.Type, uniqueName: String) { previewId = "\(preview.id)" @@ -17,14 +42,12 @@ public struct Preview: Identifiable { self.uniqueName = uniqueName } - @available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) - init?(preview: DeveloperToolsSupport.Preview, uniqueName: String) { + init?(preview: DeveloperPreview, uniqueName: String) { previewId = "0" var orientation: InterfaceOrientation = .portrait device = nil index = 0 - let preview = Mirror(reflecting: preview) - let traits = preview.descendant("traits")! as! [Any] + let traits = preview.traits var layout = PreviewLayout.device for t in traits { if let value = Mirror(reflecting: t).descendant("value") { @@ -41,8 +64,8 @@ public struct Preview: Identifiable { } self.orientation = orientation self.layout = layout - displayName = preview.descendant("displayName") as? String - let source = preview.descendant("source")! + displayName = preview.displayName + let source = preview.source let _view: @MainActor () -> any View if let source = source as? MakeViewProvider { _view = { @@ -138,8 +161,7 @@ private struct PreviewInformation { private enum InternalPreview { case previewProvider(_Preview, any SwiftUI.PreviewProvider.Type) - // Can't use DeveloperToolsSupport.Preview here because it's not available before iOS 17 - case previewRegistry(Any) + case previewRegistry(DeveloperPreview) func getPreviewId() -> String { switch self { @@ -155,7 +177,7 @@ private enum InternalPreview { case .previewProvider(let internalPreview, _): internalPreview.displayName case .previewRegistry(let internalPreview): - Mirror(reflecting: internalPreview).descendant("displayName") as? String + internalPreview.displayName } } } @@ -284,12 +306,8 @@ public enum FindPreviews { switch preview { case .previewProvider(let internalPreview, let previewType): return Preview(preview: internalPreview, type: previewType, uniqueName: uniqueName) - case .previewRegistry(let anyValue): - if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *), - let realPreview = anyValue as? DeveloperToolsSupport.Preview { - return Preview(preview: realPreview, uniqueName: uniqueName) - } - return nil + case .previewRegistry(let internalPreview): + return Preview(preview: internalPreview, uniqueName: uniqueName) } } return PreviewType(previewInformation: previewInformation, previews: previews) From 7d8faccc1890e8f551e82343bf10750a8498f6d6 Mon Sep 17 00:00:00 2001 From: Itay Date: Tue, 29 Apr 2025 12:31:13 -0300 Subject: [PATCH 10/11] Remove `nonisolated` --- Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index 6aefc85..dda2008 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -1,7 +1,7 @@ import SwiftUI import PreviewsSupport -nonisolated protocol DeveloperPreview { +protocol DeveloperPreview { var displayName: String? { get } var traits: [Any] { get } var source: Any { get } @@ -9,7 +9,7 @@ nonisolated protocol DeveloperPreview { @available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) extension DeveloperToolsSupport.Preview: DeveloperPreview { - private nonisolated var mirror: Mirror { + private var mirror: Mirror { return Mirror(reflecting: self) } From 8edbd5c279e7ba365b426154b1f1cd0f26452f51 Mon Sep 17 00:00:00 2001 From: Itay Date: Tue, 29 Apr 2025 12:36:13 -0300 Subject: [PATCH 11/11] Use nonisolated on variables instead of protocol --- Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index dda2008..da2beb8 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -2,14 +2,14 @@ import SwiftUI import PreviewsSupport protocol DeveloperPreview { - var displayName: String? { get } - var traits: [Any] { get } - var source: Any { get } + nonisolated var displayName: String? { get } + nonisolated var traits: [Any] { get } + nonisolated var source: Any { get } } @available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) extension DeveloperToolsSupport.Preview: DeveloperPreview { - private var mirror: Mirror { + private nonisolated var mirror: Mirror { return Mirror(reflecting: self) }