Skip to content
Open
173 changes: 133 additions & 40 deletions Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SwiftUI
import PreviewsSupport

public struct Preview: Identifiable {
init<P: SwiftUI.PreviewProvider>(preview: _Preview, type: P.Type) {
init<P: SwiftUI.PreviewProvider>(preview: _Preview, type: P.Type, uniqueName: String) {
previewId = "\(preview.id)"
index = preview.id
orientation = preview.interfaceOrientation
Expand All @@ -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
Expand Down Expand Up @@ -67,6 +68,7 @@ public struct Preview: Identifiable {
}

self._view = _view
self.uniqueName = uniqueName
}
#endif

Expand All @@ -77,37 +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<A: PreviewProvider>(typeName: String, previewProvider: A.Type) {
self.typeName = typeName
self.fileID = nil
self.line = nil
self.previews = A._allPreviews.map { Preview(preview: $0, type: A.self) }
self.platform = A.platform
fileprivate init(previewTypeInfo: PreviewTypeInfo, previews: [Preview]) {
self.typeName = previewTypeInfo.name
self.fileID = previewTypeInfo.fileID
self.line = previewTypeInfo.line
self.previews = previews
self.platform = previewTypeInfo.platform
}

#if compiler(>=5.9)
@available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *)
@MainActor
init?<A: PreviewRegistry>(typeName: String, registry: A.Type) {
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
}
#endif

public var module: String {
String(typeName.split(separator: ".").first!)
}
Expand Down Expand Up @@ -143,6 +130,38 @@ public struct PreviewType: Hashable, Identifiable {
public let platform: PreviewPlatform?
}

private struct PreviewTypeInfo {
Copy link
Member

Choose a reason for hiding this comment

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

Having a PreviewType and also PreviewTypeInfo is a bit confusing, can we clean this up?

let name: String
let fileID: String?
let line: Int?
let previews: [InternalPreview]
let platform: PreviewPlatform?
}

private enum InternalPreview {
case previewProvider(_Preview, any SwiftUI.PreviewProvider.Type)
// Can't use DeveloperToolsSupport.Preview here because it's not available before iOS 17
Copy link
Member

Choose a reason for hiding this comment

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

Can you use a protocol that you conform DeveloperToolsSupport.Preview to in an extension?

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
public enum FindPreviews {
@available(iOS 16.0, macOS 13.0, tvOS 16.0, *)
Expand Down Expand Up @@ -205,26 +224,100 @@ public enum FindPreviews {
shouldInclude: (String, String) -> Bool = { _, _ in true },
willAccess: (String) -> Void = { _ in }) -> [PreviewType]
{
return getPreviewTypes()
let rawPreviewTypes = getPreviewTypes()
.filter { shouldInclude($0.name, $0.proto) }
.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
return PreviewType(typeName: name, registry: previewRegistry)

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 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 = possibleUniqueIdForPreview(preview, previewTypeInfo)
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)
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)
case .previewRegistry(let anyValue):
#if compiler(>=5.9)
Copy link
Member

Choose a reason for hiding this comment

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

Can we get rid of these now that Xcode 16 is out?

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(previewTypeInfo: previewTypeInfo, previews: previews)
}
}

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
} else if let fileId = previewTypeInfo.fileID, let line = previewTypeInfo.line {
return "\(fileId)_\(line)"
} else {
return "\(previewTypeInfo.name)_\(previewId)"
}
}
}

6 changes: 1 addition & 5 deletions Sources/SnapshottingTests/SnapshotTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = preview.uniqueName
attachment.lifetime = .keepAlways
add(attachment)
} catch {
Expand Down