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..5dc4a22 100644 --- a/Sources/Scenarios/Scenario/ScenarioId.swift +++ b/Sources/Scenarios/Scenario/ScenarioId.swift @@ -37,20 +37,57 @@ 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 = { +// 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 } + // 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 {