From c835695ac346d37ab19570a60c8cee2dc961dfd0 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Wed, 30 Apr 2025 15:44:48 -0400 Subject: [PATCH 1/9] Add tests --- .../ConformanceLookup.swift | 184 +++++++++--------- .../SnapshotPreviewsCore.swift | 2 +- .../ConformanceLookupTests.swift | 28 +++ 3 files changed, 122 insertions(+), 92 deletions(-) create mode 100644 Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift diff --git a/Sources/SnapshotPreviewsCore/ConformanceLookup.swift b/Sources/SnapshotPreviewsCore/ConformanceLookup.swift index bc07c647..443947ea 100644 --- a/Sources/SnapshotPreviewsCore/ConformanceLookup.swift +++ b/Sources/SnapshotPreviewsCore/ConformanceLookup.swift @@ -8,70 +8,105 @@ import Foundation 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) - .advanced(by: MemoryLayout.offset(of: \.name)!) - .advanced(by: Int(descriptor.pointee.name)) - .assumingMemoryBound(to: CChar.self) - let typeName = String(cString: name) - if let parentName = parentName { - return "\(parentName).\(typeName)" - } - return typeName - default: - return parentName - } -} - public typealias LookupResult = (name: String, accessor: () -> UInt64, proto: String) -private func parseConformance(conformance: UnsafePointer) -> LookupResult? { - let flags = conformance.pointee.conformanceFlags - - guard case .DirectTypeDescriptor = flags.kind else { - return nil - } - - guard conformance.pointee.protocolDescriptor % 2 == 1 else { - return nil - } - let descriptorOffset = Int(conformance.pointee.protocolDescriptor & ~1) - let jumpPtr = UnsafeRawPointer(conformance).advanced(by: MemoryLayout.offset(of: \.protocolDescriptor)!).advanced(by: descriptorOffset) - let address = jumpPtr.load(as: UInt64.self) - - // Address will be 0 if the protocol is not available (such as only defined on a newer OS) - guard address != 0 else { - return nil +public class ConformanceLookup { + public static func getPreviewTypes() -> [LookupResult] { + let images = _dyld_image_count() + var types = [LookupResult]() + for i in 0...size { + let conformance = UnsafeRawPointer(sectData) + .advanced(by: Int(sectData.pointee)) + .assumingMemoryBound(to: ProtocolConformanceDescriptor.self) + if let result = parseConformance(conformance: conformance) { + types.append(result) + } + sectData = sectData.successor() + } + } + } + return types } - let protoPtr = UnsafeRawPointer(bitPattern: UInt(address))! - let proto = protoPtr.load(as: ProtocolDescriptor.self) - let namePtr = protoPtr.advanced(by: MemoryLayout.offset(of: \.name)!).advanced(by: Int(proto.name)) - let protocolName = String(cString: namePtr.assumingMemoryBound(to: CChar.self)) - guard ["PreviewProvider", "PreviewRegistry"].contains(protocolName) else { + + private static func parseConformance(conformance: UnsafePointer) -> LookupResult? { + let flags = conformance.pointee.conformanceFlags + + guard case .DirectTypeDescriptor = flags.kind else { + return nil + } + + guard conformance.pointee.protocolDescriptor % 2 == 1 else { + return nil + } + let descriptorOffset = Int(conformance.pointee.protocolDescriptor & ~1) + let jumpPtr = UnsafeRawPointer(conformance).advanced(by: MemoryLayout.offset(of: \.protocolDescriptor)!).advanced(by: descriptorOffset) + let address = jumpPtr.load(as: UInt64.self) + + // Address will be 0 if the protocol is not available (such as only defined on a newer OS) + guard address != 0 else { + return nil + } + let protoPtr = UnsafeRawPointer(bitPattern: UInt(address))! + let proto = protoPtr.load(as: ProtocolDescriptor.self) + let namePtr = protoPtr.advanced(by: MemoryLayout.offset(of: \.name)!).advanced(by: Int(proto.name)) + let protocolName = String(cString: namePtr.assumingMemoryBound(to: CChar.self)) + guard ["PreviewProvider", "PreviewRegistry"].contains(protocolName) else { + return nil + } + + let typeDescriptorPointer = UnsafeRawPointer(conformance).advanced(by: MemoryLayout.offset(of: \.nominalTypeDescriptor)!).advanced(by: Int(conformance.pointee.nominalTypeDescriptor)) + + let descriptor = typeDescriptorPointer.assumingMemoryBound(to: TargetModuleContextDescriptor.self) + if let name = getTypeName(descriptor: descriptor), + [ContextDescriptorKind.Class, ContextDescriptorKind.Struct, ContextDescriptorKind.Enum].contains(descriptor.pointee.flags.kind) { + let accessFunctionPointer = UnsafeRawPointer(descriptor).advanced(by: MemoryLayout.offset(of: \.accessFunction)!).advanced(by: Int(descriptor.pointee.accessFunction)) + let accessFunction = unsafeBitCast(accessFunctionPointer, to: (@convention(c) () -> UInt64).self) + return (name, accessFunction, protocolName) + } return nil } - - let typeDescriptorPointer = UnsafeRawPointer(conformance).advanced(by: MemoryLayout.offset(of: \.nominalTypeDescriptor)!).advanced(by: Int(conformance.pointee.nominalTypeDescriptor)) - - let descriptor = typeDescriptorPointer.assumingMemoryBound(to: TargetModuleContextDescriptor.self) - if let name = getTypeName(descriptor: descriptor), - [ContextDescriptorKind.Class, ContextDescriptorKind.Struct, ContextDescriptorKind.Enum].contains(descriptor.pointee.flags.kind) { - let accessFunctionPointer = UnsafeRawPointer(descriptor).advanced(by: MemoryLayout.offset(of: \.accessFunction)!).advanced(by: Int(descriptor.pointee.accessFunction)) - let accessFunction = unsafeBitCast(accessFunctionPointer, to: (@convention(c) () -> UInt64).self) - return (name, accessFunction, protocolName) + + private static 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) + .advanced(by: MemoryLayout.offset(of: \.name)!) + .advanced(by: Int(descriptor.pointee.name)) + .assumingMemoryBound(to: CChar.self) + let typeName = String(cString: name) + if let parentName = parentName { + return "\(parentName).\(typeName)" + } + return typeName + default: + return parentName + } } - return nil } #if arch(i386) || arch(arm) || arch(arm64_32) @@ -79,36 +114,3 @@ typealias mach_header_type = mach_header #else typealias mach_header_type = mach_header_64 #endif - -public func getPreviewTypes() -> [LookupResult] { - let images = _dyld_image_count() - var types = [LookupResult]() - for i in 0...size { - let conformance = UnsafeRawPointer(sectData) - .advanced(by: Int(sectData.pointee)) - .assumingMemoryBound(to: ProtocolConformanceDescriptor.self) - if let result = parseConformance(conformance: conformance) { - types.append(result) - } - sectData = sectData.successor() - } - } - } - return types -} diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index cb39f120..b7c3e883 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -205,7 +205,7 @@ public enum FindPreviews { shouldInclude: (String, String) -> Bool = { _, _ in true }, willAccess: (String) -> Void = { _ in }) -> [PreviewType] { - return getPreviewTypes() + return ConformanceLookup.getPreviewTypes() .filter { shouldInclude($0.name, $0.proto) } .compactMap { conformance -> PreviewType? in let (name, accessor, proto) = conformance diff --git a/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift b/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift new file mode 100644 index 00000000..2b040a62 --- /dev/null +++ b/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift @@ -0,0 +1,28 @@ +import SwiftUI +import XCTest + +@testable import SnapshotPreviewsCore + +struct TestView: View { + var body: some View { + Text("Hello world!") + } +} + +#Preview { + TestView() +} + +final class ConformanceLookupTests: XCTestCase { + func testExample() throws { + let types = ConformanceLookup.getPreviewTypes() + XCTAssertEqual(types.count, 1) + + let firstType = types.first! + XCTAssertEqual( + "SnapshotPreviewsTests.$s21SnapshotPreviewsTests0033ConformanceLookupTestsswift_DbGHjfMX11_0_33_AB2146FE95919420F4A1C0A89BE8EA36Ll7PreviewfMf_15PreviewRegistryfMu_", + firstType.name + ) + XCTAssertEqual("PreviewRegistry", firstType.proto) + } +} From 5687cc5410b1e44e4346166a4c2e655209e4c98d Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Wed, 30 Apr 2025 15:47:02 -0400 Subject: [PATCH 2/9] fix --- .../ConformanceLookupTests.swift | 15 ++------------- Tests/SnapshotPreviewsTests/TestView.swift | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 Tests/SnapshotPreviewsTests/TestView.swift diff --git a/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift b/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift index 2b040a62..c8f88c40 100644 --- a/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift +++ b/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift @@ -1,26 +1,15 @@ -import SwiftUI import XCTest @testable import SnapshotPreviewsCore -struct TestView: View { - var body: some View { - Text("Hello world!") - } -} - -#Preview { - TestView() -} - final class ConformanceLookupTests: XCTestCase { - func testExample() throws { + func testGetPreviewTypes() throws { let types = ConformanceLookup.getPreviewTypes() XCTAssertEqual(types.count, 1) let firstType = types.first! XCTAssertEqual( - "SnapshotPreviewsTests.$s21SnapshotPreviewsTests0033ConformanceLookupTestsswift_DbGHjfMX11_0_33_AB2146FE95919420F4A1C0A89BE8EA36Ll7PreviewfMf_15PreviewRegistryfMu_", + "SnapshotPreviewsTests.$s21SnapshotPreviewsTests0019TestViewswift_DJEEdfMX15_0_33_B5E96601318DE1EC85533DD88EB53190Ll7PreviewfMf_15PreviewRegistryfMu_", firstType.name ) XCTAssertEqual("PreviewRegistry", firstType.proto) diff --git a/Tests/SnapshotPreviewsTests/TestView.swift b/Tests/SnapshotPreviewsTests/TestView.swift new file mode 100644 index 00000000..461d04f0 --- /dev/null +++ b/Tests/SnapshotPreviewsTests/TestView.swift @@ -0,0 +1,18 @@ +// +// TestView.swift +// SnapshotPreviews +// +// Created by Trevor Elkins on 4/30/25. +// + +import SwiftUI + +struct TestView: View { + var body: some View { + Text("Hello world!") + } +} + +#Preview { + TestView() +} From ba24ecfc0fe2a5f2d69bc406f14c0f01336ee7b3 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Wed, 30 Apr 2025 16:55:34 -0400 Subject: [PATCH 3/9] progress --- Package.swift | 163 +++++++++++------- ...rFinder.swift => EmergeModifierView.swift} | 0 .../ETBrowserTests.swift | 12 -- .../EmergeModifierStateTests.swift | 72 ++++++++ 4 files changed, 177 insertions(+), 70 deletions(-) rename Sources/SnapshotPreviewsCore/{ModifierFinder.swift => EmergeModifierView.swift} (100%) delete mode 100644 Tests/SnapshotPreviewsTests/ETBrowserTests.swift create mode 100644 Tests/SnapshotPreviewsTests/EmergeModifierStateTests.swift diff --git a/Package.swift b/Package.swift index da0b2ea6..50fa8597 100644 --- a/Package.swift +++ b/Package.swift @@ -4,62 +4,109 @@ import PackageDescription let package = Package( - name: "SnapshotPreviews", - platforms: [.iOS(.v15), .macOS(.v12), .watchOS(.v9)], - products: [ - // 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 - .library( - name: "SnapshottingTests", - type: .static, // Replace this to build dynamic - targets: ["SnapshottingTests"]), - // Link the main app to this target to use custom snapshot settings - // This lib does not get inserted when running tests to avoid - // duplicate symbols. - .library( - name: "SnapshotPreferences", - targets: ["SnapshotPreferences"]), - // Core functionality for snapshotting exported from the internal package - .library( - name: "SnapshotPreviewsCore", - targets: ["SnapshotPreviewsCore"]), - // Dynamic library that your main app will have inserted to generate previews - .library( - name: "Snapshotting", - type: .dynamic, - targets: ["Snapshotting"]), - ], - 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/SimpleDebugger.git", exact: "1.0.0"), - ], - targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - // Target that provides the XCTest - .target(name: "SnapshottingTestsObjc", dependencies: [.product(name: "SimpleDebugger", package: "SimpleDebugger", condition: .when(platforms: [.iOS, .macOS, .macCatalyst]))]), - .target(name: "SnapshottingTests", dependencies: ["SnapshotPreviewsCore", "SnapshottingTestsObjc"]), - .target(name: "SnapshotSharedModels"), - // Core functionality - .target(name: "SnapshotPreviewsCore", dependencies: ["PreviewsSupport", "SnapshotSharedModels", .product(name: "AccessibilitySnapshotCore", package: "AccessibilitySnapshot", condition: .when(platforms: [.iOS, .macCatalyst]))]), - .target(name: "SnapshotPreferences", dependencies: ["SnapshotSharedModels"]), - // Inserted dylib - .target(name: "Snapshotting", dependencies: ["SnapshottingSwift"]), - // Swift code in the inserted dylib - .target(name: "SnapshottingSwift", dependencies: ["SnapshotPreviewsCore", .product(name: "FlyingFox", package: "FlyingFox")]), - .target(name: "PreviewGallery", dependencies: ["SnapshotPreviewsCore", "SnapshotPreferences"]), - .binaryTarget( - name: "PreviewsSupport", - path: "PreviewsSupport/PreviewsSupport.xcframework"), - .testTarget( - name: "SnapshotPreviewsTests", - dependencies: ["SnapshotPreviewsCore"]), - ], - cxxLanguageStandard: .cxx11 + name: "SnapshotPreviews", + platforms: [.iOS(.v15), .macOS(.v12), .watchOS(.v9)], + products: [ + // 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 + .library( + name: "SnapshottingTests", + type: .static, // Replace this to build dynamic + targets: ["SnapshottingTests"] + ), + // Link the main app to this target to use custom snapshot settings + // This lib does not get inserted when running tests to avoid + // duplicate symbols. + .library( + name: "SnapshotPreferences", + targets: ["SnapshotPreferences"] + ), + // Core functionality for snapshotting exported from the internal package + .library( + name: "SnapshotPreviewsCore", + targets: ["SnapshotPreviewsCore"] + ), + // Dynamic library that your main app will have inserted to generate previews + .library( + name: "Snapshotting", + type: .dynamic, + targets: ["Snapshotting"] + ), + ], + 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/SimpleDebugger.git", + exact: "1.0.0" + ), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + // Target that provides the XCTest + .target( + name: "SnapshottingTestsObjc", + dependencies: [ + .product( + name: "SimpleDebugger", + package: "SimpleDebugger", + condition: .when(platforms: [.iOS, .macOS, .macCatalyst]) + ) + ] + ), + .target( + name: "SnapshottingTests", + dependencies: ["SnapshotPreviewsCore", "SnapshottingTestsObjc"] + ), + .target(name: "SnapshotSharedModels"), + // Core functionality + .target( + name: "SnapshotPreviewsCore", + dependencies: [ + "PreviewsSupport", "SnapshotSharedModels", + .product( + name: "AccessibilitySnapshotCore", + package: "AccessibilitySnapshot", + condition: .when(platforms: [.iOS, .macCatalyst]) + ), + ] + ), + .target( + name: "SnapshotPreferences", + dependencies: ["SnapshotSharedModels"] + ), + // Inserted dylib + .target(name: "Snapshotting", dependencies: ["SnapshottingSwift"]), + // Swift code in the inserted dylib + .target( + name: "SnapshottingSwift", + dependencies: [ + "SnapshotPreviewsCore", + .product(name: "FlyingFox", package: "FlyingFox"), + ] + ), + .target( + name: "PreviewGallery", + dependencies: ["SnapshotPreviewsCore", "SnapshotPreferences"] + ), + .binaryTarget( + name: "PreviewsSupport", + path: "PreviewsSupport/PreviewsSupport.xcframework" + ), + .testTarget( + name: "SnapshotPreviewsTests", + dependencies: ["SnapshotPreviewsCore", "SnapshotPreferences"] + ), + ], + cxxLanguageStandard: .cxx11 ) diff --git a/Sources/SnapshotPreviewsCore/ModifierFinder.swift b/Sources/SnapshotPreviewsCore/EmergeModifierView.swift similarity index 100% rename from Sources/SnapshotPreviewsCore/ModifierFinder.swift rename to Sources/SnapshotPreviewsCore/EmergeModifierView.swift diff --git a/Tests/SnapshotPreviewsTests/ETBrowserTests.swift b/Tests/SnapshotPreviewsTests/ETBrowserTests.swift deleted file mode 100644 index e815f157..00000000 --- a/Tests/SnapshotPreviewsTests/ETBrowserTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import XCTest -@testable import SnapshotPreviewsCore - -final class SnapshotPreviewsTest: XCTestCase { - func testExample() throws { - // XCTest Documentation - // https://developer.apple.com/documentation/xctest - - // Defining Test Cases and Test Methods - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods - } -} diff --git a/Tests/SnapshotPreviewsTests/EmergeModifierStateTests.swift b/Tests/SnapshotPreviewsTests/EmergeModifierStateTests.swift new file mode 100644 index 00000000..8437d26f --- /dev/null +++ b/Tests/SnapshotPreviewsTests/EmergeModifierStateTests.swift @@ -0,0 +1,72 @@ +// +// EmergeModifierViewTests.swift +// SnapshotPreviews +// +// Created by Trevor Elkins on 4/30/25. +// + +import SwiftUI +import XCTest + +@testable import SnapshotPreferences +@testable import SnapshotSharedModels +@testable import SnapshotPreviewsCore + +final class EmergeModifierStateTests: XCTestCase { + + override func setUp() { + super.setUp() + EmergeModifierState.shared.reset() + } + + func testStoresRenderingMode() { + let view = makeBaseText().emergeRenderingMode(.uiView) + let state = state(for: view) + XCTAssertEqual(state.renderingMode, EmergeRenderingMode.uiView.rawValue) + } + + func testStoresPrecision() throws { + let view = makeBaseText().emergeSnapshotPrecision(0.95) + let state = state(for: view) + let precision = try XCTUnwrap(state.precision) + XCTAssertEqual(precision, 0.95, accuracy: .ulpOfOne) + } + + func testStoresExpansionPreference() { + let view = makeBaseText().emergeExpansion(false) + let state = state(for: view) + XCTAssertEqual(state.expansionPreference, false) + } + + func testStoresAccessibilityFlag() { + let view = makeBaseText().emergeAccessibility(true) + let state = state(for: view) + XCTAssertEqual(state.accessibilityEnabled, true) + } + + func testStoresAppStoreSnapshotFlag() { + let view = makeBaseText().emergeAppStoreSnapshot(true) + let state = state(for: view) + XCTAssertEqual(state.appStoreSnapshot, true) + } + + private func makeBaseText() -> Text { Text("Hello") } + + private func state( + for view: some View, + file: StaticString = #file, + line: UInt = #line + ) -> EmergeModifierState { + let wrapped = EmergeModifierView(wrapped: view) + let hosting = UIHostingController(rootView: wrapped) + + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = hosting + window.makeKeyAndVisible() + + // Give SwiftUI one tick to propagate preferences + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.01)) + + return EmergeModifierState.shared + } +} From f7db7d06c78b760f5cbe3fbd1bb42a45769cab61 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Wed, 30 Apr 2025 17:04:22 -0400 Subject: [PATCH 4/9] cleanup --- .../EmergeModifierView.swift | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/EmergeModifierView.swift b/Sources/SnapshotPreviewsCore/EmergeModifierView.swift index 476aada7..53d6caf4 100644 --- a/Sources/SnapshotPreviewsCore/EmergeModifierView.swift +++ b/Sources/SnapshotPreviewsCore/EmergeModifierView.swift @@ -6,24 +6,19 @@ // 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( - reflecting: modifierState! - .perform(NSSelectorFromString("shared")) - .takeUnretainedValue()) : nil +import SwiftUI public struct EmergeModifierView: View { private let internalView: AnyView + private let stateMirror: Mirror? init(wrapped: some View) { - let rootView = finder?(wrapped) - internalView = rootView != nil ? AnyView(rootView!) : AnyView(wrapped) + let root = RuntimeCache.finder?(wrapped) ?? wrapped + internalView = AnyView(root) + + stateMirror = RuntimeCache.stateMirror } public var body: some View { @@ -31,8 +26,9 @@ public struct EmergeModifierView: View { } var emergeRenderingMode: EmergeRenderingMode? { - let renderingMode = stateMirror?.descendant("renderingMode") as? EmergeRenderingMode.RawValue - return renderingMode != nil ? EmergeRenderingMode(rawValue: renderingMode!) : nil + let raw = + stateMirror?.descendant("renderingMode") as? EmergeRenderingMode.RawValue + return raw != nil ? EmergeRenderingMode(rawValue: raw!) : nil } var accessibilityEnabled: Bool? { @@ -51,3 +47,26 @@ public struct EmergeModifierView: View { stateMirror?.descendant("expansionPreference") as? Bool ?? true } } + +private enum RuntimeCache { + static let finder: ((any View) -> any View)? = { + guard + let finderClass = NSClassFromString("EmergeModifierFinder") + as? NSObject.Type, + let closure = Mirror(reflecting: finderClass.init()) + .descendant("finder") as? ((any View) -> any View) + else { return nil } + return closure + }() + + static let stateMirror: Mirror? = { + guard + let stateClass = NSClassFromString("EmergeModifierState") + as? NSObject.Type, + let shared = stateClass.perform( + NSSelectorFromString("shared") + )?.takeUnretainedValue() + else { return nil } + return Mirror(reflecting: shared) + }() +} From ceab84f66633ac414f121ef3e159fdb61386fb93 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Thu, 1 May 2025 12:05:12 -0400 Subject: [PATCH 5/9] more --- .../ConformanceLookupTests.swift | 2 +- .../EmergeModifierStateTests.swift | 10 +- .../ScrollExpansionTests.swift | 111 ++++++++++++++++++ 3 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 Tests/SnapshotPreviewsTests/ScrollExpansionTests.swift diff --git a/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift b/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift index c8f88c40..9228d37c 100644 --- a/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift +++ b/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift @@ -3,7 +3,7 @@ import XCTest @testable import SnapshotPreviewsCore final class ConformanceLookupTests: XCTestCase { - func testGetPreviewTypes() throws { + func test_getPreviewTypes() throws { let types = ConformanceLookup.getPreviewTypes() XCTAssertEqual(types.count, 1) diff --git a/Tests/SnapshotPreviewsTests/EmergeModifierStateTests.swift b/Tests/SnapshotPreviewsTests/EmergeModifierStateTests.swift index 8437d26f..e6e5b6e5 100644 --- a/Tests/SnapshotPreviewsTests/EmergeModifierStateTests.swift +++ b/Tests/SnapshotPreviewsTests/EmergeModifierStateTests.swift @@ -19,32 +19,32 @@ final class EmergeModifierStateTests: XCTestCase { EmergeModifierState.shared.reset() } - func testStoresRenderingMode() { + func test_storesRenderingMode() { let view = makeBaseText().emergeRenderingMode(.uiView) let state = state(for: view) XCTAssertEqual(state.renderingMode, EmergeRenderingMode.uiView.rawValue) } - func testStoresPrecision() throws { + func test_storesPrecision() throws { let view = makeBaseText().emergeSnapshotPrecision(0.95) let state = state(for: view) let precision = try XCTUnwrap(state.precision) XCTAssertEqual(precision, 0.95, accuracy: .ulpOfOne) } - func testStoresExpansionPreference() { + func test_storesExpansionPreference() { let view = makeBaseText().emergeExpansion(false) let state = state(for: view) XCTAssertEqual(state.expansionPreference, false) } - func testStoresAccessibilityFlag() { + func test_storesAccessibilityFlag() { let view = makeBaseText().emergeAccessibility(true) let state = state(for: view) XCTAssertEqual(state.accessibilityEnabled, true) } - func testStoresAppStoreSnapshotFlag() { + func test_storesAppStoreSnapshotFlag() { let view = makeBaseText().emergeAppStoreSnapshot(true) let state = state(for: view) XCTAssertEqual(state.appStoreSnapshot, true) diff --git a/Tests/SnapshotPreviewsTests/ScrollExpansionTests.swift b/Tests/SnapshotPreviewsTests/ScrollExpansionTests.swift new file mode 100644 index 00000000..68167a6c --- /dev/null +++ b/Tests/SnapshotPreviewsTests/ScrollExpansionTests.swift @@ -0,0 +1,111 @@ +// +// ScrollExpansionTests.swift +// SnapshotPreviews +// +// Created by Trevor Elkins on 5/1/25. +// + +import UIKit +import XCTest + +@testable import SnapshotPreviewsCore + +final class ScrollExpansionUIKitTests: XCTestCase { + func test_expandsWhenNeeded() { + let vc = makeVC(visible: 300, content: 500) + + vc.updateHeight {} + + XCTAssertEqual( + vc.heightAnchor?.constant, + 500, + "Height should increase by diff between content & visible" + ) + XCTAssertEqual( + vc.previousHeight, + 300, + "previousHeight must capture last visible height" + ) + } + + func test_doesNotExpandWhenNotSupported() { + let vc = makeVC(visible: 300, content: 500, supportsExpansion: false) + + vc.updateHeight {} + + XCTAssertEqual( + vc.heightAnchor?.constant, + 300, + "Constraint must stay unchanged when expansion is off" + ) + XCTAssertNil( + vc.previousHeight, + "previousHeight should remain nil when no expansion happens" + ) + } + + func test_doesNotExpandAgainIfAlreadyExpanded() { + let vc = makeVC(visible: 300, content: 500) + + vc.updateHeight {} + vc.updateHeight {} // second call — should be a no-op + + XCTAssertEqual( + vc.heightAnchor?.constant, + 500, + "Second invocation must not change the constant" + ) + } + + private func makeVC( + visible: CGFloat, + content: CGFloat, + supportsExpansion: Bool = true + ) -> ExpandableVC { + let vc = ExpandableVC( + visible: visible, + content: content, + initialConstant: visible + ) + vc.supportsExpansion = supportsExpansion + return vc + } +} + +private final class ExpandableVC: UIViewController, ScrollExpansionProviding { + + var previousHeight: CGFloat? + var supportsExpansion: Bool = true + + private(set) var heightConstraint: NSLayoutConstraint? + var heightAnchor: NSLayoutConstraint? { heightConstraint } + + init( + visible: CGFloat, + content: CGFloat, + initialConstant: CGFloat? = 100 + ) { + super.init(nibName: nil, bundle: nil) + + let scroll = UIScrollView() + scroll.contentSize = CGSize(width: 320, height: content) + scroll.frame = CGRect(x: 0, y: 0, width: 320, height: visible) + scroll.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scroll) + NSLayoutConstraint.activate([ + scroll.topAnchor.constraint(equalTo: view.topAnchor), + scroll.bottomAnchor.constraint(equalTo: view.bottomAnchor), + scroll.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scroll.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + + if let constant = initialConstant { + heightConstraint = view.heightAnchor.constraint(equalToConstant: constant) + heightConstraint?.isActive = true + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} From 24c99b66a0deec57cd2ebcfe64e213df3f6cf825 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Thu, 1 May 2025 18:00:43 -0400 Subject: [PATCH 6/9] Update modifier view --- .../EmergeModifierView.swift | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/EmergeModifierView.swift b/Sources/SnapshotPreviewsCore/EmergeModifierView.swift index 53d6caf4..a468a6da 100644 --- a/Sources/SnapshotPreviewsCore/EmergeModifierView.swift +++ b/Sources/SnapshotPreviewsCore/EmergeModifierView.swift @@ -11,14 +11,27 @@ import SwiftUI public struct EmergeModifierView: View { + private static let modifierFinderClass = + (NSClassFromString("EmergeModifierFinder") as? NSObject.Type)?.init() + private static let finder = + modifierFinderClass != nil + ? Mirror(reflecting: modifierFinderClass!).descendant("finder") + as? (any View) -> any View : nil + private static let modifierState = + NSClassFromString("EmergeModifierState") as? NSObject.Type + private static let stateMirror = + modifierState != nil + ? Mirror( + reflecting: modifierState! + .perform(NSSelectorFromString("shared")) + .takeUnretainedValue() + ) : nil + private let internalView: AnyView - private let stateMirror: Mirror? init(wrapped: some View) { - let root = RuntimeCache.finder?(wrapped) ?? wrapped - internalView = AnyView(root) - - stateMirror = RuntimeCache.stateMirror + let rootView = Self.finder?(wrapped) + internalView = rootView != nil ? AnyView(rootView!) : AnyView(wrapped) } public var body: some View { @@ -26,47 +39,26 @@ public struct EmergeModifierView: View { } var emergeRenderingMode: EmergeRenderingMode? { - let raw = - stateMirror?.descendant("renderingMode") as? EmergeRenderingMode.RawValue - return raw != nil ? EmergeRenderingMode(rawValue: raw!) : nil + let renderingMode = + Self.stateMirror?.descendant("renderingMode") + as? EmergeRenderingMode.RawValue + return renderingMode != nil + ? EmergeRenderingMode(rawValue: renderingMode!) : nil } var accessibilityEnabled: Bool? { - stateMirror?.descendant("accessibilityEnabled") as? Bool + Self.stateMirror?.descendant("accessibilityEnabled") as? Bool } var appStoreSnapshot: Bool? { - stateMirror?.descendant("appStoreSnapshot") as? Bool + Self.stateMirror?.descendant("appStoreSnapshot") as? Bool } var precision: Float? { - stateMirror?.descendant("precision") as? Float + Self.stateMirror?.descendant("precision") as? Float } var supportsExpansion: Bool { - stateMirror?.descendant("expansionPreference") as? Bool ?? true + Self.stateMirror?.descendant("expansionPreference") as? Bool ?? true } } - -private enum RuntimeCache { - static let finder: ((any View) -> any View)? = { - guard - let finderClass = NSClassFromString("EmergeModifierFinder") - as? NSObject.Type, - let closure = Mirror(reflecting: finderClass.init()) - .descendant("finder") as? ((any View) -> any View) - else { return nil } - return closure - }() - - static let stateMirror: Mirror? = { - guard - let stateClass = NSClassFromString("EmergeModifierState") - as? NSObject.Type, - let shared = stateClass.perform( - NSSelectorFromString("shared") - )?.takeUnretainedValue() - else { return nil } - return Mirror(reflecting: shared) - }() -} From d9b3646f51476ec729809bc9e4b15fbdfe0156c6 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Thu, 1 May 2025 18:04:07 -0400 Subject: [PATCH 7/9] revert breaking change --- .../ConformanceLookup.swift | 225 +++++++++++------- 1 file changed, 134 insertions(+), 91 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/ConformanceLookup.swift b/Sources/SnapshotPreviewsCore/ConformanceLookup.swift index 443947ea..7dc24059 100644 --- a/Sources/SnapshotPreviewsCore/ConformanceLookup.swift +++ b/Sources/SnapshotPreviewsCore/ConformanceLookup.swift @@ -8,109 +8,152 @@ import Foundation import MachO -public typealias LookupResult = (name: String, accessor: () -> UInt64, proto: String) +public typealias LookupResult = ( + name: String, accessor: () -> UInt64, proto: String +) -public class ConformanceLookup { - public static func getPreviewTypes() -> [LookupResult] { - let images = _dyld_image_count() - var types = [LookupResult]() - for i in 0...size { - let conformance = UnsafeRawPointer(sectData) - .advanced(by: Int(sectData.pointee)) - .assumingMemoryBound(to: ProtocolConformanceDescriptor.self) - if let result = parseConformance(conformance: conformance) { - types.append(result) - } - sectData = sectData.successor() +public func getPreviewTypes() -> [LookupResult] { + let images = _dyld_image_count() + var types = [LookupResult]() + for i in 0...size { + let conformance = UnsafeRawPointer(sectData) + .advanced(by: Int(sectData.pointee)) + .assumingMemoryBound(to: ProtocolConformanceDescriptor.self) + if let result = parseConformance(conformance: conformance) { + types.append(result) } + sectData = sectData.successor() } } - return types } - - private static func parseConformance(conformance: UnsafePointer) -> LookupResult? { - let flags = conformance.pointee.conformanceFlags - - guard case .DirectTypeDescriptor = flags.kind else { - return nil - } - - guard conformance.pointee.protocolDescriptor % 2 == 1 else { - return nil - } - let descriptorOffset = Int(conformance.pointee.protocolDescriptor & ~1) - let jumpPtr = UnsafeRawPointer(conformance).advanced(by: MemoryLayout.offset(of: \.protocolDescriptor)!).advanced(by: descriptorOffset) - let address = jumpPtr.load(as: UInt64.self) - - // Address will be 0 if the protocol is not available (such as only defined on a newer OS) - guard address != 0 else { - return nil - } - let protoPtr = UnsafeRawPointer(bitPattern: UInt(address))! - let proto = protoPtr.load(as: ProtocolDescriptor.self) - let namePtr = protoPtr.advanced(by: MemoryLayout.offset(of: \.name)!).advanced(by: Int(proto.name)) - let protocolName = String(cString: namePtr.assumingMemoryBound(to: CChar.self)) - guard ["PreviewProvider", "PreviewRegistry"].contains(protocolName) else { - return nil - } - - let typeDescriptorPointer = UnsafeRawPointer(conformance).advanced(by: MemoryLayout.offset(of: \.nominalTypeDescriptor)!).advanced(by: Int(conformance.pointee.nominalTypeDescriptor)) - - let descriptor = typeDescriptorPointer.assumingMemoryBound(to: TargetModuleContextDescriptor.self) - if let name = getTypeName(descriptor: descriptor), - [ContextDescriptorKind.Class, ContextDescriptorKind.Struct, ContextDescriptorKind.Enum].contains(descriptor.pointee.flags.kind) { - let accessFunctionPointer = UnsafeRawPointer(descriptor).advanced(by: MemoryLayout.offset(of: \.accessFunction)!).advanced(by: Int(descriptor.pointee.accessFunction)) - let accessFunction = unsafeBitCast(accessFunctionPointer, to: (@convention(c) () -> UInt64).self) - return (name, accessFunction, protocolName) - } + return types +} + +private func parseConformance( + conformance: UnsafePointer +) -> LookupResult? { + let flags = conformance.pointee.conformanceFlags + + guard case .DirectTypeDescriptor = flags.kind else { return nil } - - private static 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)) + + guard conformance.pointee.protocolDescriptor % 2 == 1 else { + return nil + } + let descriptorOffset = Int(conformance.pointee.protocolDescriptor & ~1) + let jumpPtr = UnsafeRawPointer(conformance).advanced( + by: MemoryLayout.offset( + of: \.protocolDescriptor + )! + ).advanced(by: descriptorOffset) + let address = jumpPtr.load(as: UInt64.self) + + // Address will be 0 if the protocol is not available (such as only defined on a newer OS) + guard address != 0 else { + return nil + } + let protoPtr = UnsafeRawPointer(bitPattern: UInt(address))! + let proto = protoPtr.load(as: ProtocolDescriptor.self) + let namePtr = protoPtr.advanced( + by: MemoryLayout.offset(of: \.name)! + ).advanced(by: Int(proto.name)) + let protocolName = String( + cString: namePtr.assumingMemoryBound(to: CChar.self) + ) + guard ["PreviewProvider", "PreviewRegistry"].contains(protocolName) else { + return nil + } + + let typeDescriptorPointer = UnsafeRawPointer(conformance).advanced( + by: MemoryLayout.offset( + of: \.nominalTypeDescriptor + )! + ).advanced(by: Int(conformance.pointee.nominalTypeDescriptor)) + + let descriptor = typeDescriptorPointer.assumingMemoryBound( + to: TargetModuleContextDescriptor.self + ) + if let name = getTypeName(descriptor: descriptor), + [ + ContextDescriptorKind.Class, ContextDescriptorKind.Struct, + ContextDescriptorKind.Enum, + ].contains(descriptor.pointee.flags.kind) + { + let accessFunctionPointer = UnsafeRawPointer(descriptor).advanced( + by: MemoryLayout.offset( + of: \.accessFunction + )! + ).advanced(by: Int(descriptor.pointee.accessFunction)) + let accessFunction = unsafeBitCast( + accessFunctionPointer, + to: (@convention(c) () -> UInt64).self + ) + return (name, accessFunction, protocolName) + } + return nil +} + +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 } - switch flags.kind { - case .Module, .Enum, .Struct, .Class: - let name = UnsafeRawPointer(descriptor) - .advanced(by: MemoryLayout.offset(of: \.name)!) - .advanced(by: Int(descriptor.pointee.name)) - .assumingMemoryBound(to: CChar.self) - let typeName = String(cString: name) - if let parentName = parentName { - return "\(parentName).\(typeName)" - } - return typeName - default: - return parentName + parentName = getTypeName( + descriptor: parent.assumingMemoryBound( + to: TargetModuleContextDescriptor.self + ) + ) + } + switch flags.kind { + case .Module, .Enum, .Struct, .Class: + let name = UnsafeRawPointer(descriptor) + .advanced( + by: MemoryLayout.offset(of: \.name)! + ) + .advanced(by: Int(descriptor.pointee.name)) + .assumingMemoryBound(to: CChar.self) + let typeName = String(cString: name) + if let parentName = parentName { + return "\(parentName).\(typeName)" } + return typeName + default: + return parentName } } #if arch(i386) || arch(arm) || arch(arm64_32) -typealias mach_header_type = mach_header + typealias mach_header_type = mach_header #else -typealias mach_header_type = mach_header_64 + typealias mach_header_type = mach_header_64 #endif From ca97b6ca56425a0bf0d174bed6d68d7771e7fca0 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Thu, 1 May 2025 18:31:32 -0400 Subject: [PATCH 8/9] woops --- Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift | 2 +- Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift index b7c3e883..cb39f120 100644 --- a/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift +++ b/Sources/SnapshotPreviewsCore/SnapshotPreviewsCore.swift @@ -205,7 +205,7 @@ public enum FindPreviews { shouldInclude: (String, String) -> Bool = { _, _ in true }, willAccess: (String) -> Void = { _ in }) -> [PreviewType] { - return ConformanceLookup.getPreviewTypes() + return getPreviewTypes() .filter { shouldInclude($0.name, $0.proto) } .compactMap { conformance -> PreviewType? in let (name, accessor, proto) = conformance diff --git a/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift b/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift index 9228d37c..fd93d689 100644 --- a/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift +++ b/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift @@ -4,7 +4,7 @@ import XCTest final class ConformanceLookupTests: XCTestCase { func test_getPreviewTypes() throws { - let types = ConformanceLookup.getPreviewTypes() + let types = getPreviewTypes() XCTAssertEqual(types.count, 1) let firstType = types.first! From c364ff14d1756e0d38047103b6626938a574f3db Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 2 May 2025 17:32:18 -0400 Subject: [PATCH 9/9] revert --- .../ConformanceLookup.swift | 177 +++++++----------- 1 file changed, 66 insertions(+), 111 deletions(-) diff --git a/Sources/SnapshotPreviewsCore/ConformanceLookup.swift b/Sources/SnapshotPreviewsCore/ConformanceLookup.swift index 7dc24059..bc07c647 100644 --- a/Sources/SnapshotPreviewsCore/ConformanceLookup.swift +++ b/Sources/SnapshotPreviewsCore/ConformanceLookup.swift @@ -8,52 +8,35 @@ import Foundation import MachO -public typealias LookupResult = ( - name: String, accessor: () -> UInt64, proto: String -) - -public func getPreviewTypes() -> [LookupResult] { - let images = _dyld_image_count() - var types = [LookupResult]() - for i in 0..) -> 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 } - - let header = _dyld_get_image_header(i)! - var size: UInt = 0 - let sectStart = UnsafeRawPointer( - getsectiondata( - UnsafeRawPointer(header).assumingMemoryBound(to: mach_header_type.self), - "__TEXT", - "__swift5_proto", - &size - ) - )?.assumingMemoryBound(to: Int32.self) - if var sectData = sectStart { - for _ in 0...size { - let conformance = UnsafeRawPointer(sectData) - .advanced(by: Int(sectData.pointee)) - .assumingMemoryBound(to: ProtocolConformanceDescriptor.self) - if let result = parseConformance(conformance: conformance) { - types.append(result) - } - sectData = sectData.successor() - } + parentName = getTypeName(descriptor: parent.assumingMemoryBound(to: TargetModuleContextDescriptor.self)) + } + switch flags.kind { + case .Module, .Enum, .Struct, .Class: + let name = UnsafeRawPointer(descriptor) + .advanced(by: MemoryLayout.offset(of: \.name)!) + .advanced(by: Int(descriptor.pointee.name)) + .assumingMemoryBound(to: CChar.self) + let typeName = String(cString: name) + if let parentName = parentName { + return "\(parentName).\(typeName)" } + return typeName + default: + return parentName } - return types } -private func parseConformance( - conformance: UnsafePointer -) -> LookupResult? { +public typealias LookupResult = (name: String, accessor: () -> UInt64, proto: String) + +private func parseConformance(conformance: UnsafePointer) -> LookupResult? { let flags = conformance.pointee.conformanceFlags guard case .DirectTypeDescriptor = flags.kind else { @@ -64,11 +47,7 @@ private func parseConformance( return nil } let descriptorOffset = Int(conformance.pointee.protocolDescriptor & ~1) - let jumpPtr = UnsafeRawPointer(conformance).advanced( - by: MemoryLayout.offset( - of: \.protocolDescriptor - )! - ).advanced(by: descriptorOffset) + let jumpPtr = UnsafeRawPointer(conformance).advanced(by: MemoryLayout.offset(of: \.protocolDescriptor)!).advanced(by: descriptorOffset) let address = jumpPtr.load(as: UInt64.self) // Address will be 0 if the protocol is not available (such as only defined on a newer OS) @@ -77,83 +56,59 @@ private func parseConformance( } let protoPtr = UnsafeRawPointer(bitPattern: UInt(address))! let proto = protoPtr.load(as: ProtocolDescriptor.self) - let namePtr = protoPtr.advanced( - by: MemoryLayout.offset(of: \.name)! - ).advanced(by: Int(proto.name)) - let protocolName = String( - cString: namePtr.assumingMemoryBound(to: CChar.self) - ) + let namePtr = protoPtr.advanced(by: MemoryLayout.offset(of: \.name)!).advanced(by: Int(proto.name)) + let protocolName = String(cString: namePtr.assumingMemoryBound(to: CChar.self)) guard ["PreviewProvider", "PreviewRegistry"].contains(protocolName) else { return nil } - let typeDescriptorPointer = UnsafeRawPointer(conformance).advanced( - by: MemoryLayout.offset( - of: \.nominalTypeDescriptor - )! - ).advanced(by: Int(conformance.pointee.nominalTypeDescriptor)) + let typeDescriptorPointer = UnsafeRawPointer(conformance).advanced(by: MemoryLayout.offset(of: \.nominalTypeDescriptor)!).advanced(by: Int(conformance.pointee.nominalTypeDescriptor)) - let descriptor = typeDescriptorPointer.assumingMemoryBound( - to: TargetModuleContextDescriptor.self - ) + let descriptor = typeDescriptorPointer.assumingMemoryBound(to: TargetModuleContextDescriptor.self) if let name = getTypeName(descriptor: descriptor), - [ - ContextDescriptorKind.Class, ContextDescriptorKind.Struct, - ContextDescriptorKind.Enum, - ].contains(descriptor.pointee.flags.kind) - { - let accessFunctionPointer = UnsafeRawPointer(descriptor).advanced( - by: MemoryLayout.offset( - of: \.accessFunction - )! - ).advanced(by: Int(descriptor.pointee.accessFunction)) - let accessFunction = unsafeBitCast( - accessFunctionPointer, - to: (@convention(c) () -> UInt64).self - ) + [ContextDescriptorKind.Class, ContextDescriptorKind.Struct, ContextDescriptorKind.Enum].contains(descriptor.pointee.flags.kind) { + let accessFunctionPointer = UnsafeRawPointer(descriptor).advanced(by: MemoryLayout.offset(of: \.accessFunction)!).advanced(by: Int(descriptor.pointee.accessFunction)) + let accessFunction = unsafeBitCast(accessFunctionPointer, to: (@convention(c) () -> UInt64).self) return (name, accessFunction, protocolName) } return nil } -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 +#if arch(i386) || arch(arm) || arch(arm64_32) +typealias mach_header_type = mach_header +#else +typealias mach_header_type = mach_header_64 +#endif + +public func getPreviewTypes() -> [LookupResult] { + let images = _dyld_image_count() + var types = [LookupResult]() + for i in 0...offset(of: \.name)! - ) - .advanced(by: Int(descriptor.pointee.name)) - .assumingMemoryBound(to: CChar.self) - let typeName = String(cString: name) - if let parentName = parentName { - return "\(parentName).\(typeName)" + + let header = _dyld_get_image_header(i)! + var size: UInt = 0 + let sectStart = UnsafeRawPointer( + getsectiondata( + UnsafeRawPointer(header).assumingMemoryBound(to: mach_header_type.self), + "__TEXT", + "__swift5_proto", + &size))?.assumingMemoryBound(to: Int32.self) + if var sectData = sectStart { + for _ in 0...size { + let conformance = UnsafeRawPointer(sectData) + .advanced(by: Int(sectData.pointee)) + .assumingMemoryBound(to: ProtocolConformanceDescriptor.self) + if let result = parseConformance(conformance: conformance) { + types.append(result) + } + sectData = sectData.successor() + } } - return typeName - default: - return parentName } + return types } - -#if arch(i386) || arch(arm) || arch(arm64_32) - typealias mach_header_type = mach_header -#else - typealias mach_header_type = mach_header_64 -#endif