diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bccc2be..3b7813d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,16 +27,16 @@ jobs: # xcrun simctl list devices 15.4 - name: Boot simulator - run: xcrun simctl boot "iPhone 13 Pro" + run: xcrun simctl boot "iPhone 15 Pro" - name: Run Internal tests - run: xcodebuild clean build test -workspace Scenarios.xcworkspace -scheme "Sample-Internal" -destination "platform=iOS Simulator,name=iPhone 13 Pro,OS=latest" + run: xcodebuild clean build test -workspace Scenarios.xcworkspace -scheme "Sample-Internal" -destination "platform=iOS Simulator,name=iPhone 15 Pro,OS=latest" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO - name: Run Production tests - run: xcodebuild clean build test -workspace Scenarios.xcworkspace -scheme "Sample-Production" -destination "platform=iOS Simulator,name=iPhone 13 Pro,OS=latest" + run: xcodebuild clean build test -workspace Scenarios.xcworkspace -scheme "Sample-Production" -destination "platform=iOS Simulator,name=iPhone 15 Pro,OS=latest" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO - name: Run Internal (SwiftUI) tests - run: xcodebuild clean build test -workspace Scenarios.xcworkspace -scheme "Sample-Internal" -destination "platform=iOS Simulator,name=iPhone 13 Pro,OS=latest" + run: xcodebuild clean build test -workspace Scenarios.xcworkspace -scheme "Sample-Internal" -destination "platform=iOS Simulator,name=iPhone 15 Pro,OS=latest" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO - name: Run Production (SwiftUI) tests - run: xcodebuild clean build test -workspace Scenarios.xcworkspace -scheme "Sample-Production" -destination "platform=iOS Simulator,name=iPhone 13 Pro,OS=latest" + run: xcodebuild clean build test -workspace Scenarios.xcworkspace -scheme "Sample-Production" -destination "platform=iOS Simulator,name=iPhone 15 Pro,OS=latest" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO diff --git a/Sources/Scenarios/Scenario/Scenario.swift b/Sources/Scenarios/Scenario/Scenario.swift index 96c9977..5372d23 100644 --- a/Sources/Scenarios/Scenario/Scenario.swift +++ b/Sources/Scenarios/Scenario/Scenario.swift @@ -5,6 +5,8 @@ import ObjectiveC import UIKit +@objc public protocol ScenarioMarker {} + public protocol IdentifiableType: AnyObject { static var id: String { get } } @@ -27,7 +29,7 @@ public struct ScenarioInfo { } } -public protocol BaseScenario: IdentifiableType { +public protocol BaseScenario: IdentifiableType, ScenarioMarker { static var name: String { get } static var nameForSorting: String { get } static var kind: ScenarioKind { get } diff --git a/Sources/Scenarios/Scenario/ScenarioId.swift b/Sources/Scenarios/Scenario/ScenarioId.swift index fd91b46..9255c2c 100644 --- a/Sources/Scenarios/Scenario/ScenarioId.swift +++ b/Sources/Scenarios/Scenario/ScenarioId.swift @@ -6,6 +6,7 @@ import Foundation import ObjectiveC import SwiftUI +// Highly inspried from https://github.com/zuhlke/Support public struct ScenarioId: CaseIterable, Hashable, Identifiable, RawRepresentable, Codable { public var scenarioType: Scenario.Type @@ -37,20 +38,46 @@ public struct ScenarioId: CaseIterable, Hashable, Identifiable, RawRepresentable public static func == (lhs: ScenarioId, rhs: ScenarioId) -> Bool { lhs.scenarioType.id == rhs.scenarioType.id } - + public static let allCases: [ScenarioId] = { - var count: UInt32 = 0 - let classes = objc_copyClassList(&count) - let buffer = UnsafeBufferPointer(start: classes, count: Int(count)) - return Array( - (0 ..< Int(count)) - .lazy - .compactMap { buffer[$0] as? Scenario.Type } - .map { ScenarioId(withType: $0) } - ) - .sorted { $0.scenarioType.name < $1.scenarioType.name } + Array(_allCases) + }() + + public static let _allCases: some Sequence = { + // AnyClass.init seems to register new objective-C class the first time it is called. + // + // In order for the count variable to reserve enough capacity, we call this method once + // so that any new classes are registered to the runtime. + _ = [AnyClass](unsafeUninitializedCapacity: Int(1)) { buffer, initialisedCount in + initialisedCount = 0 + } + + // Improved thanks to some hints from https://stackoverflow.com/a/54150007 + let count = objc_getClassList(nil, 0) + let classes = [AnyClass](unsafeUninitializedCapacity: Int(count)) { buffer, initialisedCount in + let autoreleasingPointer = AutoreleasingUnsafeMutablePointer(buffer.baseAddress) + initialisedCount = Int(objc_getClassList(autoreleasingPointer, count)) + } + + return classes + .lazy + // The `filter` is necessary. Without it we may crash. + // + // The cast using `as?` calls some objective-c methods on the type to check for conformance. But certain + // system types do not implement that method and would cause a crash (possible bug in the runtime?). + // + // `class_conformsToProtocol` is safe to call on all types, so we use it to filter down to “our” classes + // we try to cast them. + .filter { class_inherited_conformsToProtocol($0, ScenarioMarker.self) } + .compactMap { $0 as? Scenario.Type } + .map { ScenarioId(withType: $0) } }() +} +private func class_inherited_conformsToProtocol(_ cls: AnyClass, _ p: Protocol) -> Bool { + if class_conformsToProtocol(cls, p) { return true } + guard let sup = class_getSuperclass(cls) else { return false } + return class_inherited_conformsToProtocol(sup, p) } public protocol ScenarioCategory {