Skip to content

Commit

Permalink
Optimise getting list of scenarios
Browse files Browse the repository at this point in the history
  • Loading branch information
antranapp committed Sep 16, 2024
1 parent 9561a75 commit 5f7cd97
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 12 deletions.
4 changes: 3 additions & 1 deletion Sources/Scenarios/Scenario/Scenario.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import ObjectiveC
import UIKit

@objc public protocol ScenarioMarker {}

public protocol IdentifiableType: AnyObject {
static var id: String { get }
}
Expand All @@ -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 }
Expand Down
59 changes: 48 additions & 11 deletions Sources/Scenarios/Scenario/ScenarioId.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScenarioId> = {
// 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<AnyClass>(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 {
Expand Down

0 comments on commit 5f7cd97

Please sign in to comment.