diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b9d269..b0ea99f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,15 +15,15 @@ concurrency: jobs: macos: - name: macOS 13 (Xcode 14.3.1) - runs-on: macos-13 + name: macOS 15 (Xcode 16.4) + runs-on: macos-15 strategy: matrix: config: ['debug', 'release'] steps: - uses: actions/checkout@v3 - - name: Select Xcode 14.3.1 - run: sudo xcode-select -s /Applications/Xcode_14.3.1.app + - name: Select Xcode 16.4 + run: sudo xcode-select -s /Applications/Xcode_16.4.app - name: Run tests run: make test-swift - name: Build platforms ${{ matrix.config }} diff --git a/Makefile b/Makefile index 9ad8a20..51eee90 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ CONFIG = debug -PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iPhone,iOS-16) +PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS,iPhone \d\+ Pro [^M]) PLATFORM_MACOS = macOS PLATFORM_MAC_CATALYST = macOS,variant=Mac Catalyst -PLATFORM_TVOS = tvOS Simulator,id=$(call udid_for,TV,tvOS-16) -PLATFORM_WATCHOS = watchOS Simulator,id=$(call udid_for,Watch,watchOS-9) +PLATFORM_TVOS = tvOS Simulator,id=$(call udid_for,tvOS,TV) +PLATFORM_WATCHOS = watchOS Simulator,id=$(call udid_for,watchOS,Watch) default: swift-test @@ -49,5 +49,5 @@ test-swift: .PHONY: test-example test-swift build-for-library-evolution format define udid_for -$(shell xcrun simctl list --json devices available $(1) | jq -r '.devices | to_entries | map(select(.value | add)) | sort_by(.key) | .[] | select(.key | contains("$(2)")) | .value | last.udid') +$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }') endef diff --git a/Package.swift b/Package.swift index 4458374..19f85f3 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/ComposableUserNotifications/Interface.swift b/Sources/ComposableUserNotifications/Interface.swift index 83ba624..ff6a544 100644 --- a/Sources/ComposableUserNotifications/Interface.swift +++ b/Sources/ComposableUserNotifications/Interface.swift @@ -1,4 +1,4 @@ -import UserNotifications +@preconcurrency import UserNotifications import XCTestDynamicOverlay /// A wrapper around UserNotifications's `UNUserNotificationCenter` that exposes its functionality through @@ -8,17 +8,20 @@ import XCTestDynamicOverlay @available(macOS 10.14, *) @available(tvOS 10.0, *) @available(watchOS 3.0, *) -public struct UserNotificationClient { +public struct UserNotificationClient: Sendable { /// Actions that correspond to `UNUserNotificationCenterDelegate` methods. /// /// See `UNUserNotificationCenterDelegate` for more information. - public enum DelegateAction { + public enum DelegateAction: Sendable { case willPresentNotification( _ notification: Notification, - completionHandler: (UNNotificationPresentationOptions) -> Void) + completionHandler: @Sendable (UNNotificationPresentationOptions) -> Void) @available(tvOS, unavailable) - case didReceiveResponse(_ response: Notification.Response, completionHandler: () -> Void) + case didReceiveResponse( + _ response: Notification.Response, + completionHandler: @Sendable () -> Void + ) case openSettingsForNotification(_ notification: Notification?) } @@ -32,41 +35,41 @@ public struct UserNotificationClient { #endif #if !os(tvOS) - public var notificationCategories: () async -> Set = unimplemented( + public var notificationCategories: @Sendable () async -> Set = unimplemented( "\(Self.self).deliveredNotifications") #endif - public var notificationSettings: () async -> Notification.Settings = unimplemented( + public var notificationSettings: @Sendable () async -> Notification.Settings = unimplemented( "\(Self.self).notificationSettings") - public var pendingNotificationRequests: () async -> [Notification.Request] = unimplemented( + public var pendingNotificationRequests: @Sendable () async -> [Notification.Request] = unimplemented( "\(Self.self).pendingNotificationRequests") #if !os(tvOS) - public var removeAllDeliveredNotifications: () async -> Void = unimplemented( + public var removeAllDeliveredNotifications: @Sendable () async -> Void = unimplemented( "\(Self.self).removeAllDeliveredNotifications") #endif - public var removeAllPendingNotificationRequests: () async -> Void = unimplemented( + public var removeAllPendingNotificationRequests: @Sendable () async -> Void = unimplemented( "\(Self.self).removeAllPendingNotificationRequests") #if !os(tvOS) - public var removeDeliveredNotificationsWithIdentifiers: ([String]) async -> Void = + public var removeDeliveredNotificationsWithIdentifiers: @Sendable ([String]) async -> Void = unimplemented("\(Self.self).removeDeliveredNotificationsWithIdentifiers") #endif - public var removePendingNotificationRequestsWithIdentifiers: ([String]) async -> Void = + public var removePendingNotificationRequestsWithIdentifiers: @Sendable ([String]) async -> Void = unimplemented("\(Self.self).removePendingNotificationRequestsWithIdentifiers") - public var requestAuthorization: (UNAuthorizationOptions) async throws -> Bool = + public var requestAuthorization: @Sendable (UNAuthorizationOptions) async throws -> Bool = unimplemented("\(Self.self).requestAuthorization") #if !os(tvOS) - public var setNotificationCategories: (Set) async -> Void = + public var setNotificationCategories: @Sendable (Set) async -> Void = unimplemented("\(Self.self).setNotificationCategories") #endif - public var supportsContentExtensions: () -> Bool = unimplemented( + public var supportsContentExtensions: @Sendable () -> Bool = unimplemented( "\(Self.self).supportsContentExtensions") /// This Effect represents calls to the `UNUserNotificationCenterDelegate`. diff --git a/Sources/ComposableUserNotifications/LiveKey.swift b/Sources/ComposableUserNotifications/LiveKey.swift index 505f2a8..0a4aaab 100644 --- a/Sources/ComposableUserNotifications/LiveKey.swift +++ b/Sources/ComposableUserNotifications/LiveKey.swift @@ -1,6 +1,6 @@ import Dependencies import Foundation -import UserNotifications +@preconcurrency import UserNotifications extension UserNotificationClient: DependencyKey { public static var liveValue: Self { @@ -70,7 +70,7 @@ extension UserNotificationClient: DependencyKey { AsyncStream { continuation in let delegate = Delegate(continuation: continuation) UNUserNotificationCenter.current().delegate = delegate - continuation.onTermination = { _ in + continuation.onTermination = { [delegate = UncheckedSendable(delegate)] _ in _ = delegate } } @@ -81,7 +81,7 @@ extension UserNotificationClient: DependencyKey { } extension UserNotificationClient { - fileprivate class Delegate: NSObject, UNUserNotificationCenterDelegate { + fileprivate final class Delegate: NSObject, UNUserNotificationCenterDelegate, Sendable { let continuation: AsyncStream.Continuation init(continuation: AsyncStream.Continuation) { @@ -97,7 +97,9 @@ extension UserNotificationClient { self.continuation.yield( .willPresentNotification( Notification(rawValue: notification), - completionHandler: completionHandler + completionHandler: { [handler = UncheckedSendable(completionHandler)] options in + handler.value(options) + } ) ) } @@ -110,7 +112,12 @@ extension UserNotificationClient { ) { let wrappedResponse = Notification.Response(rawValue: response) self.continuation.yield( - .didReceiveResponse(wrappedResponse) { completionHandler() } + .didReceiveResponse( + wrappedResponse, + completionHandler: { [handler = UncheckedSendable(completionHandler)] in + handler.value() + } + ) ) } #endif diff --git a/Sources/ComposableUserNotifications/Model.swift b/Sources/ComposableUserNotifications/Model.swift index 295e527..9f4d094 100644 --- a/Sources/ComposableUserNotifications/Model.swift +++ b/Sources/ComposableUserNotifications/Model.swift @@ -1,9 +1,10 @@ -import CoreLocation -import UserNotifications +import ConcurrencyExtras +@preconcurrency import CoreLocation +@preconcurrency import UserNotifications import XCTestDynamicOverlay -public struct Notification: Equatable { - public let rawValue: UNNotification? +public struct Notification: Equatable, Sendable { + nonisolated(unsafe) public let rawValue: UNNotification? public var date: Date public var request: Request @@ -28,11 +29,11 @@ public struct Notification: Equatable { } extension Notification { - public struct Request: Equatable { - public let rawValue: UNNotificationRequest? + public struct Request: Equatable, Sendable { + nonisolated(unsafe) public let rawValue: UNNotificationRequest? public var identifier: String - public var content: UNNotificationContent + nonisolated(unsafe) public var content: UNNotificationContent public var trigger: Trigger? public init(rawValue: UNNotificationRequest) { @@ -78,7 +79,7 @@ extension Notification { } extension Notification { - public enum Trigger: Equatable { + public enum Trigger: Equatable, Sendable { case push(Push) case timeInterval(TimeInterval) case calendar(Calendar) @@ -123,8 +124,8 @@ extension Notification.Trigger { } } - public struct Push: Equatable { - public var rawValue: UNPushNotificationTrigger? + public struct Push: Equatable, Sendable { + nonisolated(unsafe) public var rawValue: UNPushNotificationTrigger? public var repeats: Bool @@ -141,25 +142,27 @@ extension Notification.Trigger { } } - public struct TimeInterval: Equatable { - public let rawValue: UNTimeIntervalNotificationTrigger? + public struct TimeInterval: Equatable, Sendable { + nonisolated(unsafe) public let rawValue: UNTimeIntervalNotificationTrigger? public var repeats: Bool public var timeInterval: Foundation.TimeInterval - public var nextTriggerDate: () -> Date? + public var nextTriggerDate: @Sendable () -> Date? init(rawValue: UNTimeIntervalNotificationTrigger) { self.rawValue = rawValue self.repeats = rawValue.repeats self.timeInterval = rawValue.timeInterval - self.nextTriggerDate = rawValue.nextTriggerDate + self.nextTriggerDate = { [nextTriggerDate = UncheckedSendable(rawValue.nextTriggerDate)] in + nextTriggerDate.value() + } } public init( repeats: Bool, timeInterval: Foundation.TimeInterval, - nextTriggerDate: @escaping () -> Date? + nextTriggerDate: @Sendable @escaping () -> Date? ) { self.rawValue = nil @@ -173,25 +176,27 @@ extension Notification.Trigger { } } - public struct Calendar: Equatable { - public let rawValue: UNCalendarNotificationTrigger? + public struct Calendar: Equatable, Sendable { + nonisolated(unsafe) public let rawValue: UNCalendarNotificationTrigger? public var repeats: Bool public var dateComponents: DateComponents - public var nextTriggerDate: () -> Date? + public var nextTriggerDate: @Sendable () -> Date? public init(rawValue: UNCalendarNotificationTrigger) { self.rawValue = rawValue self.repeats = rawValue.repeats self.dateComponents = rawValue.dateComponents - self.nextTriggerDate = rawValue.nextTriggerDate + self.nextTriggerDate = { [nextTriggerDate = UncheckedSendable(rawValue.nextTriggerDate)] in + nextTriggerDate.value() + } } public init( repeats: Bool, dateComponents: DateComponents, - nextTriggerDate: @escaping () -> Date? + nextTriggerDate: @Sendable @escaping () -> Date? ) { self.rawValue = nil @@ -208,8 +213,8 @@ extension Notification.Trigger { @available(macOS, unavailable) @available(macCatalyst, unavailable) @available(tvOS, unavailable) - public struct Location: Equatable { - public let rawValue: UNLocationNotificationTrigger? + public struct Location: Equatable, Sendable { + nonisolated(unsafe) public let rawValue: UNLocationNotificationTrigger? public var repeats: Bool public var region: Region @@ -232,7 +237,7 @@ extension Notification.Trigger { extension Notification { @available(tvOS, unavailable) - public enum Response: Equatable { + public enum Response: Equatable, Sendable { case user(UserAction) case textInput(TextInputAction) } @@ -270,8 +275,8 @@ extension Notification.Response { } } - public struct UserAction: Equatable { - public let rawValue: UNNotificationResponse? + public struct UserAction: Equatable, Sendable { + nonisolated(unsafe) public let rawValue: UNNotificationResponse? public var actionIdentifier: String public var notification: Notification @@ -290,8 +295,8 @@ extension Notification.Response { } } - public struct TextInputAction: Equatable { - public let rawValue: UNTextInputNotificationResponse? + public struct TextInputAction: Equatable, Sendable { + nonisolated(unsafe) public let rawValue: UNTextInputNotificationResponse? public var actionIdentifier: String public var notification: Notification @@ -473,8 +478,8 @@ extension Notification { } // see https://github.com/pointfreeco/swift-composable-architecture/blob/767e1d9553fcee5a95af10e0352f20fb03b98352/Sources/ComposableCoreLocation/Models/Region.swift#L5 -public struct Region: Hashable { - public let rawValue: CLRegion? +public struct Region: Hashable, Sendable { + nonisolated(unsafe) public let rawValue: CLRegion? public var identifier: String public var notifyOnEntry: Bool public var notifyOnExit: Bool diff --git a/Tests/ComposableUserNotificationsTests/ComposableUserNotificationsTests.swift b/Tests/ComposableUserNotificationsTests/ComposableUserNotificationsTests.swift index 8b7947b..817eb43 100644 --- a/Tests/ComposableUserNotificationsTests/ComposableUserNotificationsTests.swift +++ b/Tests/ComposableUserNotificationsTests/ComposableUserNotificationsTests.swift @@ -9,8 +9,4 @@ final class ComposableUserNotificationsTests: XCTestCase { // results. XCTAssertEqual(true, true) } - - static var allTests = [ - ("testExample", testExample) - ] } diff --git a/Tests/ComposableUserNotificationsTests/XCTestManifests.swift b/Tests/ComposableUserNotificationsTests/XCTestManifests.swift deleted file mode 100644 index a89ad91..0000000 --- a/Tests/ComposableUserNotificationsTests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) - public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(ComposableUserNotificationsTests.allTests) - ] - } -#endif