diff --git a/CHANGELOG.md b/CHANGELOG.md index 03602d0c..a57f216d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,22 @@ ## Unreleased +### New features + +- Add Swift 6 and strict concurrency support, including `Sendable` conformances for `Device` and its supporting types. ([#437](https://github.com/devicekit/DeviceKit/pull/437)) + +### Breaking changes + +- Require main-actor access for UIKit-backed device properties, including `isPhone`, `isPad`, screen information, battery information, and orientation, and make `Device.identifier` read-only. The Xcode project now requires Swift 6, and the Xcode and Swift Package Manager watchOS deployment targets increase to watchOS 6.0. ([#437](https://github.com/devicekit/DeviceKit/pull/437)) + ### 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)) +### Contributors + +Thanks to [SomeRandomiOSDev](https://github.com/SomeRandomiOSDev) for contributing Swift 6 and strict concurrency support in [#437](https://github.com/devicekit/DeviceKit/pull/437)! + ## Version 5.8.0 Releasedate: 2026-03-03 diff --git a/DeviceKit.xcodeproj/project.pbxproj b/DeviceKit.xcodeproj/project.pbxproj index ca9c0053..2ed01222 100644 --- a/DeviceKit.xcodeproj/project.pbxproj +++ b/DeviceKit.xcodeproj/project.pbxproj @@ -396,12 +396,12 @@ SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,3,4,7"; TVOS_DEPLOYMENT_TARGET = 13.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 4.0; + WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Debug; }; @@ -454,13 +454,13 @@ SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,3,4,7"; TVOS_DEPLOYMENT_TARGET = 13.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 4.0; + WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Release; }; @@ -506,7 +506,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TVOS_DEPLOYMENT_TARGET = 13.0; }; name = Debug; @@ -548,7 +548,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TVOS_DEPLOYMENT_TARGET = 13.0; VALIDATE_PRODUCT = YES; }; diff --git a/Package.swift b/Package.swift index e796c1cf..ebd01941 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:6.0 // ===----------------------------------------------------------------------=== // // // This source file is part of the DeviceKit open source project @@ -17,7 +17,7 @@ let package = Package( platforms: [ .iOS(.v13), .tvOS(.v13), - .watchOS(.v4) + .watchOS(.v6) ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. @@ -44,5 +44,5 @@ let package = Package( resources: [.process("../Source/PrivacyInfo.xcprivacy")] ) ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v5, .v6] ) diff --git a/Package@swift-5.6.swift b/Package@swift-5.6.swift new file mode 100644 index 00000000..b0adaa20 --- /dev/null +++ b/Package@swift-5.6.swift @@ -0,0 +1,52 @@ +// swift-tools-version:5.6 +//===----------------------------------------------------------------------===// +// +// This source file is part of the DeviceKit open source project +// +// Copyright © 2014 - 2018 Dennis Weissmann and the DeviceKit project authors +// +// License: https://github.com/dennisweissmann/DeviceKit/blob/master/LICENSE +// Contributors: https://github.com/dennisweissmann/DeviceKit#contributors +// +//===----------------------------------------------------------------------===// + +import PackageDescription + +let package = Package( + name: "DeviceKit", + platforms: [ + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6) + ], + products: [ + // Products define the executables and libraries produced by a package, and make them visible to other packages. + .library( + name: "DeviceKit", + targets: ["DeviceKit"] + ) + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages which this package depends on. + .target( + name: "DeviceKit", + dependencies: [], + path: "Source", + resources: [.process("PrivacyInfo.xcprivacy")], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ] + ), + .testTarget( + name: "DeviceKitTests", + dependencies: ["DeviceKit"], + path: "Tests", + resources: [.process("../Source/PrivacyInfo.xcprivacy")], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ] + ) + ], + swiftLanguageVersions: [.v5] +) diff --git a/README.md b/README.md index 10abba0f..53f61abb 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ See our detailed [changelog](CHANGELOG.md) for the latest features, improvements ## Requirements -- iOS 11.0+ -- tvOS 11.0+ -- watchOS 4.0+ +- iOS 13.0+ +- tvOS 13.0+ +- watchOS 6.0+ ## Installation DeviceKit can be installed in various ways. diff --git a/Source/Device.generated.swift b/Source/Device.generated.swift index bd5a0638..0d33a2f5 100644 --- a/Source/Device.generated.swift +++ b/Source/Device.generated.swift @@ -48,7 +48,7 @@ import Foundation /// showError() /// } /// -public enum Device { +public enum Device: Sendable { #if os(iOS) /// Device is an [iPod touch (5th generation)](https://support.apple.com/kb/SP657) /// @@ -606,7 +606,7 @@ public enum Device { } /// Gets the identifier from the system, such as "iPhone7,1". - public static var identifier: String = { + public static let identifier: String = { var systemInfo = utsname() uname(&systemInfo) let mirror = Mirror(reflecting: systemInfo.machine) @@ -1171,6 +1171,7 @@ public enum Device { } /// Returns whether the device is an iPhone (real or simulator) + @MainActor public var isPhone: Bool { return (isOneOf(Device.allPhones) || isOneOf(Device.allSimulatorPhones) @@ -1178,6 +1179,7 @@ public enum Device { } /// Returns whether the device is an iPad (real or simulator) + @MainActor public var isPad: Bool { return isOneOf(Device.allPads) || isOneOf(Device.allSimulatorPads) @@ -1190,6 +1192,7 @@ public enum Device { return Device.realDevice(from: self) } + @MainActor public var isZoomed: Bool? { guard isCurrent else { return nil } if Int(UIScreen.main.scale.rounded()) == 3 { @@ -1426,6 +1429,9 @@ public enum Device { /// The name identifying the device (e.g. "Dennis' iPhone"). /// As of iOS 16, this will return a generic String like "iPhone", unless your app has additional entitlements. /// See the follwing link for more information: https://developer.apple.com/documentation/uikit/uidevice/1620015-name + #if !os(watchOS) && canImport(UIKit) + @MainActor + #endif public var name: String? { guard isCurrent else { return nil } #if os(watchOS) @@ -1438,6 +1444,9 @@ public enum Device { } /// The name of the operating system running on the device represented by the receiver (e.g. "iOS" or "tvOS"). + #if !os(watchOS) && (os(iOS) || canImport(UIKit)) + @MainActor + #endif public var systemName: String? { guard isCurrent else { return nil } #if os(watchOS) @@ -1456,6 +1465,9 @@ public enum Device { } /// The current version of the operating system (e.g. 8.4 or 9.2). + #if !os(watchOS) && canImport(UIKit) + @MainActor + #endif public var systemVersion: String? { guard isCurrent else { return nil } #if os(watchOS) @@ -1468,6 +1480,9 @@ public enum Device { } /// The model of the device (e.g. "iPhone" or "iPod Touch"). + #if !os(watchOS) && canImport(UIKit) + @MainActor + #endif public var model: String? { guard isCurrent else { return nil } #if os(watchOS) @@ -1480,6 +1495,9 @@ public enum Device { } /// The model of the device as a localized string. + #if !os(watchOS) && canImport(UIKit) + @MainActor + #endif public var localizedModel: String? { guard isCurrent else { return nil } #if os(watchOS) @@ -1644,6 +1662,9 @@ public enum Device { } /// True when a Guided Access session is currently active; otherwise, false. + #if os(iOS) + @MainActor + #endif public var isGuidedAccessSessionActive: Bool { #if os(iOS) #if swift(>=4.2) @@ -1657,6 +1678,9 @@ public enum Device { } /// The brightness level of the screen. + #if os(iOS) + @MainActor + #endif public var screenBrightness: Int { #if os(iOS) return Int(UIScreen.main.brightness * 100) @@ -2024,7 +2048,7 @@ extension Device { - Charging: The device is plugged into power and the battery is less than 100% charged. - Unplugged: The device is not plugged into power; the battery is discharging. */ - public enum BatteryState: CustomStringConvertible, Equatable { + public enum BatteryState: CustomStringConvertible, Equatable, Sendable { /// The device is plugged into power and the battery is 100% charged or the device is the iOS Simulator. case full /// The device is plugged into power and the battery is less than 100% charged. @@ -2035,6 +2059,7 @@ extension Device { case unplugged(Int) #if os(iOS) + @MainActor fileprivate init() { let wasBatteryMonitoringEnabled = UIDevice.current.isBatteryMonitoringEnabled UIDevice.current.isBatteryMonitoringEnabled = true @@ -2091,12 +2116,18 @@ extension Device { } /// The state of the battery + #if os(iOS) + @MainActor + #endif public var batteryState: BatteryState? { guard isCurrent else { return nil } return BatteryState() } /// Battery level ranges from 0 (fully discharged) to 100 (100% charged). + #if os(iOS) + @MainActor + #endif public var batteryLevel: Int? { guard isCurrent else { return nil } switch BatteryState() { @@ -2152,16 +2183,18 @@ extension Device { - Portrait: The device is in Portrait Orientation - Unknown: The device orientation is unknown. */ - public enum Orientation { + public enum Orientation: Sendable { case landscape case portrait case unknown } + @MainActor public var orientation: Orientation { - if UIDevice.current.orientation.isLandscape { + let orientation = UIDevice.current.orientation + if orientation.isLandscape { return .landscape - } else if UIDevice.current.orientation.isPortrait { + } else if orientation.isPortrait { return .portrait } else { return .unknown @@ -2237,7 +2270,7 @@ extension Device { - firstGenerationUsbC: 1st Generation Apple Pencil (USB-C) - pro: Apple Pencil Pro */ - public struct ApplePencilSupport: OptionSet { + public struct ApplePencilSupport: OptionSet, Sendable { public var rawValue: UInt @@ -2304,7 +2337,7 @@ extension Device { // MARK: Cameras extension Device { - public enum CameraType { + public enum CameraType: Sendable { @available(*, deprecated, renamed: "wide") case normal @@ -2507,7 +2540,7 @@ extension Device { extension Device { - public enum CPU: Comparable { + public enum CPU: Comparable, Sendable { #if os(iOS) || os(tvOS) case a4 case a5 diff --git a/Source/Device.swift.gyb b/Source/Device.swift.gyb index 7a61e94a..cee9c829 100644 --- a/Source/Device.swift.gyb +++ b/Source/Device.swift.gyb @@ -643,7 +643,7 @@ import Foundation /// showError() /// } /// -public enum Device { +public enum Device: Sendable { #if os(iOS) % for device in iOSDevices: /// ${device.comment} @@ -683,7 +683,7 @@ public enum Device { } /// Gets the identifier from the system, such as "iPhone7,1". - public static var identifier: String = { + public static let identifier: String = { var systemInfo = utsname() uname(&systemInfo) let mirror = Mirror(reflecting: systemInfo.machine) @@ -875,6 +875,7 @@ public enum Device { } /// Returns whether the device is an iPhone (real or simulator) + @MainActor public var isPhone: Bool { return (isOneOf(Device.allPhones) || isOneOf(Device.allSimulatorPhones) @@ -882,6 +883,7 @@ public enum Device { } /// Returns whether the device is an iPad (real or simulator) + @MainActor public var isPad: Bool { return isOneOf(Device.allPads) || isOneOf(Device.allSimulatorPads) @@ -894,6 +896,7 @@ public enum Device { return Device.realDevice(from: self) } + @MainActor public var isZoomed: Bool? { guard isCurrent else { return nil } if Int(UIScreen.main.scale.rounded()) == 3 { @@ -1130,6 +1133,9 @@ public enum Device { /// The name identifying the device (e.g. "Dennis' iPhone"). /// As of iOS 16, this will return a generic String like "iPhone", unless your app has additional entitlements. /// See the follwing link for more information: https://developer.apple.com/documentation/uikit/uidevice/1620015-name + #if !os(watchOS) && canImport(UIKit) + @MainActor + #endif public var name: String? { guard isCurrent else { return nil } #if os(watchOS) @@ -1142,6 +1148,9 @@ public enum Device { } /// The name of the operating system running on the device represented by the receiver (e.g. "iOS" or "tvOS"). + #if !os(watchOS) && (os(iOS) || canImport(UIKit)) + @MainActor + #endif public var systemName: String? { guard isCurrent else { return nil } #if os(watchOS) @@ -1160,6 +1169,9 @@ public enum Device { } /// The current version of the operating system (e.g. 8.4 or 9.2). + #if !os(watchOS) && canImport(UIKit) + @MainActor + #endif public var systemVersion: String? { guard isCurrent else { return nil } #if os(watchOS) @@ -1172,6 +1184,9 @@ public enum Device { } /// The model of the device (e.g. "iPhone" or "iPod Touch"). + #if !os(watchOS) && canImport(UIKit) + @MainActor + #endif public var model: String? { guard isCurrent else { return nil } #if os(watchOS) @@ -1184,6 +1199,9 @@ public enum Device { } /// The model of the device as a localized string. + #if !os(watchOS) && canImport(UIKit) + @MainActor + #endif public var localizedModel: String? { guard isCurrent else { return nil } #if os(watchOS) @@ -1224,6 +1242,9 @@ public enum Device { } /// True when a Guided Access session is currently active; otherwise, false. + #if os(iOS) + @MainActor + #endif public var isGuidedAccessSessionActive: Bool { #if os(iOS) #if swift(>=4.2) @@ -1237,6 +1258,9 @@ public enum Device { } /// The brightness level of the screen. + #if os(iOS) + @MainActor + #endif public var screenBrightness: Int { #if os(iOS) return Int(UIScreen.main.brightness * 100) @@ -1354,7 +1378,7 @@ extension Device { - Charging: The device is plugged into power and the battery is less than 100% charged. - Unplugged: The device is not plugged into power; the battery is discharging. */ - public enum BatteryState: CustomStringConvertible, Equatable { + public enum BatteryState: CustomStringConvertible, Equatable, Sendable { /// The device is plugged into power and the battery is 100% charged or the device is the iOS Simulator. case full /// The device is plugged into power and the battery is less than 100% charged. @@ -1365,6 +1389,7 @@ extension Device { case unplugged(Int) #if os(iOS) + @MainActor fileprivate init() { let wasBatteryMonitoringEnabled = UIDevice.current.isBatteryMonitoringEnabled UIDevice.current.isBatteryMonitoringEnabled = true @@ -1421,12 +1446,18 @@ extension Device { } /// The state of the battery + #if os(iOS) + @MainActor + #endif public var batteryState: BatteryState? { guard isCurrent else { return nil } return BatteryState() } /// Battery level ranges from 0 (fully discharged) to 100 (100% charged). + #if os(iOS) + @MainActor + #endif public var batteryLevel: Int? { guard isCurrent else { return nil } switch BatteryState() { @@ -1482,16 +1513,18 @@ extension Device { - Portrait: The device is in Portrait Orientation - Unknown: The device orientation is unknown. */ - public enum Orientation { + public enum Orientation: Sendable { case landscape case portrait case unknown } + @MainActor public var orientation: Orientation { - if UIDevice.current.orientation.isLandscape { + let orientation = UIDevice.current.orientation + if orientation.isLandscape { return .landscape - } else if UIDevice.current.orientation.isPortrait { + } else if orientation.isPortrait { return .portrait } else { return .unknown @@ -1567,7 +1600,7 @@ extension Device { - firstGenerationUsbC: 1st Generation Apple Pencil (USB-C) - pro: Apple Pencil Pro */ - public struct ApplePencilSupport: OptionSet { + public struct ApplePencilSupport: OptionSet, Sendable { public var rawValue: UInt @@ -1627,7 +1660,7 @@ extension Device { // MARK: Cameras extension Device { - public enum CameraType { + public enum CameraType: Sendable { @available(*, deprecated, renamed: "wide") case normal @@ -1809,7 +1842,7 @@ watchOS_cpus = [ extension Device { - public enum CPU: Comparable { + public enum CPU: Comparable, Sendable { #if os(iOS) || os(tvOS) % for cpu in iOS_cpus: case ${cpu.name} diff --git a/Tests/Tests.swift b/Tests/Tests.swift index 1e87af88..585f1fa3 100644 --- a/Tests/Tests.swift +++ b/Tests/Tests.swift @@ -116,6 +116,7 @@ class DeviceKitTests: XCTestCase { // MARK: - iOS #if os(iOS) + @MainActor func testIsPhoneIsPadIsPod() { // Test for https://github.com/devicekit/DeviceKit/issues/165 to prevent it from happening in the future. @@ -151,6 +152,7 @@ class DeviceKitTests: XCTestCase { } } + @MainActor func testSystemName() { if UIDevice.current.userInterfaceIdiom == .pad { XCTAssertEqual(device.systemName, "iPadOS") @@ -396,6 +398,7 @@ class DeviceKitTests: XCTestCase { } } + @MainActor func testIsPad() { Device.allPads.forEach { XCTAssertTrue($0.isPad) } } @@ -513,11 +516,13 @@ class DeviceKitTests: XCTestCase { ]) } + @MainActor func testGuidedAccessSession() { XCTAssertFalse(Device.current.isGuidedAccessSessionActive) } // enable once unit tests can be run on device + @MainActor func testKeepsBatteryMonitoringState() { UIDevice.current.isBatteryMonitoringEnabled = true XCTAssertTrue(UIDevice.current.isBatteryMonitoringEnabled)