diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d8410..ddea5e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### Provisional iPhone Duo support + +- Add `Device.iPhoneDuo` with the A20 Pro chip and `isFoldable`, true only for iPhone Duo and its simulator. +- Recognize the `iPhone19,4` hardware identifier and omit the image. Display metrics remain unknown (`-1` for diagonal/ratio, `nil` for PPI); other unconfirmed capabilities use false/empty defaults. + +### Bug fixes + +- Make `isSimulator` return `true` for simulators of unknown devices, so detecting a simulator does not require support for its model. + ## Version 5.9.0 Releasedate: 2026-09-22 diff --git a/Source/Device.generated.swift b/Source/Device.generated.swift index 1fc5534..d54aacb 100644 --- a/Source/Device.generated.swift +++ b/Source/Device.generated.swift @@ -236,6 +236,8 @@ public enum Device { case iPhone18Pro /// Device is an [iPhone 18 Pro Max](https://support.apple.com/en-us/148591) case iPhone18ProMax + /// Provisional iPhone Duo; display metrics and other capabilities await verification. + case iPhoneDuo /// Device is an [iPad 2](https://support.apple.com/kb/SP622) /// /// ![Image](https://support.apple.com/library/APPLE/APPLECARE_ALLGEOS/SP622/SP622_01-ipad2-mul.png) @@ -638,6 +640,7 @@ public enum Device { case "iPhone18,5": return iPhone17e case "iPhone19,2": return iPhone18Pro case "iPhone19,3", "iPhone19,7": return iPhone18ProMax + case "iPhone19,4": return iPhoneDuo case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return iPad2 case "iPad3,1", "iPad3,2", "iPad3,3": return iPad3 case "iPad3,4", "iPad3,5", "iPad3,6": return iPad4 @@ -744,6 +747,19 @@ public enum Device { #endif } + /// Returns whether the device has a foldable display, including simulators. + public var isFoldable: Bool { + #if os(iOS) + switch self { + case .iPhoneDuo: return true + case .simulator(let model): return model.isFoldable + default: return false + } + #else + return false + #endif + } + /// Get the real device from a device. /// If the device is a an iPhone8Plus simulator this function returns .iPhone8Plus (the real device). /// If the parameter is a real device, this function returns just that passed parameter. @@ -818,6 +834,7 @@ public enum Device { case .iPhone17e: return 6.1 case .iPhone18Pro: return 6.3 case .iPhone18ProMax: return 6.9 + case .iPhoneDuo: return -1 case .iPad2: return 9.7 case .iPad3: return 9.7 case .iPad4: return 9.7 @@ -969,6 +986,7 @@ public enum Device { case .iPhone17e: return (width: 9, height: 19.5) case .iPhone18Pro: return (width: 9, height: 19.5) case .iPhone18ProMax: return (width: 9, height: 19.5) + case .iPhoneDuo: return (width: -1, height: -1) case .iPad2: return (width: 3, height: 4) case .iPad3: return (width: 3, height: 4) case .iPad4: return (width: 3, height: 4) @@ -1073,7 +1091,7 @@ public enum Device { /// All iPhones public static var allPhones: [Device] { - return [.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] + return [.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] } /// All iPads @@ -1379,10 +1397,13 @@ public enum Device { return allRealDevices.map(Device.simulator) } - /// Returns whether the device is any of the simulator + /// Returns whether the device is a simulator, including simulators of unknown devices. /// Useful when there is a need to check and skip running a portion of code (location request or others) public var isSimulator: Bool { - return isOneOf(Device.allSimulators) + if case .simulator = self { + return true + } + return false } /** @@ -1551,6 +1572,7 @@ public enum Device { case .iPhone17e: return 460 case .iPhone18Pro: return 460 case .iPhone18ProMax: return 460 + case .iPhoneDuo: return nil case .iPad2: return 132 case .iPad3: return 264 case .iPad4: return 264 @@ -1734,6 +1756,7 @@ extension Device: CustomStringConvertible { case .iPhone17e: return "iPhone 17e" case .iPhone18Pro: return "iPhone 18 Pro" case .iPhone18ProMax: return "iPhone 18 Pro Max" + case .iPhoneDuo: return "iPhone Duo" case .iPad2: return "iPad 2" case .iPad3: return "iPad (3rd generation)" case .iPad4: return "iPad (4th generation)" @@ -1904,6 +1927,7 @@ extension Device: CustomStringConvertible { case .iPhone17e: return "iPhone 17e" case .iPhone18Pro: return "iPhone 18 Pro" case .iPhone18ProMax: return "iPhone 18 Pro Max" + case .iPhoneDuo: return "iPhone Duo" case .iPad2: return "iPad 2" case .iPad3: return "iPad (3rd generation)" case .iPad4: return "iPad (4th generation)" @@ -2634,6 +2658,7 @@ extension Device { case .iPhone17e: return .a19 case .iPhone18Pro: return .a20Pro case .iPhone18ProMax: return .a20Pro + case .iPhoneDuo: return .a20Pro case .iPad2: return .a5 case .iPad3: return .a5X case .iPad4: return .a6X diff --git a/Source/Device.swift.gyb b/Source/Device.swift.gyb index 63b4348..e878903 100644 --- a/Source/Device.swift.gyb +++ b/Source/Device.swift.gyb @@ -32,6 +32,7 @@ class Device: has5gSupport=False, hasEsimSupport=False, hasDualEsimSupport=False, + isFoldable=False, ): self.caseName = caseName self.comment = comment @@ -61,6 +62,7 @@ class Device: self.has5gSupport = has5gSupport self.hasEsimSupport = hasEsimSupport self.hasDualEsimSupport = hasDualEsimSupport + self.isFoldable = isFoldable # iOS iPods = [ @@ -284,6 +286,10 @@ iPhones = [ hasDynamicIsland=True, cameras=123, hasLidarSensor=True, hasUSBCConnectivity=True, has5gSupport=True, hasEsimSupport=True, hasDualEsimSupport=True), + + # Duo capabilities other than its chip and foldability are unconfirmed (false/empty defaults). + Device("iPhoneDuo", "Provisional iPhone Duo; display metrics and other capabilities await verification.", "", ["iPhone19,4"], -1, (-1, -1), "iPhone Duo", "iPhone Duo", "nil", + cpu="a20Pro", isFoldable=True), ] iPads = [ @@ -828,6 +834,23 @@ public enum Device { #endif } + /// Returns whether the device has a foldable display, including simulators. + public var isFoldable: Bool { + #if os(iOS) + switch self { +% for device in iOSDevices: +% if device.isFoldable: + case .${device.caseName}: return true +% end +% end + case .simulator(let model): return model.isFoldable + default: return false + } + #else + return false + #endif + } + /// Get the real device from a device. /// If the device is a an iPhone8Plus simulator this function returns .iPhone8Plus (the real device). /// If the parameter is a real device, this function returns just that passed parameter. @@ -1205,10 +1228,13 @@ public enum Device { return allRealDevices.map(Device.simulator) } - /// Returns whether the device is any of the simulator + /// Returns whether the device is a simulator, including simulators of unknown devices. /// Useful when there is a need to check and skip running a portion of code (location request or others) public var isSimulator: Bool { - return isOneOf(Device.allSimulators) + if case .simulator = self { + return true + } + return false } /** diff --git a/Tests/Tests.swift b/Tests/Tests.swift index ce99bbc..74e298f 100644 --- a/Tests/Tests.swift +++ b/Tests/Tests.swift @@ -16,6 +16,43 @@ class DeviceKitTests: XCTestCase { let device = Device.current + func testIsFoldable() { + for model in Device.allRealDevices { + #if os(iOS) + let expected = model == .iPhoneDuo + #else + let expected = false + #endif + XCTAssertEqual(model.isFoldable, expected, model.description) + XCTAssertEqual(Device.simulator(model).isFoldable, expected, model.description) + } + XCTAssertFalse(Device.unknown("unrecognized").isFoldable) + XCTAssertFalse(Device.simulator(.unknown("unrecognized")).isFoldable) + } + + #if os(iOS) + func testProvisionalIPhoneDuo() { + let model = Device.iPhoneDuo + XCTAssertEqual(Device.mapToDevice(identifier: "iPhone19,4"), model) + XCTAssertEqual(Device.mapToDevice(identifier: "placeholder:iPhoneDuo"), .unknown("placeholder:iPhoneDuo")) + XCTAssertEqual(model.description, "iPhone Duo") + XCTAssertEqual(model.safeDescription, "iPhone Duo") + XCTAssertTrue(Device.allPhones.contains(model)) + XCTAssertTrue(Device.allRealDevices.contains(model)) + XCTAssertTrue(Device.allSimulators.contains(.simulator(model))) + for variant in [model, .simulator(model)] { + XCTAssertTrue(variant.isPhone) + XCTAssertFalse(variant.isPad) + XCTAssertTrue(variant.isFoldable) + XCTAssertEqual(variant.cpu, .a20Pro) + XCTAssertEqual(variant.diagonal, -1) + XCTAssertEqual(variant.screenRatio.width, -1) + XCTAssertEqual(variant.screenRatio.height, -1) + XCTAssertNil(variant.ppi) + } + } + #endif + #if os(iOS) || os(watchOS) func testSeptember2026Devices() { struct ExpectedDevice { @@ -32,8 +69,8 @@ class DeviceKitTests: XCTestCase { ] let chip = Device.CPU.a20Pro XCTAssertEqual(chip.description, "A20 Pro") - // The deferred model and unverified identifiers must remain unknown. - for identifier in ["iPhone19,1", "iPhone19,4", "iPhone19,5", "iPhone19,6", "placeholder:iPhone18Pro", "placeholder:iPhone18ProMax"] { + // Unverified identifiers must remain unknown. + for identifier in ["iPhone19,1", "iPhone19,5", "iPhone19,6", "placeholder:iPhone18Pro", "placeholder:iPhone18ProMax"] { XCTAssertEqual(Device.mapToDevice(identifier: identifier), .unknown(identifier)) } #else @@ -91,6 +128,36 @@ class DeviceKitTests: XCTestCase { #endif } + func testIsSimulatorForKnownAndUnknownDevices() { + for model in Device.allRealDevices + [.unknown("unrecognized")] { + XCTAssertFalse(model.isSimulator, model.description) + XCTAssertTrue(Device.simulator(model).isSimulator, model.description) + } + } + + #if os(iOS) && targetEnvironment(simulator) + func testCurrentSimulatorWithKnownAndUnknownModels() { + let originalModelIdentifier = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] + defer { + if let originalModelIdentifier = originalModelIdentifier { + setenv("SIMULATOR_MODEL_IDENTIFIER", originalModelIdentifier, 1) + } else { + unsetenv("SIMULATOR_MODEL_IDENTIFIER") + } + } + + let models: [(String, Device)] = [ + ("iPhone19,4", .iPhoneDuo), + ("iPhone999,1", .unknown("iPhone999,1")) + ] + for (identifier, model) in models { + setenv("SIMULATOR_MODEL_IDENTIFIER", identifier, 1) + XCTAssertEqual(Device.current, .simulator(model), identifier) + XCTAssertTrue(Device.current.isSimulator, identifier) + } + } + #endif + func testDeviceDescription() { #if os(macOS) #else