From 826344fc0459a65faf0785fa94a7464ea72fa652 Mon Sep 17 00:00:00 2001 From: Simon Mau Date: Thu, 23 Apr 2026 01:17:38 +0800 Subject: [PATCH 1/4] feat(ios): add Action Button capability detection --- README.md | 9 +++++++++ Source/Device.generated.swift | 20 ++++++++++++++++++++ Source/Device.swift.gyb | 20 ++++++++++++++++++++ Tests/Tests.swift | 17 +++++++++++++++++ 4 files changed, 66 insertions(+) diff --git a/README.md b/README.md index 10abba0f..aa09c96e 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ See our detailed [changelog](CHANGELOG.md) for the latest features, improvements - [x] Detect available sensors (Touch ID, Face ID) - [x] Detect available disk space - [x] Apple Pencil support detection +- [x] Action Button detection ## Requirements @@ -169,6 +170,14 @@ if device.isSimulator { } ``` +### Check if a device has an Action Button +```swift +let device = Device.current +if device.hasActionButton { + // Action Button specific behavior +} +``` + ### Get the Simulator Device ```swift let device = Device.current diff --git a/Source/Device.generated.swift b/Source/Device.generated.swift index bd5a0638..03a5ba25 100644 --- a/Source/Device.generated.swift +++ b/Source/Device.generated.swift @@ -1265,6 +1265,26 @@ public enum Device { return isOneOf(Device.allDevicesWithDynamicIsland) || isOneOf(Device.allDevicesWithDynamicIsland.map(Device.simulator)) } + /// All devices that have an Action Button. + public static var allDevicesWithActionButton: [Device] { + return [ + .iPhone15Pro, + .iPhone15ProMax, + .iPhone16, + .iPhone16Plus, + .iPhone16Pro, + .iPhone16ProMax, + .iPhone17, + .iPhone17Pro, + .iPhone17ProMax, + ] + } + + /// Returns whether or not the device has an Action Button. + public var hasActionButton: Bool { + return isOneOf(Device.allDevicesWithActionButton) || isOneOf(Device.allDevicesWithActionButton.map(Device.simulator)) + } + /// All devices that have 3D Touch support. public static var allDevicesWith3dTouchSupport: [Device] { return [.iPhone6s, .iPhone6sPlus, .iPhone7, .iPhone7Plus, .iPhone8, .iPhone8Plus, .iPhoneX, .iPhoneXS, .iPhoneXSMax] diff --git a/Source/Device.swift.gyb b/Source/Device.swift.gyb index 7a61e94a..35533daa 100644 --- a/Source/Device.swift.gyb +++ b/Source/Device.swift.gyb @@ -969,6 +969,26 @@ public enum Device { return isOneOf(Device.allDevicesWithDynamicIsland) || isOneOf(Device.allDevicesWithDynamicIsland.map(Device.simulator)) } + /// All devices that have an Action Button. + public static var allDevicesWithActionButton: [Device] { + return [ + .iPhone15Pro, + .iPhone15ProMax, + .iPhone16, + .iPhone16Plus, + .iPhone16Pro, + .iPhone16ProMax, + .iPhone17, + .iPhone17Pro, + .iPhone17ProMax, + ] + } + + /// Returns whether or not the device has an Action Button. + public var hasActionButton: Bool { + return isOneOf(Device.allDevicesWithActionButton) || isOneOf(Device.allDevicesWithActionButton.map(Device.simulator)) + } + /// All devices that have 3D Touch support. public static var allDevicesWith3dTouchSupport: [Device] { return [${', '.join(list(map(lambda device: "." + device.caseName, list(filter(lambda device: device.hasForce3dTouchSupport == True, iOSDevices)))))}] diff --git a/Tests/Tests.swift b/Tests/Tests.swift index 1e87af88..618935d7 100644 --- a/Tests/Tests.swift +++ b/Tests/Tests.swift @@ -551,6 +551,23 @@ class DeviceKitTests: XCTestCase { } } + func testHasActionButton() { + let actionButtonDevices: [Device] = [ + .iPhone15Pro, + .iPhone15ProMax, + .iPhone16, + .iPhone16Plus, + .iPhone16Pro, + .iPhone16ProMax, + .iPhone17, + .iPhone17Pro, + .iPhone17ProMax, + ] + for device in Device.allRealDevices { + XCTAssertTrue(device.hasActionButton == device.isOneOf(actionButtonDevices), "testHasActionButton failed for \(device.description)") + } + } + func testHas5gSupport() { let has5gDevices: [Device] = [ .iPhone12, From 26c40397c8fb0b7de8d9cd9e13948337fa14bcd7 Mon Sep 17 00:00:00 2001 From: Zandor Smith Date: Thu, 10 Sep 2026 10:22:28 +0200 Subject: [PATCH 2/4] Generate Action Button support from device feature parameters --- Source/Device.swift.gyb | 50 ++++++++++++++++++++++------------------- Tests/Tests.swift | 10 ++++++++- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/Source/Device.swift.gyb b/Source/Device.swift.gyb index 35533daa..b38fd17b 100644 --- a/Source/Device.swift.gyb +++ b/Source/Device.swift.gyb @@ -24,6 +24,7 @@ class Device: supportsWirelessCharging=False, hasRoundedDisplayCorners=False, hasDynamicIsland=False, + hasActionButton=False, applePencilSupport=0, hasForce3dTouchSupport=False, cameras=0, @@ -50,6 +51,7 @@ class Device: self.supportsWirelessCharging = supportsWirelessCharging self.hasRoundedDisplayCorners = hasRoundedDisplayCorners self.hasDynamicIsland = hasDynamicIsland + self.hasActionButton = hasActionButton self.applePencilSupport = applePencilSupport self.hasForce3dTouchSupport = hasForce3dTouchSupport self.cameras = cameras @@ -184,54 +186,66 @@ iPhones = [ Device("iPhone15Pro", "Device is an [iPhone 15 Pro](https://support.apple.com/en-us/111829)", "", ["iPhone16,1"], 6.1, (9, 19.5), "iPhone 15 Pro", "iPhone 15 Pro", 460, cpu="a17Pro", isPro=True, isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, hasDynamicIsland=True, - cameras=123, hasLidarSensor=True, hasUSBCConnectivity=True, has5gSupport=True), + cameras=123, hasLidarSensor=True, hasUSBCConnectivity=True, has5gSupport=True, + hasActionButton=True), Device("iPhone15ProMax", "Device is an [iPhone 15 Pro Max](https://support.apple.com/en-us/111828)", "", ["iPhone16,2"], 6.7, (9, 19.5), "iPhone 15 Pro Max", "iPhone 15 Pro Max", 460, cpu="a17Pro", isPlusFormFactor=True, isPro=True, isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, hasDynamicIsland=True, cameras=123, hasLidarSensor=True, hasUSBCConnectivity=True, - has5gSupport=True), + has5gSupport=True, + hasActionButton=True), Device("iPhone16", "Device is an [iPhone 16]()", "", ["iPhone17,3"], 6.1, (9, 19.5), "iPhone 16", "iPhone 16", 460, cpu="a18", isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, hasDynamicIsland=True, - cameras=13, hasUSBCConnectivity=True, has5gSupport=True), + cameras=13, hasUSBCConnectivity=True, has5gSupport=True, + hasActionButton=True), Device("iPhone16Plus", "Device is an [iPhone 16 Plus]()", "", ["iPhone17,4"], 6.7, (9, 19.5), "iPhone 16 Plus", "iPhone 16 Plus", 460, cpu="a18", isPlusFormFactor=True, isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, hasDynamicIsland=True, - cameras=13, hasUSBCConnectivity=True, has5gSupport=True), + cameras=13, hasUSBCConnectivity=True, has5gSupport=True, + hasActionButton=True), Device("iPhone16Pro", "Device is an [iPhone 16 Pro]()", "", ["iPhone17,1"], 6.3, (9, 19.5), "iPhone 16 Pro", "iPhone 16 Pro", 460, cpu="a18Pro", isPro=True, isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, hasDynamicIsland=True, - cameras=123, hasLidarSensor=True, hasUSBCConnectivity=True, has5gSupport=True), + cameras=123, hasLidarSensor=True, hasUSBCConnectivity=True, has5gSupport=True, + hasActionButton=True), Device("iPhone16ProMax", "Device is an [iPhone 16 Pro Max]()", "", ["iPhone17,2"], 6.9, (9, 19.5), "iPhone 16 Pro Max", "iPhone 16 Pro Max", 460, cpu="a18Pro", isPlusFormFactor=True, isPro=True, isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, hasDynamicIsland=True, cameras=123, hasLidarSensor=True, hasUSBCConnectivity=True, - has5gSupport=True), + has5gSupport=True, + hasActionButton=True), Device("iPhone16e", "Device is an [iPhone 16e](https://support.apple.com/en-us/122208)", "https://cdsassets.apple.com/live/7WUAS350/images/tech-specs/122208-iphone-16e.png", ["iPhone17,5"], 6.1, (9, 19.5), "iPhone 16e", "iPhone 16e", 460, cpu="a18", isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, cameras=1, - hasUSBCConnectivity=True, has5gSupport=True), + hasUSBCConnectivity=True, has5gSupport=True, + hasActionButton=True), Device("iPhone17", "Device is an [iPhone 17]()", "", ["iPhone18,3"], 6.3, (9, 19.5), "iPhone 17", "iPhone 17", 460, cpu="a19", isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, hasDynamicIsland=True, - cameras=13, hasUSBCConnectivity=True, has5gSupport=True), + cameras=13, hasUSBCConnectivity=True, has5gSupport=True, + hasActionButton=True), Device("iPhone17Pro", "Device is an [iPhone 17 Pro]()", "", ["iPhone18,1"], 6.3, (9, 19.5), "iPhone 17 Pro", "iPhone 17 Pro", 460, cpu="a19Pro", isPro=True, isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, hasDynamicIsland=True, - cameras=123, hasLidarSensor=True, hasUSBCConnectivity=True, has5gSupport=True), + cameras=123, hasLidarSensor=True, hasUSBCConnectivity=True, has5gSupport=True, + hasActionButton=True), Device("iPhone17ProMax", "Device is an [iPhone 17 Pro Max]()", "", ["iPhone18,2"], 6.9, (9, 19.5), "iPhone 17 Pro Max", "iPhone 17 Pro Max", 460, cpu="a19Pro", isPlusFormFactor=True, isPro=True, isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, hasDynamicIsland=True, cameras=123, hasLidarSensor=True, hasUSBCConnectivity=True, - has5gSupport=True), + has5gSupport=True, + hasActionButton=True), Device("iPhoneAir", "Device is an [iPhone Air]()", "", ["iPhone18,4"], 6.5, (9, 19.5), "iPhone Air", "iPhone Air", 460, cpu="a19Pro", isPlusFormFactor=True, isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, hasDynamicIsland=True, - cameras=1, hasUSBCConnectivity=True, has5gSupport=True), + cameras=1, hasUSBCConnectivity=True, has5gSupport=True, + hasActionButton=True), Device("iPhone17e", "Device is an [iPhone 17e]()", "", ["iPhone18,5"], 6.1, (9, 19.5), "iPhone 17e", "iPhone 17e", 460, cpu="a19", isXSeries=True, hasFaceID=True, hasSensorHousing=True, supportsWirelessCharging=True, hasRoundedDisplayCorners=True, cameras=1, - hasUSBCConnectivity=True, has5gSupport=True), + hasUSBCConnectivity=True, has5gSupport=True, + hasActionButton=True), ] iPads = [ @@ -971,17 +985,7 @@ public enum Device { /// All devices that have an Action Button. public static var allDevicesWithActionButton: [Device] { - return [ - .iPhone15Pro, - .iPhone15ProMax, - .iPhone16, - .iPhone16Plus, - .iPhone16Pro, - .iPhone16ProMax, - .iPhone17, - .iPhone17Pro, - .iPhone17ProMax, - ] + return [${', '.join(list(map(lambda device: "." + device.caseName, list(filter(lambda device: device.hasActionButton == True, iOSDevices)))))}] } /// Returns whether or not the device has an Action Button. diff --git a/Tests/Tests.swift b/Tests/Tests.swift index 618935d7..45219a17 100644 --- a/Tests/Tests.swift +++ b/Tests/Tests.swift @@ -559,13 +559,21 @@ class DeviceKitTests: XCTestCase { .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, + .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, + .iPhoneAir, + .iPhone17e, ] + XCTAssertEqual(Device.allDevicesWithActionButton, actionButtonDevices) for device in Device.allRealDevices { - XCTAssertTrue(device.hasActionButton == device.isOneOf(actionButtonDevices), "testHasActionButton failed for \(device.description)") + let expected = device.isOneOf(actionButtonDevices) + XCTAssertEqual(device.hasActionButton, expected, "Action Button support for \(device)") + XCTAssertEqual(Device.simulator(device).hasActionButton, expected, "Simulator Action Button support for \(device)") } + XCTAssertFalse(Device.unknown("unrecognized-device").hasActionButton) + XCTAssertFalse(Device.simulator(.unknown("unrecognized-device")).hasActionButton) } func testHas5gSupport() { From 48d27a545dbda75419022a9e5fe13042bb1fa63c Mon Sep 17 00:00:00 2001 From: Zandor Smith Date: Thu, 10 Sep 2026 10:22:28 +0200 Subject: [PATCH 3/4] Regenerate Swift device definitions for Action Button support --- Source/Device.generated.swift | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Source/Device.generated.swift b/Source/Device.generated.swift index 03a5ba25..13e7908b 100644 --- a/Source/Device.generated.swift +++ b/Source/Device.generated.swift @@ -1267,17 +1267,7 @@ public enum Device { /// All devices that have an Action Button. public static var allDevicesWithActionButton: [Device] { - return [ - .iPhone15Pro, - .iPhone15ProMax, - .iPhone16, - .iPhone16Plus, - .iPhone16Pro, - .iPhone16ProMax, - .iPhone17, - .iPhone17Pro, - .iPhone17ProMax, - ] + return [.iPhone15Pro, .iPhone15ProMax, .iPhone16, .iPhone16Plus, .iPhone16Pro, .iPhone16ProMax, .iPhone16e, .iPhone17, .iPhone17Pro, .iPhone17ProMax, .iPhoneAir, .iPhone17e] } /// Returns whether or not the device has an Action Button. From 6b66a9a08f701c76ece605c3c9eaada528126d00 Mon Sep 17 00:00:00 2001 From: Zandor Smith Date: Thu, 10 Sep 2026 10:22:28 +0200 Subject: [PATCH 4/4] Document Action Button detection in changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03602d0c..1ca292ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New features + +- Add `Device.hasActionButton` and `Device.allDevicesWithActionButton` on iOS, including simulator detection, with support generated from GYB device parameters. ([#499](https://github.com/devicekit/DeviceKit/pull/499)) + ### Maintenance - Simplify GYB device definitions with named feature parameters and defaults for unsupported features, preserving the generated Swift output. ([#508](https://github.com/devicekit/DeviceKit/pull/508))