Skip to content

Conversation

@Miiha
Copy link
Owner

@Miiha Miiha commented Oct 20, 2025

Summary by CodeRabbit

  • Chores
    • Updated Swift toolchain requirement to version 6.0
    • Upgraded continuous integration to macOS 15 with Xcode 16.4
    • Improved simulator testing with more flexible device matching patterns
    • Enhanced notification API with thread-safety and concurrency attributes

@Miiha Miiha requested a review from Copilot October 20, 2025 12:03
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR updates the ComposableUserNotifications package to handle Swift 6 concurrency warnings by adding Sendable conformance to types and using @sendable annotations for closures.

  • Added @preconcurrency imports to suppress warnings from legacy APIs
  • Made all public types conform to Sendable protocol
  • Wrapped non-Sendable closures with UncheckedSendable to maintain thread safety

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
Package.swift Updated Swift tools version from 5.6 to 6.0
Sources/ComposableUserNotifications/Model.swift Added Sendable conformance and wrapped closures with UncheckedSendable
Sources/ComposableUserNotifications/Interface.swift Added Sendable conformance and @sendable annotations to function types
Sources/ComposableUserNotifications/LiveKey.swift Made Delegate class final and Sendable, wrapped closures with UncheckedSendable

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@Miiha Miiha requested a review from Copilot October 20, 2025 14:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.


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) }')
Copy link

Copilot AI Oct 20, 2025

Choose a reason for hiding this comment

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

The new shell command is complex and fragile. The awk field extraction $$(NF-3) assumes a specific format that may break if simulator output changes. Consider adding error handling or using a more robust parsing approach.

Suggested change
$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }')
$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | grep -Eo '[0-9A-Fa-f-]{36}' | head -1)

Copilot uses AI. Check for mistakes.
@coderabbitai
Copy link

coderabbitai bot commented Oct 27, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

The PR upgrades the Swift package to Swift 6.0 tooling, updates CI to macOS 15 with Xcode 16.4, modernizes build device matching in the Makefile, and adds comprehensive Sendable conformance and concurrency attributes across the public API surface. Test discovery manifests for non-Objective-C environments are removed.

Changes

Cohort / File(s) Change Summary
CI & Build Infrastructure
.github/workflows/ci.yml, Makefile, Package.swift
Updates GitHub Actions workflow to macOS 15 with Xcode 16.4; replaces hard-coded device identifiers with regex patterns and changes UDID selection from JSON/jq parsing to grep/sort/head/awk; bumps Swift tools version to 6.0
Concurrency & Sendable Conformance
Sources/ComposableUserNotifications/Interface.swift, Sources/ComposableUserNotifications/LiveKey.swift, Sources/ComposableUserNotifications/Model.swift
Adds @preconcurrency imports, Sendable conformance to public types and enums, @sendable closure annotations on all public closure-typed properties, and wraps completion handlers with UncheckedSendable; marks internal delegate as final Sendable
Test Discovery Removal
Tests/ComposableUserNotificationsTests/ComposableUserNotificationsTests.swift, Tests/ComposableUserNotificationsTests/XCTestManifests.swift
Removes allTests static declaration and Linux XCTest manifest entry function for automatic test discovery

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant UserNotificationClient
    participant Delegate
    participant UNUserNotificationCenter

    Note over Client,UNUserNotificationCenter: With Sendable Conformance
    Client->>UserNotificationClient: Call @Sendable closure property
    activate UserNotificationClient
    UserNotificationClient->>Delegate: Route through AsyncStream (UncheckedSendable)
    activate Delegate
    Delegate->>UNUserNotificationCenter: Invoke UserNotifications API
    activate UNUserNotificationCenter
    UNUserNotificationCenter-->>Delegate: Response
    deactivate UNUserNotificationCenter
    rect rgba(200, 150, 100, 0.1)
        Delegate->>Delegate: Wrap completion handler in @Sendable
    end
    Delegate-->>UserNotificationClient: Yield result
    deactivate Delegate
    UserNotificationClient-->>Client: Return async value
    deactivate UserNotificationClient
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Model.swift: Dense concurrency refactoring with Sendable conformance across 10+ types; nonisolated(unsafe) rawValue properties and wrapped @sendable closures require careful verification of thread-safety assumptions
  • Interface.swift: Extensive public API surface changes with @sendable attributes on all closure-typed properties; @preconcurrency imports need validation
  • LiveKey.swift: UncheckedSendable wrapping of completion handlers and delegate lifecycle management; verify AsyncStream termination closure correctness
  • Makefile: Regex-based device pattern changes and UDID selection logic shift from JSON to grep/awk pipeline; test against various simulator configurations

Poem

🐰 Swift six arrives with Sendable grace,
Closures don thread-safe embrace,
No more allTests, tests auto-find,
macOS 15, Xcode—we're redesigned!
Concurrency crowned, our notifs now race,
Safe in the cloud, at any pace ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Handle and work around concurrency warnings" accurately describes the primary focus of this changeset. The core substantive changes—across Interface.swift, LiveKey.swift, and Model.swift—center on adding concurrency safety through Sendable conformance, @sendable closure attributes, @preconcurrency imports, and UncheckedSendable workarounds. While the PR also includes supporting changes (CI workflow update to macOS 15/Xcode 16.4, Makefile device identifier updates, Swift tools version bump to 6.0, and test infrastructure cleanup), these align with or enable the concurrency work. The title is specific enough that a teammate reviewing history would clearly understand this PR addresses concurrency warnings and safety concerns in the composable-user-notifications library.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch task/concurrency-warnings

Comment @coderabbitai help to get the list of available commands and usage tips.

@Miiha
Copy link
Owner Author

Miiha commented Oct 27, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Oct 27, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
Sources/ComposableUserNotifications/Model.swift (5)

26-28: Equatable is incorrect when rawValue is nil; compare struct fields as fallback

Two Notifications built via init(date:request:) both have nil rawValue and currently compare equal, regardless of contents.

Apply:

-  public static func == (lhs: Notification, rhs: Notification) -> Bool {
-    lhs.rawValue == rhs.rawValue
-  }
+  public static func == (lhs: Notification, rhs: Notification) -> Bool {
+    if let l = lhs.rawValue, let r = rhs.rawValue { return l === r }
+    return lhs.date == rhs.date && lhs.request == rhs.request
+  }

75-77: Same Equatable pitfall for Request; add identity-or-structural comparison

Avoid “nil rawValue == nil rawValue” collapsing distinct values.

Apply:

-    public static func == (lhs: Request, rhs: Request) -> Bool {
-      lhs.rawValue == rhs.rawValue
-    }
+    public static func == (lhs: Request, rhs: Request) -> Bool {
+      if let l = lhs.rawValue, let r = rhs.rawValue { return l === r }
+      return lhs.identifier == rhs.identifier
+        && lhs.content === rhs.content
+        && lhs.trigger == rhs.trigger
+    }

127-143: Push.rawValue is never set; == collapses all values to equal

The initializer doesn’t assign rawValue, and equality checks only rawValue. This makes every Push equal.

Apply:

   public struct Push: Equatable, Sendable {
-    nonisolated(unsafe) public var rawValue: UNPushNotificationTrigger?
+    nonisolated(unsafe) public var rawValue: UNPushNotificationTrigger?
@@
-    public init(rawValue: UNPushNotificationTrigger) {
-      self.repeats = rawValue.repeats
-    }
+    public init(rawValue: UNPushNotificationTrigger) {
+      self.rawValue = rawValue
+      self.repeats = rawValue.repeats
+    }
@@
-    public init(repeats: Bool) {
-      self.repeats = repeats
-    }
+    public init(repeats: Bool) {
+      self.rawValue = nil
+      self.repeats = repeats
+    }
@@
-    public static func == (lhs: Push, rhs: Push) -> Bool {
-      lhs.rawValue == rhs.rawValue
-    }
+    public static func == (lhs: Push, rhs: Push) -> Bool {
+      if let l = lhs.rawValue, let r = rhs.rawValue { return l === r }
+      return lhs.repeats == rhs.repeats
+    }

174-176: TimeInterval equality should also fall back to structural fields

Raw-only equality yields false positives when either side was constructed without rawValue.

Apply:

-    public static func == (lhs: TimeInterval, rhs: TimeInterval) -> Bool {
-      lhs.rawValue == rhs.rawValue
-    }
+    public static func == (lhs: TimeInterval, rhs: TimeInterval) -> Bool {
+      if let l = lhs.rawValue, let r = rhs.rawValue { return l === r }
+      return lhs.repeats == rhs.repeats
+        && lhs.timeInterval == rhs.timeInterval
+    }

208-210: Calendar equality: add structural fallback

Same rationale as TimeInterval/Request.

Apply:

-    public static func == (lhs: Calendar, rhs: Calendar) -> Bool {
-      lhs.rawValue == rhs.rawValue
-    }
+    public static func == (lhs: Calendar, rhs: Calendar) -> Bool {
+      if let l = lhs.rawValue, let r = rhs.rawValue { return l === r }
+      return lhs.repeats == rhs.repeats
+        && lhs.dateComponents == rhs.dateComponents
+    }
♻️ Duplicate comments (1)
Makefile (1)

51-53: Make UDID parsing resilient; avoid brittle awk field offsets

Use a UUID regex and extended grep. This also addresses the earlier fragility concern.

Apply:

-$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }')
+$(shell xcrun simctl list devices available '$(1)' \
+  | grep -E "$(2)" \
+  | head -1 \
+  | grep -Eo '[0-9A-Fa-f-]{36}' \
+  | head -1)

Optional: if jq is acceptable, the JSON path is even safer:

# $(shell xcrun simctl list devices --json available '$(1)' | jq -r '.devices[][].udid' | head -1)
🧹 Nitpick comments (3)
.github/workflows/ci.yml (1)

25-26: Prefer setup-xcode over hard-coded xcode-select path

Hard-coding /Applications/Xcode_16.4.app is brittle across runner images. Use a maintained action to select Xcode by version.

Example:

-      - name: Select Xcode 16.4
-        run: sudo xcode-select -s /Applications/Xcode_16.4.app
+      - name: Select Xcode 16.4
+        uses: maxim-lobanov/setup-xcode@v1
+        with:
+          xcode-version: '16.4'

Optionally add SPM cache to speed builds:

- name: Cache SPM
  uses: actions/cache@v4
  with:
    path: |
      ~/.swiftpm
      .build
    key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
    restore-keys: |
      ${{ runner.os }}-spm-
Makefile (1)

5-6: TV/watch filters are very broad; consider narrowing to a stable default device

Filtering on “TV” / “Watch” may match unintended entries. Consider targeting a canonical device (e.g., latest Apple TV 4K, Apple Watch Series).

Example (patternized):

PLATFORM_TVOS = tvOS Simulator,id=$(call udid_for,tvOS,Apple TV 4K)
PLATFORM_WATCHOS = watchOS Simulator,id=$(call udid_for,Apple Watch Series)
Sources/ComposableUserNotifications/LiveKey.swift (1)

115-120: Mirror the same main‑actor handoff for response completion.

Keep completion callsite semantics consistent across delegate methods.

-            completionHandler: { [handler = UncheckedSendable(completionHandler)] in
-              handler.value()
-            }
+            completionHandler: { [handler = UncheckedSendable(completionHandler)] in
+              if Thread.isMainThread {
+                handler.value()
+              } else {
+                DispatchQueue.main.async { handler.value() }
+              }
+            }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fedca0b and d9b7ad4.

📒 Files selected for processing (8)
  • .github/workflows/ci.yml (1 hunks)
  • Makefile (2 hunks)
  • Package.swift (1 hunks)
  • Sources/ComposableUserNotifications/Interface.swift (3 hunks)
  • Sources/ComposableUserNotifications/LiveKey.swift (5 hunks)
  • Sources/ComposableUserNotifications/Model.swift (11 hunks)
  • Tests/ComposableUserNotificationsTests/ComposableUserNotificationsTests.swift (0 hunks)
  • Tests/ComposableUserNotificationsTests/XCTestManifests.swift (0 hunks)
💤 Files with no reviewable changes (2)
  • Tests/ComposableUserNotificationsTests/ComposableUserNotificationsTests.swift
  • Tests/ComposableUserNotificationsTests/XCTestManifests.swift
🧰 Additional context used
🪛 actionlint (1.7.8)
.github/workflows/ci.yml

24-24: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🔇 Additional comments (9)
Package.swift (1)

1-1: Swift tools 6.0 bump looks good

Matches the concurrency updates elsewhere. No action needed.

Sources/ComposableUserNotifications/Model.swift (2)

150-160: Nice: Sendable wrapper for nextTriggerDate

Capturing the ObjC method as a closure via UncheckedSendable is a pragmatic workaround.

Please confirm this compiles cleanly across all package platforms in Swift 6 mode (Xcode 16.4). If any warnings persist, I can help adjust the capture.


481-483: Region Sendable + rawValue nonisolated is consistent

Looks good and matches TCA patterns.

Sources/ComposableUserNotifications/Interface.swift (2)

1-1: @preconcurrency import is appropriate here

Prevents retroactive Sendable issues with UserNotifications. LGTM.


11-25: Sendable annotations across API surface look consistent

Closures and DelegateAction are marked @sendable; equality intentionally ignores the completion handlers. Looks correct.

Consider adding brief doc comments noting that completion handlers must be invoked on the main actor to match UNUserNotificationCenter delegate expectations.

Sources/ComposableUserNotifications/LiveKey.swift (4)

3-3: LGTM on @preconcurrency import.

This is the right lever to silence pre‑annotated SDK concurrency warnings while keeping the rest of the file strict.


84-84: AsyncStream.Continuation is Sendable—no action required.

AsyncStream.Continuation is documented as Sendable and Swift 6 continues to treat AsyncStream continuation APIs as safe-to-send. The Delegate class can safely conform to Sendable provided its other stored properties are also immutable and Sendable.


69-77: Confirm delegate lifecycle management intent.

The review comment is substantively correct. The variable center is in scope at line 7 and is accessible within the client.delegate closure. The current code does not capture or restore the previous delegate, which the review appropriately flags as a potential issue.

The review's suggestion is syntactically sound for Swift and feasible to implement. However, the developer must clarify their intended behavior:

  1. If the caller owns the delegate lifecycle exclusively (recommended unless there are other delegates in the system), apply the suggested refactor to restore the previous delegate on stream termination.
  2. If cleanup is not needed, confirm this assumption and the review can be closed.

The review comment appropriately requests this verification and provides both the preferred solution and the alternative option. No action is needed from you until you determine your delegate management intent.


100-103: Reject this refactor—the library should not force main thread dispatch.

Apple's documentation does not specify which thread the willPresent completion handler is invoked on. However, this does not justify the library adding unconditional dispatch-to-main.

The library correctly defers threading responsibility to consumers. The completion handler is wrapped in UncheckedSendable and yielded through AsyncStream, allowing consumers to call it from their chosen execution context. If a consumer requires main thread execution (e.g., for UI updates), they already have the tools: @MainActor, .mainThreadExecutor, or explicit dispatch. The README examples show consumers calling completion directly without expecting main thread dispatch.

Silently forcing dispatch-to-main would:

  • Introduce an undocumented behavior change with no API contract
  • Add unnecessary synchronization overhead for consumers not requiring it
  • Violate the principle that libraries should not dictate threading for callers
  • Contradict the library's modern async/await design

The current implementation is correct.

matrix:
config: ['debug', 'release']
steps:
- uses: actions/checkout@v3
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Update checkout action to v4 (v3 is too old on macos-15)

This will avoid runner/runtime warnings.

Apply:

-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
📝 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
- uses: actions/checkout@v3
- uses: actions/checkout@v4
🧰 Tools
🪛 actionlint (1.7.8)

24-24: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
.github/workflows/ci.yml around line 24: the workflow uses actions/checkout@v3
which triggers runner/runtime warnings on newer macOS runners; update the step
to use actions/checkout@v4 by replacing the uses reference to
actions/checkout@v4 (ensure no other breaking inputs/options are required by v4
in your workflow).

@@ -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])
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix regex: grep default doesn’t support \d. Use -E and [0-9]+; tighten Pro-not-Max match

Current pattern won’t match digits; it literally searches for "d+". Also ensure we exclude "Pro Max" robustly.

Apply:

-PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS,iPhone \d\+ Pro [^M])
+PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS,iPhone [0-9]+ Pro( \(|$$))

Note: The pattern matches “… Pro (” or end-of-line, excluding “Pro Max …”.

📝 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
PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS,iPhone \d\+ Pro [^M])
PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS,iPhone [0-9]+ Pro( \(|$$))
🤖 Prompt for AI Agents
In Makefile around line 2, the grep pattern uses the unsupported \d and can
accidentally match "Pro Max"; change to use extended regex (grep -E) with [0-9]+
for digits and tighten the "Pro" match to exclude "Pro Max" — e.g., use grep -E
'iPhone [0-9]+ Pro($| )' or use grep -E 'iPhone [0-9]+ Pro' combined with a
subsequent grep -v 'Pro Max' to ensure only "Pro" (not "Pro Max") models are
matched.

Comment on lines +38 to 39
public var notificationCategories: @Sendable () async -> Set<UNNotificationCategory> = unimplemented(
"\(Self.self).deliveredNotifications")
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant