From 52b10585abb42c21aa9f00f6b28c56f8a4a6e208 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Wed, 11 Jun 2025 08:36:44 -0700 Subject: [PATCH] Fixes for Swift 6.2 on iOS 26 and macOS 26 --- Readme.md | 6 +++--- Sources/AsyncMonitor/AsyncMonitor.swift | 3 ++- Sources/AsyncMonitor/AsyncSequence+Extensions.swift | 3 ++- Sources/AsyncMonitor/NSObject+AsyncKVO.swift | 3 ++- Tests/AsyncMonitorTests/AsyncMonitorTests.swift | 12 +++++++----- Tests/AsyncMonitorTests/NSObject+AsyncKVOTests.swift | 8 ++++---- Tests/AsyncMonitorTests/ReadmeExamples.swift | 8 +++++--- 7 files changed, 25 insertions(+), 18 deletions(-) diff --git a/Readme.md b/Readme.md index ba80cdd..8d6f7cb 100644 --- a/Readme.md +++ b/Readme.md @@ -32,8 +32,8 @@ class SimplestVersion { This example uses the context parameter to avoid reference cycles with `self`. ```swift -class WithContext { - var cancellables = Set() +final class WithContext: Sendable { + nonisolated(unsafe) var cancellables = Set() init() { NotificationCenter.default @@ -55,7 +55,7 @@ class WithContext { Working with Combine publishers is trivial thanks to [`AnyPublisher.values`][values]. ```swift -import Combine +@preconcurrency import Combine class CombineExample { var cancellables = Set() diff --git a/Sources/AsyncMonitor/AsyncMonitor.swift b/Sources/AsyncMonitor/AsyncMonitor.swift index 5729a24..d8cbaf0 100644 --- a/Sources/AsyncMonitor/AsyncMonitor.swift +++ b/Sources/AsyncMonitor/AsyncMonitor.swift @@ -20,7 +20,7 @@ public final class AsyncMonitor: Hashable, AsyncCancellable { /// Defaults to `#isolation`, preserving the caller's actor isolation. /// - sequence: The asynchronous sequence of elements to observe. /// - block: A closure to execute for each element yielded by the sequence. - @available(iOS 18, *) + @available(iOS 18, macOS 15, *) public init( isolation: isolated (any Actor)? = #isolation, sequence: any AsyncSequence, @@ -41,6 +41,7 @@ public final class AsyncMonitor: Hashable, AsyncCancellable { /// - sequence: The asynchronous sequence of elements to observe. /// - block: A closure to execute for each element yielded by the sequence. @available(iOS, introduced: 17, obsoleted: 18) + @available(macOS, introduced: 14, obsoleted: 15) public init( sequence: sending Sequence, @_inheritActorContext performing block: @escaping @Sendable (Element) async -> Void diff --git a/Sources/AsyncMonitor/AsyncSequence+Extensions.swift b/Sources/AsyncMonitor/AsyncSequence+Extensions.swift index cd6d280..4743d39 100644 --- a/Sources/AsyncMonitor/AsyncSequence+Extensions.swift +++ b/Sources/AsyncMonitor/AsyncSequence+Extensions.swift @@ -1,4 +1,4 @@ -@available(iOS 18, *) +@available(iOS 18, macOS 15, *) public extension AsyncSequence where Element: Sendable, Failure == Never { /// Observes the elements yielded by this sequence and executes the given closure with each element. /// @@ -36,6 +36,7 @@ public extension AsyncSequence where Element: Sendable, Failure == Never { } @available(iOS, introduced: 17, obsoleted: 18) +@available(macOS, introduced: 14, obsoleted: 15) public extension AsyncSequence where Self: Sendable, Element: Sendable { /// Observes the elements yielded by this sequence and executes the given closure with each element. /// diff --git a/Sources/AsyncMonitor/NSObject+AsyncKVO.swift b/Sources/AsyncMonitor/NSObject+AsyncKVO.swift index acce324..a43b11f 100644 --- a/Sources/AsyncMonitor/NSObject+AsyncKVO.swift +++ b/Sources/AsyncMonitor/NSObject+AsyncKVO.swift @@ -25,7 +25,7 @@ public extension NSObjectProtocol where Self: NSObject { } } -@available(iOS 18, *) +@available(iOS 18, macOS 15, *) public extension NSObjectProtocol where Self: NSObject { /// Observes changes to the specified key path on the object and asynchronously yields each value. Values must be `Sendable`. /// @@ -44,6 +44,7 @@ public extension NSObjectProtocol where Self: NSObject { } @available(iOS, introduced: 17, obsoleted: 18) +@available(macOS, introduced: 14, obsoleted: 15) public extension NSObjectProtocol where Self: NSObject { /// Observes changes to the specified key path on the object and asynchronously yields each value. Values must be `Sendable`. /// diff --git a/Tests/AsyncMonitorTests/AsyncMonitorTests.swift b/Tests/AsyncMonitorTests/AsyncMonitorTests.swift index 276d6e4..deca416 100644 --- a/Tests/AsyncMonitorTests/AsyncMonitorTests.swift +++ b/Tests/AsyncMonitorTests/AsyncMonitorTests.swift @@ -49,12 +49,12 @@ class AsyncMonitorTests { try await Task.sleep(for: .milliseconds(10)) } - class Owner { - let deinitHook: () -> Void + final class Owner: Sendable { + let deinitHook: @Sendable () -> Void - private var cancellable: (any AsyncCancellable)? + nonisolated(unsafe) private var cancellable: (any AsyncCancellable)? - init(center: NotificationCenter, deinitHook: @escaping () -> Void) { + init(center: NotificationCenter, deinitHook: @escaping @Sendable () -> Void) { self.deinitHook = deinitHook let name = Notification.Name("irrelevant name") cancellable = center.notifications(named: name) @@ -78,8 +78,10 @@ class AsyncMonitorTests { } } + final class SendableObject: NSObject, Sendable {} + @Test func stopsCallingBlockWhenContextIsDeallocated() async throws { - var context: NSObject? = NSObject() + var context: SendableObject? = SendableObject() subject = center.notifications(named: name) .map(\.name) .monitor(context: context!) { context, receivedName in diff --git a/Tests/AsyncMonitorTests/NSObject+AsyncKVOTests.swift b/Tests/AsyncMonitorTests/NSObject+AsyncKVOTests.swift index f8b7333..95d2eef 100644 --- a/Tests/AsyncMonitorTests/NSObject+AsyncKVOTests.swift +++ b/Tests/AsyncMonitorTests/NSObject+AsyncKVOTests.swift @@ -9,22 +9,22 @@ class AsyncKVOTests { @Test(.timeLimit(.minutes(1))) func monitorValuesYieldsChanges() async throws { let subject = try #require(subject) - var values = [Double]() + let values = ValueLocker(value: [Double]()) let total = 3 cancellable = subject.values(for: \.fractionCompleted) .prefix(total) .monitor { progress in - values.append(progress) + values.modify { $0.append(progress) } } for n in 1...total { subject.completedUnitCount += 1 - while values.count < n { + while values.value.count < n { try await Task.sleep(for: .microseconds(2)) } } - #expect(values.count == total) + #expect(values.value.count == total) } // It's important that the test and the progress-observing task are not on the same actor, so diff --git a/Tests/AsyncMonitorTests/ReadmeExamples.swift b/Tests/AsyncMonitorTests/ReadmeExamples.swift index 76d5754..f24242d 100644 --- a/Tests/AsyncMonitorTests/ReadmeExamples.swift +++ b/Tests/AsyncMonitorTests/ReadmeExamples.swift @@ -3,6 +3,8 @@ import Foundation // MARK: Basics +extension Notification: @unchecked @retroactive Sendable {} + class SimplestVersion { let cancellable = NotificationCenter.default .notifications(named: .NSCalendarDayChanged) @@ -12,8 +14,8 @@ class SimplestVersion { } } -class WithContext { - var cancellables = Set() +final class WithContext: Sendable { + nonisolated(unsafe) var cancellables = Set() init() { NotificationCenter.default @@ -31,7 +33,7 @@ class WithContext { // MARK: - Combine -import Combine +@preconcurrency import Combine class CombineExample { var cancellables = Set()