From be3a28f5c60361eafa80ad0b12a5b82b94dad355 Mon Sep 17 00:00:00 2001 From: Zandor Smith Date: Tue, 27 Oct 2026 08:12:52 +0100 Subject: [PATCH 1/4] Add HomePod mini 2, iPad mini (A20 Pro) and Apple TV 4K (4th generation) HomePod mini 2 maps from its verified AudioAccessory11,1 identifier and uses a new S10 CPU case. iPad mini (A20 Pro) and Apple TV 4K (4th generation) are added provisionally with Apple's published specs but no identifiers, since neither has shipped yet. The mapToDevice template now skips devices without identifiers. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 12 ++++++++ Source/Device.generated.swift | 50 +++++++++++++++++++++++++-------- Source/Device.swift.gyb | 20 ++++++++++++++ Tests/Tests.swift | 52 ++++++++++++++++++++++++++++++++++- 4 files changed, 121 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09804786..acbed6c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## Unreleased +### New October 2026 devices + +This version adds support for HomePod mini 2, iPad mini (A20 Pro) and Apple TV 4K (4th generation), along with the `S10` CPU case on iOS and tvOS: (PR_LINK) + +| Device | Case value | +| --- | --- | +| HomePod mini 2 | `Device.homePodMini2` | +| iPad mini (A20 Pro) | `Device.iPadMiniA20Pro` | +| Apple TV 4K (4th generation) | `Device.appleTV4K4` | + +iPad mini (A20 Pro) and Apple TV 4K (4th generation) are provisional. Their hardware identifiers aren't public yet, so `Device.current` and `mapToDevice(identifier:)` won't return them until a later update adds the identifiers. + ## Version 5.10.0 Releasedate: 2026-11-01 diff --git a/Source/Device.generated.swift b/Source/Device.generated.swift index b10138c7..faf2c25a 100644 --- a/Source/Device.generated.swift +++ b/Source/Device.generated.swift @@ -346,6 +346,8 @@ public enum Device { /// /// ![Image](https://cdsassets.apple.com/live/7WUAS350/images/tech-specs/iPad_mini_A17_Pro_Wi-Fi_Lineup_Print__USEN.png) case iPadMiniA17Pro + /// Device is an [iPad mini (A20 Pro)](https://www.apple.com/ipad-mini/specs/) + case iPadMiniA20Pro /// Device is an [iPad Pro 9.7-inch](https://support.apple.com/kb/SP739) /// /// ![Image](https://support.apple.com/library/APPLE/APPLECARE_ALLGEOS/SP739/SP739.png) @@ -414,6 +416,8 @@ public enum Device { /// /// ![Image](https://support.apple.com/library/APPLE/APPLECARE_ALLGEOS/SP773/homepod_space_gray_large_2x.jpg) case homePod + /// Device is a [HomePod mini 2](https://www.apple.com/homepod-mini/specs/) + case homePodMini2 #elseif os(tvOS) /// Device is an [Apple TV HD](https://support.apple.com/kb/SP724) (Previously Apple TV (4th generation)) /// @@ -431,6 +435,8 @@ public enum Device { /// /// ![Image](https://support.apple.com/library/APPLE/APPLECARE_ALLGEOS/SP886/apple-tv-4k-3gen_2x.png) case appleTV4K3 + /// Device is an [Apple TV 4K (4th generation)](https://www.apple.com/apple-tv-4k/specs/) + case appleTV4K4 #elseif os(watchOS) /// Device is an [Apple Watch (1st generation)](https://support.apple.com/kb/SP735) /// @@ -688,6 +694,7 @@ public enum Device { case "iPad17,1", "iPad17,2": return iPadPro11M5 case "iPad17,3", "iPad17,4": return iPadPro13M5 case "AudioAccessory1,1": return homePod + case "AudioAccessory11,1": return homePodMini2 case "i386", "x86_64", "arm64": return simulator(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS")) default: return unknown(identifier) } @@ -865,6 +872,7 @@ public enum Device { case .iPadMini5: return 7.9 case .iPadMini6: return 8.3 case .iPadMiniA17Pro: return 8.3 + case .iPadMiniA20Pro: return 8.5 case .iPadPro9Inch: return 9.7 case .iPadPro12Inch: return 12.9 case .iPadPro12Inch2: return 12.9 @@ -882,6 +890,7 @@ public enum Device { case .iPadPro11M5: return 11.0 case .iPadPro13M5: return 13.0 case .homePod: return -1 + case .homePodMini2: return -1 case .simulator(let model): return model.diagonal case .unknown: return -1 } @@ -1017,6 +1026,7 @@ public enum Device { case .iPadMini5: return (width: 3, height: 4) case .iPadMini6: return (width: 744, height: 1133) case .iPadMiniA17Pro: return (width: 744, height: 1133) + case .iPadMiniA20Pro: return (width: 756, height: 1158) case .iPadPro9Inch: return (width: 3, height: 4) case .iPadPro12Inch: return (width: 3, height: 4) case .iPadPro12Inch2: return (width: 3, height: 4) @@ -1034,6 +1044,7 @@ public enum Device { case .iPadPro11M5: return (width: 605, height: 417) case .iPadPro13M5: return (width: 4, height: 3) case .homePod: return (width: 4, height: 5) + case .homePodMini2: return (width: -1, height: -1) case .simulator(let model): return model.screenRatio case .unknown: return (width: -1, height: -1) } @@ -1098,7 +1109,7 @@ public enum Device { /// All iPads public static var allPads: [Device] { - return [.iPad2, .iPad3, .iPad4, .iPadAir, .iPadAir2, .iPad5, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini, .iPadMini2, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] + return [.iPad2, .iPad3, .iPad4, .iPadAir, .iPadAir2, .iPad5, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini, .iPadMini2, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadMiniA20Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] } /// All X-Series Devices @@ -1119,7 +1130,7 @@ public enum Device { /// All mini Devices public static var allMiniDevices: [Device] { - return [.iPadMini, .iPadMini2, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro] + return [.iPadMini, .iPadMini2, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadMiniA20Pro] } /// All simulator iPods @@ -1195,7 +1206,7 @@ public enum Device { /// All Touch ID Capable Devices public static var allTouchIDCapableDevices: [Device] { - return [.iPhone5s, .iPhone6, .iPhone6Plus, .iPhone6s, .iPhone6sPlus, .iPhone7, .iPhone7Plus, .iPhoneSE, .iPhone8, .iPhone8Plus, .iPhoneSE2, .iPhoneSE3, .iPhoneDuo, .iPadAir2, .iPad5, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch] + return [.iPhone5s, .iPhone6, .iPhone6Plus, .iPhone6s, .iPhone6sPlus, .iPhone7, .iPhone7Plus, .iPhoneSE, .iPhone8, .iPhone8Plus, .iPhoneSE2, .iPhoneSE3, .iPhoneDuo, .iPadAir2, .iPad5, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadMiniA20Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch] } /// All Face ID Capable Devices @@ -1205,7 +1216,7 @@ public enum Device { /// All Devices with Touch ID or Face ID public static var allBiometricAuthenticationCapableDevices: [Device] { - return [.iPhone5s, .iPhone6, .iPhone6Plus, .iPhone6s, .iPhone6sPlus, .iPhone7, .iPhone7Plus, .iPhoneSE, .iPhone8, .iPhone8Plus, .iPhoneX, .iPhoneXS, .iPhoneXSMax, .iPhoneXR, .iPhone11, .iPhone11Pro, .iPhone11ProMax, .iPhoneSE2, .iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhoneSE3, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPadAir2, .iPad5, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] + return [.iPhone5s, .iPhone6, .iPhone6Plus, .iPhone6s, .iPhone6sPlus, .iPhone7, .iPhone7Plus, .iPhoneSE, .iPhone8, .iPhone8Plus, .iPhoneX, .iPhoneXS, .iPhoneXSMax, .iPhoneXR, .iPhone11, .iPhone11Pro, .iPhone11ProMax, .iPhoneSE2, .iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhoneSE3, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPadAir2, .iPad5, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadMiniA20Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] } /// Returns whether or not the device has Touch ID @@ -1240,7 +1251,7 @@ public enum Device { /// All devices that feature a screen with rounded corners. public static var allDevicesWithRoundedDisplayCorners: [Device] { - return [.iPhoneX, .iPhoneXS, .iPhoneXSMax, .iPhoneXR, .iPhone11, .iPhone11Pro, .iPhone11ProMax, .iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini6, .iPadMiniA17Pro, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] + return [.iPhoneX, .iPhoneXS, .iPhoneXSMax, .iPhoneXR, .iPhone11, .iPhone11Pro, .iPhone11ProMax, .iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini6, .iPadMiniA17Pro, .iPadMiniA20Pro, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] } /// Returns whether or not the device has a screen with rounded corners. @@ -1280,7 +1291,7 @@ public enum Device { /// All devices that support 5G. public static var allDevicesWith5gSupport: [Device] { - return [.iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhoneSE3, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPad10, .iPadA16, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini6, .iPadMiniA17Pro, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] + return [.iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhoneSE3, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPad10, .iPadA16, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini6, .iPadMiniA17Pro, .iPadMiniA20Pro, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] } /// Returns whether or not the device has 5G support. @@ -1295,7 +1306,7 @@ public enum Device { /// model capabilities and does not guarantee eSIM availability on a particular device. /// See https://support.apple.com/en-us/109317 and https://support.apple.com/en-us/119592. public static var allDevicesWithEsimSupport: [Device] { - return [.iPhoneXS, .iPhoneXSMax, .iPhoneXR, .iPhone11, .iPhone11Pro, .iPhone11ProMax, .iPhoneSE2, .iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhoneSE3, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] + return [.iPhoneXS, .iPhoneXSMax, .iPhoneXR, .iPhone11, .iPhone11Pro, .iPhone11ProMax, .iPhoneSE2, .iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhoneSE3, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadMiniA20Pro, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] } /// Returns whether the device model supports eSIM. Simulators use their simulated model. @@ -1329,7 +1340,7 @@ public enum Device { /// All devices that have a USB-C connectivity. public static var allDevicesWithUSBCConnectivity: [Device] { - return [.iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini6, .iPadMiniA17Pro, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] + return [.iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini6, .iPadMiniA17Pro, .iPadMiniA20Pro, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] } /// Returns whether or not the device has a USB-C power supply. @@ -1339,7 +1350,7 @@ public enum Device { #elseif os(tvOS) /// All TVs public static var allTVs: [Device] { - return [.appleTVHD, .appleTV4K, .appleTV4K2, .appleTV4K3] + return [.appleTVHD, .appleTV4K, .appleTV4K2, .appleTV4K3, .appleTV4K4] } /// All simulator TVs @@ -1603,6 +1614,7 @@ public enum Device { case .iPadMini5: return 326 case .iPadMini6: return 326 case .iPadMiniA17Pro: return 326 + case .iPadMiniA20Pro: return 326 case .iPadPro9Inch: return 264 case .iPadPro12Inch: return 264 case .iPadPro12Inch2: return 264 @@ -1620,6 +1632,7 @@ public enum Device { case .iPadPro11M5: return 264 case .iPadPro13M5: return 264 case .homePod: return -1 + case .homePodMini2: return -1 case .simulator(let model): return model.ppi case .unknown: return nil } @@ -1787,6 +1800,7 @@ extension Device: CustomStringConvertible { case .iPadMini5: return "iPad mini (5th generation)" case .iPadMini6: return "iPad mini (6th generation)" case .iPadMiniA17Pro: return "iPad mini (A17 Pro)" + case .iPadMiniA20Pro: return "iPad mini (A20 Pro)" case .iPadPro9Inch: return "iPad Pro (9.7-inch)" case .iPadPro12Inch: return "iPad Pro (12.9-inch)" case .iPadPro12Inch2: return "iPad Pro (12.9-inch) (2nd generation)" @@ -1804,6 +1818,7 @@ extension Device: CustomStringConvertible { case .iPadPro11M5: return "iPad Pro (11-inch) (M5)" case .iPadPro13M5: return "iPad Pro (13-inch) (M5)" case .homePod: return "HomePod" + case .homePodMini2: return "HomePod mini 2" case .simulator(let model): return "Simulator (\(model.description))" case .unknown(let identifier): return identifier } @@ -1854,6 +1869,7 @@ extension Device: CustomStringConvertible { case .appleTV4K: return "Apple TV 4K" case .appleTV4K2: return "Apple TV 4K (2nd generation)" case .appleTV4K3: return "Apple TV 4K (3rd generation)" + case .appleTV4K4: return "Apple TV 4K (4th generation)" case .simulator(let model): return "Simulator (\(model.description))" case .unknown(let identifier): return identifier } @@ -1958,6 +1974,7 @@ extension Device: CustomStringConvertible { case .iPadMini5: return "iPad mini (5th generation)" case .iPadMini6: return "iPad mini (6th generation)" case .iPadMiniA17Pro: return "iPad mini (A17 Pro)" + case .iPadMiniA20Pro: return "iPad mini (A20 Pro)" case .iPadPro9Inch: return "iPad Pro (9.7-inch)" case .iPadPro12Inch: return "iPad Pro (12.9-inch)" case .iPadPro12Inch2: return "iPad Pro (12.9-inch) (2nd generation)" @@ -1975,6 +1992,7 @@ extension Device: CustomStringConvertible { case .iPadPro11M5: return "iPad Pro (11-inch) (M5)" case .iPadPro13M5: return "iPad Pro (13-inch) (M5)" case .homePod: return "HomePod" + case .homePodMini2: return "HomePod mini 2" case .simulator(let model): return "Simulator (\(model.safeDescription))" case .unknown(let identifier): return identifier } @@ -2025,6 +2043,7 @@ extension Device: CustomStringConvertible { case .appleTV4K: return "Apple TV 4K" case .appleTV4K2: return "Apple TV 4K (2nd generation)" case .appleTV4K3: return "Apple TV 4K (3rd generation)" + case .appleTV4K4: return "Apple TV 4K (4th generation)" case .simulator(let model): return "Simulator (\(model.safeDescription))" case .unknown(let identifier): return identifier } @@ -2296,7 +2315,7 @@ extension Device { /// All Apple Pencil Capable Devices public static var allApplePencilCapableDevices: [Device] { - return [.iPhoneDuo, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] + return [.iPhoneDuo, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadMiniA20Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] } /// Returns supported version of the Apple Pencil @@ -2335,6 +2354,7 @@ extension Device { case .iPadAir11M4: return [.firstGenerationUsbC, .pro] case .iPadAir13M4: return [.firstGenerationUsbC, .pro] case .iPadMiniA17Pro: return [.firstGenerationUsbC, .pro] + case .iPadMiniA20Pro: return [.firstGenerationUsbC, .pro] case .iPadPro11M4: return [.firstGenerationUsbC, .pro] case .iPadPro13M4: return [.firstGenerationUsbC, .pro] case .simulator(let model): return model.applePencilSupport @@ -2409,6 +2429,7 @@ extension Device { case .iPadMini5: return [.wide] case .iPadMini6: return [.wide] case .iPadMiniA17Pro: return [.wide] + case .iPadMiniA20Pro: return [.wide] case .iPadPro9Inch: return [.wide] case .iPadPro12Inch: return [.wide] case .iPadPro12Inch2: return [.wide] @@ -2465,7 +2486,7 @@ extension Device { /// All devices that feature a camera public static var allDevicesWithCamera: [Device] { - return [.iPodTouch5, .iPodTouch6, .iPodTouch7, .iPhone4, .iPhone4s, .iPhone5, .iPhone5c, .iPhone5s, .iPhone6, .iPhone6Plus, .iPhone6s, .iPhone6sPlus, .iPhone7, .iPhone7Plus, .iPhoneSE, .iPhone8, .iPhone8Plus, .iPhoneX, .iPhoneXS, .iPhoneXSMax, .iPhoneXR, .iPhone11, .iPhone11Pro, .iPhone11ProMax, .iPhoneSE2, .iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhoneSE3, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPad2, .iPad3, .iPad4, .iPadAir, .iPadAir2, .iPad5, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini, .iPadMini2, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] + return [.iPodTouch5, .iPodTouch6, .iPodTouch7, .iPhone4, .iPhone4s, .iPhone5, .iPhone5c, .iPhone5s, .iPhone6, .iPhone6Plus, .iPhone6s, .iPhone6sPlus, .iPhone7, .iPhone7Plus, .iPhoneSE, .iPhone8, .iPhone8Plus, .iPhoneX, .iPhoneXS, .iPhoneXSMax, .iPhoneXR, .iPhone11, .iPhone11Pro, .iPhone11ProMax, .iPhoneSE2, .iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhoneSE3, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPad2, .iPad3, .iPad4, .iPadAir, .iPadAir2, .iPad5, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini, .iPadMini2, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadMiniA20Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] } /// All devices that feature a normal camera @@ -2476,7 +2497,7 @@ extension Device { /// All devices that feature a wide camera public static var allDevicesWithWideCamera: [Device] { - return [.iPodTouch5, .iPodTouch6, .iPodTouch7, .iPhone4, .iPhone4s, .iPhone5, .iPhone5c, .iPhone5s, .iPhone6, .iPhone6Plus, .iPhone6s, .iPhone6sPlus, .iPhone7, .iPhone7Plus, .iPhoneSE, .iPhone8, .iPhone8Plus, .iPhoneX, .iPhoneXS, .iPhoneXSMax, .iPhoneXR, .iPhone11, .iPhone11Pro, .iPhone11ProMax, .iPhoneSE2, .iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhoneSE3, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPad2, .iPad3, .iPad4, .iPadAir, .iPadAir2, .iPad5, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini, .iPadMini2, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] + return [.iPodTouch5, .iPodTouch6, .iPodTouch7, .iPhone4, .iPhone4s, .iPhone5, .iPhone5c, .iPhone5s, .iPhone6, .iPhone6Plus, .iPhone6s, .iPhone6sPlus, .iPhone7, .iPhone7Plus, .iPhoneSE, .iPhone8, .iPhone8Plus, .iPhoneX, .iPhoneXS, .iPhoneXSMax, .iPhoneXR, .iPhone11, .iPhone11Pro, .iPhone11ProMax, .iPhoneSE2, .iPhone12, .iPhone12Mini, .iPhone12Pro, .iPhone12ProMax, .iPhone13, .iPhone13Mini, .iPhone13Pro, .iPhone13ProMax, .iPhoneSE3, .iPhone14, .iPhone14Plus, .iPhone14Pro, .iPhone14ProMax, .iPhone15, .iPhone15Plus, .iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e, .iPhone18Pro, .iPhone18ProMax, .iPhoneDuo, .iPad2, .iPad3, .iPad4, .iPadAir, .iPadAir2, .iPad5, .iPad6, .iPadAir3, .iPad7, .iPad8, .iPad9, .iPad10, .iPadA16, .iPadAir4, .iPadAir5, .iPadAir11M2, .iPadAir13M2, .iPadAir11M3, .iPadAir13M3, .iPadAir11M4, .iPadAir13M4, .iPadMini, .iPadMini2, .iPadMini3, .iPadMini4, .iPadMini5, .iPadMini6, .iPadMiniA17Pro, .iPadMiniA20Pro, .iPadPro9Inch, .iPadPro12Inch, .iPadPro12Inch2, .iPadPro10Inch, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, .iPadPro12Inch4, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, .iPadPro12Inch6, .iPadPro11M4, .iPadPro13M4, .iPadPro11M5, .iPadPro13M5] } /// All devices that feature a telephoto camera @@ -2587,6 +2608,7 @@ extension Device { case m3 case m4 case m5 + case s10 #elseif os(watchOS) case s1 case s1P @@ -2691,6 +2713,7 @@ extension Device { case .iPadMini5: return .a12Bionic case .iPadMini6: return .a15Bionic case .iPadMiniA17Pro: return .a17Pro + case .iPadMiniA20Pro: return .a20Pro case .iPadPro9Inch: return .a9X case .iPadPro12Inch: return .a9X case .iPadPro12Inch2: return .a10XFusion @@ -2708,6 +2731,7 @@ extension Device { case .iPadPro11M5: return .m5 case .iPadPro13M5: return .m5 case .homePod: return .a8 + case .homePodMini2: return .s10 case .simulator(let model): return model.cpu case .unknown: return .unknown } @@ -2758,6 +2782,7 @@ extension Device { case .appleTV4K: return .a10XFusion case .appleTV4K2: return .a12Bionic case .appleTV4K3: return .a15Bionic + case .appleTV4K4: return .a19 case .simulator(let model): return model.cpu case .unknown: return .unknown } @@ -2807,6 +2832,7 @@ extension Device.CPU: CustomStringConvertible { case .m3: return "M3" case .m4: return "M4" case .m5: return "M5" + case .s10: return "S10" case .unknown: return "unknown" } #elseif os(watchOS) diff --git a/Source/Device.swift.gyb b/Source/Device.swift.gyb index af7f2821..f9d52f8d 100644 --- a/Source/Device.swift.gyb +++ b/Source/Device.swift.gyb @@ -382,6 +382,12 @@ iPads = [ cpu="a17Pro", isPadMiniFormFactor=True, hasTouchID=True, hasRoundedDisplayCorners=True, applePencilSupport=34, cameras=1, hasUSBCConnectivity=True, has5gSupport=True, hasEsimSupport=True), + # Specs: https://www.apple.com/ipad-mini/specs/ (verified 2026-10-27). + # Provisional: identifiers are not public yet (ships 2026-11-06), so this model does not map from any identifier. + Device("iPadMiniA20Pro", "Device is an [iPad mini (A20 Pro)](https://www.apple.com/ipad-mini/specs/)", "", [], 8.5, (756, 1158), "iPad mini (A20 Pro)", "iPad mini (A20 Pro)", 326, + cpu="a20Pro", isPadMiniFormFactor=True, hasTouchID=True, hasRoundedDisplayCorners=True, + applePencilSupport=34, cameras=1, hasUSBCConnectivity=True, has5gSupport=True, + hasEsimSupport=True), Device("iPadPro9Inch", "Device is an [iPad Pro 9.7-inch](https://support.apple.com/kb/SP739)", "https://support.apple.com/library/APPLE/APPLECARE_ALLGEOS/SP739/SP739.png", ["iPad6,3", "iPad6,4"], 9.7, (3, 4), "iPad Pro (9.7-inch)", "iPad Pro (9.7-inch)", 264, cpu="a9X", isPro=True, hasTouchID=True, applePencilSupport=1, cameras=1), Device("iPadPro12Inch", "Device is an [iPad Pro 12-inch](https://support.apple.com/kb/SP723)", "https://support.apple.com/library/APPLE/APPLECARE_ALLGEOS/SP723/SP723-iPad_Pro_2x.png", ["iPad6,7", "iPad6,8"], 12.9, (3, 4), "iPad Pro (12.9-inch)", "iPad Pro (12.9-inch)", 264, @@ -443,6 +449,9 @@ iPads = [ homePods = [ Device("homePod", "Device is a [HomePod](https://support.apple.com/kb/SP773)", "https://support.apple.com/library/APPLE/APPLECARE_ALLGEOS/SP773/homepod_space_gray_large_2x.jpg", ["AudioAccessory1,1"], -1, (4, 5), "HomePod", "HomePod", -1, cpu="a8"), + # Identifiers: https://api.ipsw.me/v4/devices (verified 2026-10-27). + Device("homePodMini2", "Device is a [HomePod mini 2](https://www.apple.com/homepod-mini/specs/)", "", ["AudioAccessory11,1"], -1, (-1, -1), "HomePod mini 2", "HomePod mini 2", -1, + cpu="s10"), ] # tvOS tvs = [ @@ -454,6 +463,10 @@ tvs = [ cpu="a12Bionic"), 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, cpu="a15Bionic"), + # Specs: https://www.apple.com/apple-tv-4k/specs/ (verified 2026-10-27). + # Provisional: identifiers are not public yet (ships 2026-11-13), so this model does not map from any identifier. + Device("appleTV4K4", "Device is an [Apple TV 4K (4th generation)](https://www.apple.com/apple-tv-4k/specs/)", "", [], 0, (), "Apple TV 4K (4th generation)", "Apple TV 4K (4th generation)", -1, + cpu="a19"), ] # watchOS @@ -809,7 +822,9 @@ public enum Device { #if os(iOS) switch identifier { % for device in iOSDevices: +% if device.identifiers: case ${', '.join(list(map(lambda device: "\"" + device + "\"", device.identifiers)))}: return ${device.caseName} +% end % end case "i386", "x86_64", "arm64": return simulator(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS")) default: return unknown(identifier) @@ -817,7 +832,9 @@ public enum Device { #elseif os(tvOS) switch identifier { % for device in tvOSDevices: +% if device.identifiers: case ${', '.join(list(map(lambda device: "\"" + device + "\"", device.identifiers)))}: return ${device.caseName} +% end % end case "i386", "x86_64", "arm64": return simulator(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS")) default: return unknown(identifier) @@ -825,7 +842,9 @@ public enum Device { #elseif os(watchOS) switch identifier { % for device in watchOSDevices: +% if device.identifiers: case ${', '.join(list(map(lambda device: "\"" + device + "\"", device.identifiers)))}: return ${device.caseName} +% end % end case "i386", "x86_64", "arm64": return simulator(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "watchOS")) default: return unknown(identifier) @@ -1945,6 +1964,7 @@ iOS_cpus = [ CPU("m3", "M3"), CPU("m4", "M4"), CPU("m5", "M5"), + CPU("s10", "S10"), ] watchOS_cpus = [ diff --git a/Tests/Tests.swift b/Tests/Tests.swift index 2809633a..35819052 100644 --- a/Tests/Tests.swift +++ b/Tests/Tests.swift @@ -134,6 +134,51 @@ class DeviceKitTests: XCTestCase { } #endif + #if os(iOS) || os(tvOS) + func testOctober2026Devices() { + #if os(iOS) + // HomePod mini 2 has a verified identifier. + XCTAssertEqual(Device.mapToDevice(identifier: "AudioAccessory11,1"), .homePodMini2) + XCTAssertEqual(Device.homePodMini2.description, "HomePod mini 2") + XCTAssertEqual(Device.homePodMini2.cpu, .s10) + XCTAssertEqual(Device.CPU.s10.description, "S10") + + // iPad mini (A20 Pro) is provisional until its identifiers are public. + let model = Device.iPadMiniA20Pro + XCTAssertTrue(model.isPad) + XCTAssertTrue(Device.allRealDevices.contains(model)) + XCTAssertTrue(Device.allSimulators.contains(.simulator(model))) + XCTAssertTrue(Device.allMiniDevices.contains(model)) + XCTAssertEqual(model.cameras, [.wide]) + for variant in [model, .simulator(model)] { + XCTAssertEqual(variant.description, variant == model ? "iPad mini (A20 Pro)" : "Simulator (iPad mini (A20 Pro))") + XCTAssertEqual(variant.cpu, .a20Pro) + XCTAssertEqual(variant.diagonal, 8.5) + XCTAssertEqual(variant.screenRatio.width, 756) + XCTAssertEqual(variant.screenRatio.height, 1158) + XCTAssertEqual(variant.ppi, 326) + XCTAssertTrue(variant.isTouchIDCapable) + XCTAssertFalse(variant.isFaceIDCapable) + XCTAssertEqual(variant.applePencilSupport, [.firstGenerationUsbC, .pro]) + XCTAssertTrue(variant.hasEsimSupport) + XCTAssertFalse(variant.hasDualEsimSupport) + } + #else + // Apple TV 4K (4th generation) is provisional until its identifiers are public. + let model = Device.appleTV4K4 + XCTAssertTrue(Device.allRealDevices.contains(model)) + XCTAssertTrue(Device.allSimulators.contains(.simulator(model))) + XCTAssertEqual(model.description, "Apple TV 4K (4th generation)") + XCTAssertEqual(model.cpu, .a19) + XCTAssertEqual(model.ppi, nil) + #endif + // Unannounced identifiers must remain unknown. + for identifier in ["AudioAccessory11,2", "iPad18,1", "iPad18,2", "AppleTV15,1", ""] { + XCTAssertEqual(Device.mapToDevice(identifier: identifier), .unknown(identifier)) + } + } + #endif + func testDeviceSimulator() { #if os(macOS) XCTAssertFalse(device.isOneOf(Device.allSimulators)) @@ -232,6 +277,7 @@ class DeviceKitTests: XCTestCase { XCTAssertEqual(Device.iPadMini5.cpu, Device.CPU.a12Bionic) XCTAssertEqual(Device.iPadMini6.cpu, Device.CPU.a15Bionic) XCTAssertEqual(Device.iPadMiniA17Pro.cpu, Device.CPU.a17Pro) + XCTAssertEqual(Device.iPadMiniA20Pro.cpu, Device.CPU.a20Pro) XCTAssertEqual(Device.iPadPro11Inch.cpu, Device.CPU.a12XBionic) XCTAssertEqual(Device.iPadPro11Inch2.cpu, Device.CPU.a12ZBionic) XCTAssertEqual(Device.iPadPro11Inch3.cpu, Device.CPU.m1) @@ -248,6 +294,7 @@ class DeviceKitTests: XCTestCase { XCTAssertEqual(Device.appleTV4K.cpu, Device.CPU.a10XFusion) XCTAssertEqual(Device.appleTV4K2.cpu, Device.CPU.a12Bionic) XCTAssertEqual(Device.appleTV4K3.cpu, Device.CPU.a15Bionic) + XCTAssertEqual(Device.appleTV4K4.cpu, Device.CPU.a19) #endif } @@ -748,6 +795,7 @@ class DeviceKitTests: XCTestCase { .iPadAir13M4, .iPadMini6, .iPadMiniA17Pro, + .iPadMiniA20Pro, .iPadPro11Inch3, .iPadPro12Inch5, .iPadPro11Inch4, @@ -784,7 +832,8 @@ class DeviceKitTests: XCTestCase { .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e] let pads: [Device] = [.iPadAir11M2, .iPadAir13M2, .iPadPro11M4, .iPadPro13M4, .iPadMiniA17Pro, .iPadA16, .iPadAir11M3, .iPadAir13M3, - .iPadPro11M5, .iPadPro13M5, .iPadAir11M4, .iPadAir13M4] + .iPadPro11M5, .iPadPro13M5, .iPadAir11M4, .iPadAir13M4, + .iPadMiniA20Pro] for model in phones + pads { for variant in [model, .simulator(model)] { XCTAssertTrue(variant.hasEsimSupport, "\(variant)") @@ -928,6 +977,7 @@ class DeviceKitTests: XCTestCase { .iPadAir13M4, .iPadMini6, .iPadMiniA17Pro, + .iPadMiniA20Pro, .iPadPro11Inch, .iPadPro12Inch3, .iPadPro11Inch2, From 73e67c8bc78df6e35a17e2b101a0e567117e2dbe Mon Sep 17 00:00:00 2001 From: Zandor Smith Date: Tue, 27 Oct 2026 08:39:44 +0100 Subject: [PATCH 2/4] Link changelog entry to PR #520 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acbed6c2..57bc832d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### New October 2026 devices -This version adds support for HomePod mini 2, iPad mini (A20 Pro) and Apple TV 4K (4th generation), along with the `S10` CPU case on iOS and tvOS: (PR_LINK) +This version adds support for HomePod mini 2, iPad mini (A20 Pro) and Apple TV 4K (4th generation), along with the `S10` CPU case on iOS and tvOS: ([#520](https://github.com/devicekit/DeviceKit/pull/520)) | Device | Case value | | --- | --- | From 6246912271311b13724fe2e61b19dd9516bd39bc Mon Sep 17 00:00:00 2001 From: Zandor Smith Date: Tue, 27 Oct 2026 12:12:17 +0100 Subject: [PATCH 3/4] Drop HomePod mini 2 support Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +-- Source/Device.generated.swift | 11 ----------- Source/Device.swift.gyb | 4 ---- Tests/Tests.swift | 8 +------- 4 files changed, 2 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57bc832d..51735257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,10 @@ ### New October 2026 devices -This version adds support for HomePod mini 2, iPad mini (A20 Pro) and Apple TV 4K (4th generation), along with the `S10` CPU case on iOS and tvOS: ([#520](https://github.com/devicekit/DeviceKit/pull/520)) +This version adds support for iPad mini (A20 Pro) and Apple TV 4K (4th generation): ([#520](https://github.com/devicekit/DeviceKit/pull/520)) | Device | Case value | | --- | --- | -| HomePod mini 2 | `Device.homePodMini2` | | iPad mini (A20 Pro) | `Device.iPadMiniA20Pro` | | Apple TV 4K (4th generation) | `Device.appleTV4K4` | diff --git a/Source/Device.generated.swift b/Source/Device.generated.swift index faf2c25a..2ecbbb88 100644 --- a/Source/Device.generated.swift +++ b/Source/Device.generated.swift @@ -416,8 +416,6 @@ public enum Device { /// /// ![Image](https://support.apple.com/library/APPLE/APPLECARE_ALLGEOS/SP773/homepod_space_gray_large_2x.jpg) case homePod - /// Device is a [HomePod mini 2](https://www.apple.com/homepod-mini/specs/) - case homePodMini2 #elseif os(tvOS) /// Device is an [Apple TV HD](https://support.apple.com/kb/SP724) (Previously Apple TV (4th generation)) /// @@ -694,7 +692,6 @@ public enum Device { case "iPad17,1", "iPad17,2": return iPadPro11M5 case "iPad17,3", "iPad17,4": return iPadPro13M5 case "AudioAccessory1,1": return homePod - case "AudioAccessory11,1": return homePodMini2 case "i386", "x86_64", "arm64": return simulator(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS")) default: return unknown(identifier) } @@ -890,7 +887,6 @@ public enum Device { case .iPadPro11M5: return 11.0 case .iPadPro13M5: return 13.0 case .homePod: return -1 - case .homePodMini2: return -1 case .simulator(let model): return model.diagonal case .unknown: return -1 } @@ -1044,7 +1040,6 @@ public enum Device { case .iPadPro11M5: return (width: 605, height: 417) case .iPadPro13M5: return (width: 4, height: 3) case .homePod: return (width: 4, height: 5) - case .homePodMini2: return (width: -1, height: -1) case .simulator(let model): return model.screenRatio case .unknown: return (width: -1, height: -1) } @@ -1632,7 +1627,6 @@ public enum Device { case .iPadPro11M5: return 264 case .iPadPro13M5: return 264 case .homePod: return -1 - case .homePodMini2: return -1 case .simulator(let model): return model.ppi case .unknown: return nil } @@ -1818,7 +1812,6 @@ extension Device: CustomStringConvertible { case .iPadPro11M5: return "iPad Pro (11-inch) (M5)" case .iPadPro13M5: return "iPad Pro (13-inch) (M5)" case .homePod: return "HomePod" - case .homePodMini2: return "HomePod mini 2" case .simulator(let model): return "Simulator (\(model.description))" case .unknown(let identifier): return identifier } @@ -1992,7 +1985,6 @@ extension Device: CustomStringConvertible { case .iPadPro11M5: return "iPad Pro (11-inch) (M5)" case .iPadPro13M5: return "iPad Pro (13-inch) (M5)" case .homePod: return "HomePod" - case .homePodMini2: return "HomePod mini 2" case .simulator(let model): return "Simulator (\(model.safeDescription))" case .unknown(let identifier): return identifier } @@ -2608,7 +2600,6 @@ extension Device { case m3 case m4 case m5 - case s10 #elseif os(watchOS) case s1 case s1P @@ -2731,7 +2722,6 @@ extension Device { case .iPadPro11M5: return .m5 case .iPadPro13M5: return .m5 case .homePod: return .a8 - case .homePodMini2: return .s10 case .simulator(let model): return model.cpu case .unknown: return .unknown } @@ -2832,7 +2822,6 @@ extension Device.CPU: CustomStringConvertible { case .m3: return "M3" case .m4: return "M4" case .m5: return "M5" - case .s10: return "S10" case .unknown: return "unknown" } #elseif os(watchOS) diff --git a/Source/Device.swift.gyb b/Source/Device.swift.gyb index f9d52f8d..c8a554ab 100644 --- a/Source/Device.swift.gyb +++ b/Source/Device.swift.gyb @@ -449,9 +449,6 @@ iPads = [ homePods = [ Device("homePod", "Device is a [HomePod](https://support.apple.com/kb/SP773)", "https://support.apple.com/library/APPLE/APPLECARE_ALLGEOS/SP773/homepod_space_gray_large_2x.jpg", ["AudioAccessory1,1"], -1, (4, 5), "HomePod", "HomePod", -1, cpu="a8"), - # Identifiers: https://api.ipsw.me/v4/devices (verified 2026-10-27). - Device("homePodMini2", "Device is a [HomePod mini 2](https://www.apple.com/homepod-mini/specs/)", "", ["AudioAccessory11,1"], -1, (-1, -1), "HomePod mini 2", "HomePod mini 2", -1, - cpu="s10"), ] # tvOS tvs = [ @@ -1964,7 +1961,6 @@ iOS_cpus = [ CPU("m3", "M3"), CPU("m4", "M4"), CPU("m5", "M5"), - CPU("s10", "S10"), ] watchOS_cpus = [ diff --git a/Tests/Tests.swift b/Tests/Tests.swift index 35819052..1f0fa276 100644 --- a/Tests/Tests.swift +++ b/Tests/Tests.swift @@ -137,12 +137,6 @@ class DeviceKitTests: XCTestCase { #if os(iOS) || os(tvOS) func testOctober2026Devices() { #if os(iOS) - // HomePod mini 2 has a verified identifier. - XCTAssertEqual(Device.mapToDevice(identifier: "AudioAccessory11,1"), .homePodMini2) - XCTAssertEqual(Device.homePodMini2.description, "HomePod mini 2") - XCTAssertEqual(Device.homePodMini2.cpu, .s10) - XCTAssertEqual(Device.CPU.s10.description, "S10") - // iPad mini (A20 Pro) is provisional until its identifiers are public. let model = Device.iPadMiniA20Pro XCTAssertTrue(model.isPad) @@ -173,7 +167,7 @@ class DeviceKitTests: XCTestCase { XCTAssertEqual(model.ppi, nil) #endif // Unannounced identifiers must remain unknown. - for identifier in ["AudioAccessory11,2", "iPad18,1", "iPad18,2", "AppleTV15,1", ""] { + for identifier in ["iPad18,1", "iPad18,2", "AppleTV15,1", ""] { XCTAssertEqual(Device.mapToDevice(identifier: identifier), .unknown(identifier)) } } From 2b5bd3d259d1d38342c2408fe6524ff302f2d0d7 Mon Sep 17 00:00:00 2001 From: Zandor Smith Date: Tue, 27 Oct 2026 12:17:20 +0100 Subject: [PATCH 4/4] Add iPad mini (A20 Pro) and Apple TV 4K (4th generation) identifiers Map iPad19,1 and iPad19,2 to iPad mini (A20 Pro) and AppleTV18,1 to Apple TV 4K (4th generation), as listed on The Apple Wiki. Revert the mapToDevice template change for devices without identifiers, since no device needs it any more. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +---- Source/Device.generated.swift | 2 ++ Source/Device.swift.gyb | 14 ++++---------- Tests/Tests.swift | 8 +++++--- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51735257..f96c3c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,13 @@ ### New October 2026 devices -This version adds support for iPad mini (A20 Pro) and Apple TV 4K (4th generation): ([#520](https://github.com/devicekit/DeviceKit/pull/520)) +This version adds support for iPad mini (A20 Pro) and Apple TV 4K (4th generation), including hardware identifiers, display metrics and technical specification links: ([#520](https://github.com/devicekit/DeviceKit/pull/520)) | Device | Case value | | --- | --- | | iPad mini (A20 Pro) | `Device.iPadMiniA20Pro` | | Apple TV 4K (4th generation) | `Device.appleTV4K4` | -iPad mini (A20 Pro) and Apple TV 4K (4th generation) are provisional. Their hardware identifiers aren't public yet, so `Device.current` and `mapToDevice(identifier:)` won't return them until a later update adds the identifiers. - ## Version 5.10.0 Releasedate: 2026-11-01 @@ -38,7 +36,6 @@ Display metrics describe the outer display: 5.4 inches, a 233:339 aspect ratio ( ### Bug fixes - Fix `isSimulator` returning `false` for simulators of unknown devices. ([#507](https://github.com/devicekit/DeviceKit/pull/507)) - ## Version 5.9.0 Releasedate: 2026-09-22 diff --git a/Source/Device.generated.swift b/Source/Device.generated.swift index 2ecbbb88..e473855a 100644 --- a/Source/Device.generated.swift +++ b/Source/Device.generated.swift @@ -675,6 +675,7 @@ public enum Device { case "iPad11,1", "iPad11,2": return iPadMini5 case "iPad14,1", "iPad14,2": return iPadMini6 case "iPad16,1", "iPad16,2": return iPadMiniA17Pro + case "iPad19,1", "iPad19,2": return iPadMiniA20Pro case "iPad6,3", "iPad6,4": return iPadPro9Inch case "iPad6,7", "iPad6,8": return iPadPro12Inch case "iPad7,1", "iPad7,2": return iPadPro12Inch2 @@ -701,6 +702,7 @@ public enum Device { case "AppleTV6,2": return appleTV4K case "AppleTV11,1": return appleTV4K2 case "AppleTV14,1": return appleTV4K3 + case "AppleTV18,1": return appleTV4K4 case "i386", "x86_64", "arm64": return simulator(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS")) default: return unknown(identifier) } diff --git a/Source/Device.swift.gyb b/Source/Device.swift.gyb index c8a554ab..fa54e830 100644 --- a/Source/Device.swift.gyb +++ b/Source/Device.swift.gyb @@ -383,8 +383,8 @@ iPads = [ applePencilSupport=34, cameras=1, hasUSBCConnectivity=True, has5gSupport=True, hasEsimSupport=True), # Specs: https://www.apple.com/ipad-mini/specs/ (verified 2026-10-27). - # Provisional: identifiers are not public yet (ships 2026-11-06), so this model does not map from any identifier. - Device("iPadMiniA20Pro", "Device is an [iPad mini (A20 Pro)](https://www.apple.com/ipad-mini/specs/)", "", [], 8.5, (756, 1158), "iPad mini (A20 Pro)", "iPad mini (A20 Pro)", 326, + # Identifiers: https://theapplewiki.com/wiki/Firmware/iPad_mini/27.x (verified 2026-10-27). + Device("iPadMiniA20Pro", "Device is an [iPad mini (A20 Pro)](https://www.apple.com/ipad-mini/specs/)", "", ["iPad19,1", "iPad19,2"], 8.5, (756, 1158), "iPad mini (A20 Pro)", "iPad mini (A20 Pro)", 326, cpu="a20Pro", isPadMiniFormFactor=True, hasTouchID=True, hasRoundedDisplayCorners=True, applePencilSupport=34, cameras=1, hasUSBCConnectivity=True, has5gSupport=True, hasEsimSupport=True), @@ -461,8 +461,8 @@ 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, cpu="a15Bionic"), # Specs: https://www.apple.com/apple-tv-4k/specs/ (verified 2026-10-27). - # Provisional: identifiers are not public yet (ships 2026-11-13), so this model does not map from any identifier. - Device("appleTV4K4", "Device is an [Apple TV 4K (4th generation)](https://www.apple.com/apple-tv-4k/specs/)", "", [], 0, (), "Apple TV 4K (4th generation)", "Apple TV 4K (4th generation)", -1, + # Identifiers: https://theapplewiki.com/wiki/Apple_TV_4K_(4th_generation) (verified 2026-10-27). + Device("appleTV4K4", "Device is an [Apple TV 4K (4th generation)](https://www.apple.com/apple-tv-4k/specs/)", "", ["AppleTV18,1"], 0, (), "Apple TV 4K (4th generation)", "Apple TV 4K (4th generation)", -1, cpu="a19"), ] @@ -819,9 +819,7 @@ public enum Device { #if os(iOS) switch identifier { % for device in iOSDevices: -% if device.identifiers: case ${', '.join(list(map(lambda device: "\"" + device + "\"", device.identifiers)))}: return ${device.caseName} -% end % end case "i386", "x86_64", "arm64": return simulator(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS")) default: return unknown(identifier) @@ -829,9 +827,7 @@ public enum Device { #elseif os(tvOS) switch identifier { % for device in tvOSDevices: -% if device.identifiers: case ${', '.join(list(map(lambda device: "\"" + device + "\"", device.identifiers)))}: return ${device.caseName} -% end % end case "i386", "x86_64", "arm64": return simulator(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS")) default: return unknown(identifier) @@ -839,9 +835,7 @@ public enum Device { #elseif os(watchOS) switch identifier { % for device in watchOSDevices: -% if device.identifiers: case ${', '.join(list(map(lambda device: "\"" + device + "\"", device.identifiers)))}: return ${device.caseName} -% end % end case "i386", "x86_64", "arm64": return simulator(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "watchOS")) default: return unknown(identifier) diff --git a/Tests/Tests.swift b/Tests/Tests.swift index 1f0fa276..ac21f75b 100644 --- a/Tests/Tests.swift +++ b/Tests/Tests.swift @@ -137,8 +137,10 @@ class DeviceKitTests: XCTestCase { #if os(iOS) || os(tvOS) func testOctober2026Devices() { #if os(iOS) - // iPad mini (A20 Pro) is provisional until its identifiers are public. let model = Device.iPadMiniA20Pro + for identifier in ["iPad19,1", "iPad19,2"] { + XCTAssertEqual(Device.mapToDevice(identifier: identifier), model, identifier) + } XCTAssertTrue(model.isPad) XCTAssertTrue(Device.allRealDevices.contains(model)) XCTAssertTrue(Device.allSimulators.contains(.simulator(model))) @@ -158,8 +160,8 @@ class DeviceKitTests: XCTestCase { XCTAssertFalse(variant.hasDualEsimSupport) } #else - // Apple TV 4K (4th generation) is provisional until its identifiers are public. let model = Device.appleTV4K4 + XCTAssertEqual(Device.mapToDevice(identifier: "AppleTV18,1"), model) XCTAssertTrue(Device.allRealDevices.contains(model)) XCTAssertTrue(Device.allSimulators.contains(.simulator(model))) XCTAssertEqual(model.description, "Apple TV 4K (4th generation)") @@ -167,7 +169,7 @@ class DeviceKitTests: XCTestCase { XCTAssertEqual(model.ppi, nil) #endif // Unannounced identifiers must remain unknown. - for identifier in ["iPad18,1", "iPad18,2", "AppleTV15,1", ""] { + for identifier in ["iPad19,3", "iPad19,4", "AppleTV18,2", ""] { XCTAssertEqual(Device.mapToDevice(identifier: identifier), .unknown(identifier)) } }