From 14134d72888447c83e8cbe560220adad8b933439 Mon Sep 17 00:00:00 2001 From: Joe Newton Date: Wed, 12 Mar 2025 10:36:36 -0400 Subject: [PATCH 1/2] Added support for Strict Concurrency --- .gitignore | 1 + DeviceKit.xcodeproj/project.pbxproj | 12 +++---- Package.swift | 6 ++-- Package@swift-5.6.swift | 52 ++++++++++++++++++++++++++++ README.md | 6 ++-- Source/Device.generated.swift | 53 +++++++++++++++++++++++------ Source/Device.swift.gyb | 53 +++++++++++++++++++++++------ Tests/Tests.swift | 5 +++ 8 files changed, 156 insertions(+), 32 deletions(-) create mode 100644 Package@swift-5.6.swift diff --git a/.gitignore b/.gitignore index 5ea1e9b3..f96d4e4a 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ vendor ## Generated Files # *.generated.swift # we have to check it in because of CocoaPods ... +Utils/__pycache__ diff --git a/DeviceKit.xcodeproj/project.pbxproj b/DeviceKit.xcodeproj/project.pbxproj index 00a538bc..5262f525 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 55e14ab9..564c67c1 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. @@ -42,5 +42,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 0e8cf26c..514a2d7e 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 ba44ea70..5617df4d 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) /// @@ -550,7 +550,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) @@ -1073,6 +1073,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) @@ -1080,6 +1081,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) @@ -1092,6 +1094,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 { @@ -1328,6 +1331,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) @@ -1340,6 +1346,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) @@ -1358,6 +1367,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) @@ -1370,6 +1382,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) @@ -1382,6 +1397,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) @@ -1532,6 +1550,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) @@ -1545,6 +1566,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) @@ -1884,7 +1908,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. @@ -1895,6 +1919,7 @@ extension Device { case unplugged(Int) #if os(iOS) + @MainActor fileprivate init() { let wasBatteryMonitoringEnabled = UIDevice.current.isBatteryMonitoringEnabled UIDevice.current.isBatteryMonitoringEnabled = true @@ -1951,12 +1976,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() { @@ -2012,16 +2043,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 @@ -2097,7 +2130,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 @@ -2160,7 +2193,7 @@ extension Device { // MARK: Cameras extension Device { - public enum CameraType { + public enum CameraType: Sendable { @available(*, deprecated, renamed: "wide") case normal @@ -2317,7 +2350,7 @@ extension Device { } #endif -// MARK: ThermalState +// MARK: ThermalState: Sendable @available(iOS 11.0, watchOS 4.0, macOS 10.10.3, tvOS 11.0, *) extension Device { /// The thermal state of the system. @@ -2354,7 +2387,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 3a7ee0ba..9f8c190b 100644 --- a/Source/Device.swift.gyb +++ b/Source/Device.swift.gyb @@ -362,7 +362,7 @@ import Foundation /// showError() /// } /// -public enum Device { +public enum Device: Sendable { #if os(iOS) % for device in iOSDevices: /// ${device.comment} @@ -402,7 +402,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) @@ -594,6 +594,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) @@ -601,6 +602,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) @@ -613,6 +615,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 { @@ -849,6 +852,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) @@ -861,6 +867,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) @@ -879,6 +888,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) @@ -891,6 +903,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) @@ -903,6 +918,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) @@ -943,6 +961,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) @@ -956,6 +977,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) @@ -1073,7 +1097,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. @@ -1084,6 +1108,7 @@ extension Device { case unplugged(Int) #if os(iOS) + @MainActor fileprivate init() { let wasBatteryMonitoringEnabled = UIDevice.current.isBatteryMonitoringEnabled UIDevice.current.isBatteryMonitoringEnabled = true @@ -1140,12 +1165,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() { @@ -1201,16 +1232,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 @@ -1286,7 +1319,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 @@ -1340,7 +1373,7 @@ extension Device { // MARK: Cameras extension Device { - public enum CameraType { + public enum CameraType: Sendable { @available(*, deprecated, renamed: "wide") case normal @@ -1431,7 +1464,7 @@ extension Device { } #endif -// MARK: ThermalState +// MARK: ThermalState: Sendable @available(iOS 11.0, watchOS 4.0, macOS 10.10.3, tvOS 11.0, *) extension Device { /// The thermal state of the system. @@ -1519,7 +1552,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 7c6af499..f16d735e 100644 --- a/Tests/Tests.swift +++ b/Tests/Tests.swift @@ -107,6 +107,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. @@ -142,6 +143,7 @@ class DeviceKitTests: XCTestCase { } } + @MainActor func testSystemName() { if UIDevice.current.userInterfaceIdiom == .pad { XCTAssertEqual(device.systemName, "iPadOS") @@ -382,6 +384,7 @@ class DeviceKitTests: XCTestCase { } } + @MainActor func testIsPad() { Device.allPads.forEach { XCTAssertTrue($0.isPad) } } @@ -493,11 +496,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) From bbfd0f8cc729ae82f8efd259e94beaa6a4c656d8 Mon Sep 17 00:00:00 2001 From: Zandor Smith Date: Thu, 20 Mar 2025 08:46:33 +0100 Subject: [PATCH 2/2] Remove unnecessary change to mark line. --- Source/Device.generated.swift | 2 +- Source/Device.swift.gyb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Device.generated.swift b/Source/Device.generated.swift index 5617df4d..7c8157ba 100644 --- a/Source/Device.generated.swift +++ b/Source/Device.generated.swift @@ -2350,7 +2350,7 @@ extension Device { } #endif -// MARK: ThermalState: Sendable +// MARK: ThermalState @available(iOS 11.0, watchOS 4.0, macOS 10.10.3, tvOS 11.0, *) extension Device { /// The thermal state of the system. diff --git a/Source/Device.swift.gyb b/Source/Device.swift.gyb index 9f8c190b..d21236c7 100644 --- a/Source/Device.swift.gyb +++ b/Source/Device.swift.gyb @@ -1464,7 +1464,7 @@ extension Device { } #endif -// MARK: ThermalState: Sendable +// MARK: ThermalState @available(iOS 11.0, watchOS 4.0, macOS 10.10.3, tvOS 11.0, *) extension Device { /// The thermal state of the system.