Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
33 changes: 18 additions & 15 deletions Sources/ComposableUserNotifications/Interface.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import UserNotifications
@preconcurrency import UserNotifications
import XCTestDynamicOverlay

/// A wrapper around UserNotifications's `UNUserNotificationCenter` that exposes its functionality through
Expand All @@ -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?)
}
Expand All @@ -32,41 +35,41 @@ public struct UserNotificationClient {
#endif

#if !os(tvOS)
public var notificationCategories: () async -> Set<UNNotificationCategory> = unimplemented(
public var notificationCategories: @Sendable () async -> Set<UNNotificationCategory> = unimplemented(
"\(Self.self).deliveredNotifications")
Comment on lines +38 to 39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in unimplemented placeholder

Use the correct label to aid debugging.

-    public var notificationCategories: @Sendable () async -> Set<UNNotificationCategory> = unimplemented(
-      "\(Self.self).deliveredNotifications")
+    public var notificationCategories: @Sendable () async -> Set<UNNotificationCategory> = unimplemented(
+      "\(Self.self).notificationCategories")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public var notificationCategories: @Sendable () async -> Set<UNNotificationCategory> = unimplemented(
"\(Self.self).deliveredNotifications")
public var notificationCategories: @Sendable () async -> Set<UNNotificationCategory> = unimplemented(
"\(Self.self).notificationCategories")
🤖 Prompt for AI Agents
In Sources/ComposableUserNotifications/Interface.swift around lines 38–39, the
unimplemented placeholder message uses the wrong label
("deliveredNotifications"); update it to use the actual property name so the
failure message is accurate—replace the placeholder string with
"\(Self.self).notificationCategories" (or the exact property identifier) when
calling unimplemented.

#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<UNNotificationCategory>) async -> Void =
public var setNotificationCategories: @Sendable (Set<UNNotificationCategory>) 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`.
Expand Down
17 changes: 12 additions & 5 deletions Sources/ComposableUserNotifications/LiveKey.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Dependencies
import Foundation
import UserNotifications
@preconcurrency import UserNotifications

extension UserNotificationClient: DependencyKey {
public static var liveValue: Self {
Expand Down Expand Up @@ -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
}
}
Expand All @@ -81,7 +81,7 @@ extension UserNotificationClient: DependencyKey {
}

extension UserNotificationClient {
fileprivate class Delegate: NSObject, UNUserNotificationCenterDelegate {
fileprivate final class Delegate: NSObject, UNUserNotificationCenterDelegate, Sendable {
let continuation: AsyncStream<UserNotificationClient.DelegateAction>.Continuation

init(continuation: AsyncStream<UserNotificationClient.DelegateAction>.Continuation) {
Expand All @@ -97,7 +97,9 @@ extension UserNotificationClient {
self.continuation.yield(
.willPresentNotification(
Notification(rawValue: notification),
completionHandler: completionHandler
completionHandler: { [handler = UncheckedSendable(completionHandler)] options in
handler.value(options)
}
)
)
}
Expand All @@ -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
Expand Down
43 changes: 24 additions & 19 deletions Sources/ComposableUserNotifications/Model.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import CoreLocation
import UserNotifications
import ConcurrencyExtras
@preconcurrency import CoreLocation
@preconcurrency import UserNotifications
import XCTestDynamicOverlay

public struct Notification: Equatable {
public struct Notification: Equatable, Sendable {
public let rawValue: UNNotification?

public var date: Date
Expand All @@ -28,7 +29,7 @@ public struct Notification: Equatable {
}

extension Notification {
public struct Request: Equatable {
public struct Request: Equatable, Sendable {
public let rawValue: UNNotificationRequest?

public var identifier: String
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -123,7 +124,7 @@ extension Notification.Trigger {
}
}

public struct Push: Equatable {
public struct Push: Equatable, Sendable {
public var rawValue: UNPushNotificationTrigger?

public var repeats: Bool
Expand All @@ -141,25 +142,27 @@ extension Notification.Trigger {
}
}

public struct TimeInterval: Equatable {
public struct TimeInterval: Equatable, Sendable {
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

Expand All @@ -173,25 +176,27 @@ extension Notification.Trigger {
}
}

public struct Calendar: Equatable {
public struct Calendar: Equatable, Sendable {
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

Expand All @@ -208,7 +213,7 @@ extension Notification.Trigger {
@available(macOS, unavailable)
@available(macCatalyst, unavailable)
@available(tvOS, unavailable)
public struct Location: Equatable {
public struct Location: Equatable, Sendable {
public let rawValue: UNLocationNotificationTrigger?

public var repeats: Bool
Expand All @@ -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)
}
Expand Down Expand Up @@ -270,7 +275,7 @@ extension Notification.Response {
}
}

public struct UserAction: Equatable {
public struct UserAction: Equatable, Sendable {
public let rawValue: UNNotificationResponse?

public var actionIdentifier: String
Expand All @@ -290,7 +295,7 @@ extension Notification.Response {
}
}

public struct TextInputAction: Equatable {
public struct TextInputAction: Equatable, Sendable {
public let rawValue: UNTextInputNotificationResponse?

public var actionIdentifier: String
Expand Down Expand Up @@ -473,7 +478,7 @@ extension Notification {
}

// see https://github.com/pointfreeco/swift-composable-architecture/blob/767e1d9553fcee5a95af10e0352f20fb03b98352/Sources/ComposableCoreLocation/Models/Region.swift#L5
public struct Region: Hashable {
public struct Region: Hashable, Sendable {
public let rawValue: CLRegion?
public var identifier: String
public var notifyOnEntry: Bool
Expand Down
Loading