Add Apple Vision Pro (M2) and Apple Vision Pro (M5) support

Implement full visionOS device support with both Vision Pro models (identifiers RealityDevice14,1 and RealityDevice17,1). Replace all visionOS TODO placeholders with proper implementations including device enum cases, identifier mapping, property getters (description, safeDescription, cpu), and static device arrays. Add comprehensive unit tests for visionOS platform. Enable visionOS simulator testing in GitHub Actions CI pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Zandor Smith
2026-03-02 19:14:39 +01:00
parent 43fedd8fc3
commit 0d92b33997
7 changed files with 131 additions and 32 deletions
View File
View File
+1
View File
@@ -43,6 +43,7 @@ jobs:
- platform=tvOS Simulator,name=Apple TV
- platform=tvOS Simulator,name=Apple TV 4K (3rd generation)
- platform=watchOS Simulator,name=Apple Watch Series 10 (46mm)
- platform=visionOS Simulator,name=Apple Vision Pro
steps:
- uses: actions/checkout@v4
+3
View File
@@ -7,6 +7,7 @@
- Add support for M5 iPad Pro models. ([#467](https://github.com/devicekit/DeviceKit/pull/467))
- Add support for Apple Watch SE (3rd generation) ([#473](https://github.com/devicekit/DeviceKit/pull/473))
- Add support for iPhone 17e and iPad Air (M4)
- Add support for Apple Vision Pro and Apple Vision Pro (M5)
| Device | Case value |
| --- | --- |
@@ -17,6 +18,8 @@
| iPhone 17e | `Device.iPhone17e` |
| iPad Air (11-inch) (M4) | `Device.iPadAir11M4` |
| iPad Air (13-inch) (M4) | `Device.iPadAir13M4` |
| Apple Vision Pro | `Device.appleVisionPro` |
| Apple Vision Pro (M5) | `Device.appleVisionProM5` |
### Bug fixes
+46 -16
View File
@@ -588,6 +588,15 @@ public enum Device {
///
/// ![Image]()
case appleWatchSeries11_46mm
#elseif os(visionOS)
/// Device is an [Apple Vision Pro](https://support.apple.com/kb/SP911)
///
/// ![Image]()
case appleVisionPro
/// Device is an [Apple Vision Pro (M5)](https://support.apple.com/en-us/125436)
///
/// ![Image]()
case appleVisionProM5
#endif
/// Device is [Simulator](https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/iOS_Simulator_Guide/Introduction/Introduction.html)
@@ -774,8 +783,12 @@ public enum Device {
default: return unknown(identifier)
}
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return unknown(identifier)
switch identifier {
case "RealityDevice14,1": return appleVisionPro
case "RealityDevice17,1": return appleVisionProM5
case "i386", "x86_64", "arm64": return simulator(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "visionOS"))
default: return unknown(identifier)
}
#else
return unknown(identifier)
#endif
@@ -1344,6 +1357,16 @@ public enum Device {
public var hasForceTouchSupport: Bool {
return isOneOf(Device.allWatchesWithForceTouchSupport) || isOneOf(Device.allWatchesWithForceTouchSupport.map(Device.simulator))
}
#elseif os(visionOS)
/// All Vision devices
public static var allVisions: [Device] {
return [.appleVisionPro, .appleVisionProM5]
}
/// All simulator Vision devices
public static var allSimulatorVisions: [Device] {
return allVisions.map(Device.simulator)
}
#endif
/// Returns whether the current device is a SwiftUI preview canvas
@@ -1365,8 +1388,7 @@ public enum Device {
#elseif os(watchOS)
return allWatches
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return []
return allVisions
#else
return []
#endif
@@ -1636,7 +1658,6 @@ public enum Device {
#elseif os(tvOS)
return nil
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return nil
#else
return nil
@@ -1821,8 +1842,12 @@ extension Device: CustomStringConvertible {
case .unknown(let identifier): return identifier
}
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return "Apple Vision Pro"
switch self {
case .appleVisionPro: return "Apple Vision Pro"
case .appleVisionProM5: return "Apple Vision Pro (M5)"
case .simulator(let model): return "Simulator (\(model.description))"
case .unknown(let identifier): return identifier
}
#else
switch self {
case .simulator(let model): return "Simulator (\(model.description))"
@@ -1986,8 +2011,12 @@ extension Device: CustomStringConvertible {
case .unknown(let identifier): return identifier
}
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return "Apple Vision Pro"
switch self {
case .appleVisionPro: return "Apple Vision Pro"
case .appleVisionProM5: return "Apple Vision Pro (M5)"
case .simulator(let model): return "Simulator (\(model.safeDescription))"
case .unknown(let identifier): return identifier
}
#else
switch self {
case .simulator(let model): return "Simulator (\(model.safeDescription))"
@@ -2508,7 +2537,7 @@ extension Device {
extension Device {
public enum CPU: Comparable {
#if os(iOS) || os(tvOS)
#if os(iOS) || os(tvOS) || os(visionOS)
case a4
case a5
case a5X
@@ -2707,8 +2736,12 @@ extension Device {
case .unknown: return .unknown
}
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return .unknown
switch self {
case .appleVisionPro: return .m2
case .appleVisionProM5: return .m5
case .simulator(let model): return model.cpu
case .unknown: return .unknown
}
#else
return .unknown
#endif
@@ -2719,7 +2752,7 @@ extension Device.CPU: CustomStringConvertible {
/// A textual representation of the device.
public var description: String {
#if os(iOS) || os(tvOS)
#if os(iOS) || os(tvOS) || os(visionOS)
switch self {
case .a4: return "A4"
case .a5: return "A5"
@@ -2768,9 +2801,6 @@ extension Device.CPU: CustomStringConvertible {
case .s10: return "S10"
case .unknown: return "unknown"
}
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return "unknown"
#else
return "unknown"
#endif
+55 -16
View File
@@ -156,6 +156,12 @@ tvs = [
Device("appleTV4K3", "Device is an [Apple TV 4K (3rd generation)](https://support.apple.com/kb/SP886)", "https://support.apple.com/library/APPLE/APPLECARE_ALLGEOS/SP886/apple-tv-4k-3gen_2x.png", ["AppleTV14,1"], 0, (), "Apple TV 4K (3rd generation)", "Apple TV 4K (3rd generation)", -1, False, False, False, False, False, False, False, False, False, False, 0, False, 0, False, "a15Bionic", False, False),
]
# visionOS
visions = [
Device("appleVisionPro", "Device is an [Apple Vision Pro](https://support.apple.com/kb/SP911)", "", ["RealityDevice14,1"], 0, (1, 1), "Apple Vision Pro", "Apple Vision Pro", -1, False, False, False, False, False, False, False, False, False, False, 0, False, 0, True, "m2", True, False),
Device("appleVisionProM5", "Device is an [Apple Vision Pro (M5)](https://support.apple.com/en-us/125436)", "", ["RealityDevice17,1"], 0, (1, 1), "Apple Vision Pro (M5)", "Apple Vision Pro (M5)", -1, False, False, False, False, False, False, False, False, False, False, 0, False, 0, True, "m5", True, False),
]
# watchOS
watches = [
Device(
@@ -361,6 +367,7 @@ watches = [
iOSDevices = iPods + iPhones + iPads + homePods
tvOSDevices = tvs
watchOSDevices = watches
visionOSDevices = visions
}%
#if os(watchOS)
import WatchKit
@@ -422,6 +429,13 @@ public enum Device {
///
/// ![Image](${device.imageURL})
case ${device.caseName}
% end
#elseif os(visionOS)
% for device in visionOSDevices:
/// ${device.comment}
///
/// ![Image](${device.imageURL})
case ${device.caseName}
% end
#endif
@@ -484,8 +498,13 @@ public enum Device {
default: return unknown(identifier)
}
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return unknown(identifier)
switch identifier {
% for device in visionOSDevices:
case ${', '.join(list(map(lambda device: "\"" + device + "\"", device.identifiers)))}: return ${device.caseName}
% end
case "i386", "x86_64", "arm64": return simulator(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "visionOS"))
default: return unknown(identifier)
}
#else
return unknown(identifier)
#endif
@@ -806,6 +825,16 @@ public enum Device {
public var hasForceTouchSupport: Bool {
return isOneOf(Device.allWatchesWithForceTouchSupport) || isOneOf(Device.allWatchesWithForceTouchSupport.map(Device.simulator))
}
#elseif os(visionOS)
/// All Vision devices
public static var allVisions: [Device] {
return [${', '.join(list(map(lambda device: "." + device.caseName, visionOSDevices)))}]
}
/// All simulator Vision devices
public static var allSimulatorVisions: [Device] {
return allVisions.map(Device.simulator)
}
#endif
/// Returns whether the current device is a SwiftUI preview canvas
@@ -827,8 +856,7 @@ public enum Device {
#elseif os(watchOS)
return allWatches
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return []
return allVisions
#else
return []
#endif
@@ -974,7 +1002,6 @@ public enum Device {
#elseif os(tvOS)
return nil
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return nil
#else
return nil
@@ -1034,8 +1061,13 @@ extension Device: CustomStringConvertible {
case .unknown(let identifier): return identifier
}
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return "Apple Vision Pro"
switch self {
% for device in visionOSDevices:
case .${device.caseName}: return "${device.description}"
% end
case .simulator(let model): return "Simulator (\(model.description))"
case .unknown(let identifier): return identifier
}
#else
switch self {
case .simulator(let model): return "Simulator (\(model.description))"
@@ -1074,8 +1106,13 @@ extension Device: CustomStringConvertible {
case .unknown(let identifier): return identifier
}
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return "Apple Vision Pro"
switch self {
% for device in visionOSDevices:
case .${device.caseName}: return "${device.safeDescription}"
% end
case .simulator(let model): return "Simulator (\(model.safeDescription))"
case .unknown(let identifier): return identifier
}
#else
switch self {
case .simulator(let model): return "Simulator (\(model.safeDescription))"
@@ -1568,7 +1605,7 @@ watchOS_cpus = [
extension Device {
public enum CPU: Comparable {
#if os(iOS) || os(tvOS)
#if os(iOS) || os(tvOS) || os(visionOS)
% for cpu in iOS_cpus:
case ${cpu.name}
% end
@@ -1607,8 +1644,13 @@ extension Device {
case .unknown: return .unknown
}
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return .unknown
switch self {
% for device in visionOSDevices:
case .${device.caseName}: return .${device.cpu}
% end
case .simulator(let model): return model.cpu
case .unknown: return .unknown
}
#else
return .unknown
#endif
@@ -1619,7 +1661,7 @@ extension Device.CPU: CustomStringConvertible {
/// A textual representation of the device.
public var description: String {
#if os(iOS) || os(tvOS)
#if os(iOS) || os(tvOS) || os(visionOS)
switch self {
% for cpu in iOS_cpus:
case .${cpu.name}: return "${cpu.description}"
@@ -1633,9 +1675,6 @@ extension Device.CPU: CustomStringConvertible {
% end
case .unknown: return "unknown"
}
#elseif os(visionOS)
// TODO: Replace with proper implementation for visionOS.
return "unknown"
#else
return "unknown"
#endif
+26
View File
@@ -819,4 +819,30 @@ class DeviceKitTests: XCTestCase {
#endif
// MARK: - visionOS
#if os(visionOS)
func testMapFromIdentifier() {
XCTAssertEqual(Device.mapToDevice(identifier: "RealityDevice14,1"), .appleVisionPro)
XCTAssertEqual(Device.mapToDevice(identifier: "RealityDevice17,1"), .appleVisionProM5)
}
func testDeviceCPU() {
XCTAssertEqual(Device.appleVisionPro.cpu, Device.CPU.m2)
XCTAssertEqual(Device.appleVisionProM5.cpu, Device.CPU.m5)
}
func testDescription() {
XCTAssertEqual(Device.appleVisionPro.description, "Apple Vision Pro")
XCTAssertEqual(Device.appleVisionProM5.description, "Apple Vision Pro (M5)")
}
func testSafeDescription() {
for device in Device.allRealDevices {
XCTAssertEqual(device.description, device.safeDescription)
}
}
#endif
}