diff --git a/Readme.md b/Readme.md index 8d6f7cb..d256945 100644 --- a/Readme.md +++ b/Readme.md @@ -58,7 +58,7 @@ Working with Combine publishers is trivial thanks to [`AnyPublisher.values`][val @preconcurrency import Combine class CombineExample { - var cancellables = Set() + var cancellables: Set = [] init() { Timer.publish(every: 1.0, on: .main, in: .common) @@ -66,8 +66,7 @@ class CombineExample { .values .monitor { date in print("Timer fired at \(date)") - } - .store(in: &cancellables) + }.store(in: &cancellables) } } ``` @@ -80,11 +79,11 @@ When you need to observe an object that uses [KVO][] there's an extension method ```swift class KVOExample { - var cancellables = Set() + var cancellables: Set = [] init() { let progress = Progress(totalUnitCount: 42) - progress.monitorValues(for: \.fractionCompleted) { fraction in + progress.monitorValues(for: \.fractionCompleted, options: [.initial, .new]) { fraction in print("Progress is \(fraction.formatted(.percent))%") }.store(in: &cancellables) } diff --git a/Sources/AsyncMonitor/AnyAsyncCancellable.swift b/Sources/AsyncMonitor/AnyAsyncCancellable.swift index 596b7b2..c318b04 100644 --- a/Sources/AsyncMonitor/AnyAsyncCancellable.swift +++ b/Sources/AsyncMonitor/AnyAsyncCancellable.swift @@ -1,10 +1,19 @@ -/// Type-erasing wrapper for ``AsyncCancellable`` that ties its instance lifetime to cancellation. In other words, when you release -/// an instance of ``AnyAsyncCancellable`` and it's deallocated then it automatically cancels its given ``AsyncCancellable``. +/// Type-erasing wrapper for ``AsyncCancellable`` that automatically cancels when deallocated. +/// +/// `AnyAsyncCancellable` provides automatic cancellation when deallocated, making it safe to store +/// cancellables without explicitly managing their lifecycle. +/// public class AnyAsyncCancellable: AsyncCancellable { lazy var id = ObjectIdentifier(self) let canceller: () -> Void + /// Creates a type-erased wrapper around the provided cancellable. + /// + /// The wrapper will call the cancellable's `cancel()` method when either + /// explicitly cancelled or deallocated. + /// + /// - Parameter cancellable: The ``AsyncCancellable`` to wrap. public init(cancellable: AC) { canceller = { cancellable.cancel() } } @@ -15,6 +24,7 @@ public class AnyAsyncCancellable: AsyncCancellable { // MARK: AsyncCancellable conformance + /// Cancels the wrapped cancellable. Safe to call multiple times and automatically called on deallocation. public func cancel() { canceller() } diff --git a/Sources/AsyncMonitor/AsyncCancellable.swift b/Sources/AsyncMonitor/AsyncCancellable.swift index 94dcc87..2072f7d 100644 --- a/Sources/AsyncMonitor/AsyncCancellable.swift +++ b/Sources/AsyncMonitor/AsyncCancellable.swift @@ -1,10 +1,15 @@ /// Represents an async operation that can be cancelled. +/// +/// `AsyncCancellable` provides a common interface for cancelling async operations, similar to +/// Combine's `AnyCancellable` but designed for Swift concurrency patterns. +/// public protocol AsyncCancellable: AnyObject, Hashable { - /// Cancels the operation. + /// Cancels the operation. Safe to call multiple times. func cancel() - /// Stores this cancellable in the given set, using the type-erasing wrapper ``AnyAsyncCancellable``. This method has a - /// default implementation and you typically shouldn't need to override it. + /// Stores this cancellable in the given set using ``AnyAsyncCancellable``. + /// + /// - Parameter set: The set to store the wrapped cancellable in. func store(in set: inout Set) } diff --git a/Sources/AsyncMonitor/AsyncMonitor.docc/AsyncMonitor.md b/Sources/AsyncMonitor/AsyncMonitor.docc/AsyncMonitor.md new file mode 100644 index 0000000..a362cef --- /dev/null +++ b/Sources/AsyncMonitor/AsyncMonitor.docc/AsyncMonitor.md @@ -0,0 +1,112 @@ +# ``AsyncMonitor`` + +Wraps async sequence observation in manageable tasks. + +## Overview + +AsyncMonitor wraps async sequence observation in a `Task` that can be cancelled and stored. It preserves actor isolation on iOS 18+ and includes KVO integration. + +## Basic Usage + +```swift +import AsyncMonitor + +// Monitor notifications +NotificationCenter.default + .notifications(named: .NSCalendarDayChanged) + .map(\.name) + .monitor { _ in print("Day changed!") } + +// Store for longer lifetime +var cancellables: Set = [] + +sequence.monitor { element in + // Handle element +}.store(in: &cancellables) +``` + +## Context-Aware Monitoring + +Prevent retain cycles with weak context: + +```swift +class DataController { + var cancellables: Set = [] + + func startMonitoring() { + dataStream + .monitor(context: self) { controller, data in + controller.processData(data) + }.store(in: &cancellables) + } +} +``` + +## KVO Integration + +```swift +let progress = Progress(totalUnitCount: 100) + +progress.monitorValues(for: \.fractionCompleted, options: [.initial, .new]) { fraction in + print("Progress: \(fraction.formatted(.percent))") +}.store(in: &cancellables) +``` + +## Error Handling + +Both throwing and non-throwing sequences work. Errors are caught and logged automatically. + +```swift +// Non-throwing +Timer.publish(every: 1.0, on: .main, in: .common) + .autoconnect() + .values + .monitor { date in print("Timer: \(date)") } + +// Throwing (errors caught automatically) +networkDataStream() + .monitor { data in processData(data) } +``` + +## Memory Management + +Use weak captures or context to avoid retain cycles: + +```swift +// Good +sequence.monitor(context: self) { controller, element in + controller.handle(element) +} + +// Good +sequence.monitor { [weak self] element in + self?.handle(element) +} + +// Bad - creates retain cycle +sequence.monitor { element in + self.handle(element) +} +``` + +## Platform Requirements + +- iOS 17.0+ / macOS 14.0+ +- Swift 6.0+ + +## Topics + +### Core Types + +- ``AsyncMonitor`` - Wraps async sequence observation in a managed Task +- ``AsyncCancellable`` - Protocol for async operations that can be cancelled +- ``AnyAsyncCancellable`` - Type-erasing wrapper that auto-cancels on deallocation + +### Sequence Extensions + +- ``Foundation/AsyncSequence/monitor(_:)`` +- ``Foundation/AsyncSequence/monitor(context:_:)`` + +### KVO Integration + +- ``Foundation/NSObjectProtocol/monitorValues(for:options:changeHandler:)`` diff --git a/Sources/AsyncMonitor/AsyncMonitor.swift b/Sources/AsyncMonitor/AsyncMonitor.swift index d8cbaf0..1fe1338 100644 --- a/Sources/AsyncMonitor/AsyncMonitor.swift +++ b/Sources/AsyncMonitor/AsyncMonitor.swift @@ -1,23 +1,24 @@ /// A monitor that observes an asynchronous sequence and invokes the given block for each received element. /// -/// The element must be `Sendable` so to use it to monitor notifications from `NotificationCenter` you'll need to map them to -/// something sendable before calling `monitor` on the sequence. e.g. +/// `AsyncMonitor` wraps the observation of an async sequence in a `Task`, providing automatic cancellation +/// and memory management. Elements must be `Sendable`. For notifications, map to something sendable: /// -/// ``` +/// ```swift /// NotificationCenter.default /// .notifications(named: .NSCalendarDayChanged) /// .map(\.name) -/// .monitor { _ in whatever() } -/// .store(in: &cancellables) +/// .monitor { _ in print("Day changed!") } /// ``` +/// +/// On iOS 18+, preserves the caller's actor isolation context by default. +/// public final class AsyncMonitor: Hashable, AsyncCancellable { let task: Task - /// Creates an ``AsyncMonitor`` that observes the provided asynchronous sequence. + /// Creates an ``AsyncMonitor`` that observes the provided asynchronous sequence with actor isolation support (iOS 18+). /// /// - Parameters: - /// - isolation: An optional actor isolation context to inherit. - /// Defaults to `#isolation`, preserving the caller's actor isolation. + /// - isolation: An optional actor isolation context to inherit. Defaults to `#isolation`. /// - sequence: The asynchronous sequence of elements to observe. /// - block: A closure to execute for each element yielded by the sequence. @available(iOS 18, macOS 15, *) @@ -35,11 +36,36 @@ public final class AsyncMonitor: Hashable, AsyncCancellable { } } - /// Creates an ``AsyncMonitor`` that observes the provided asynchronous sequence. + /// Creates an ``AsyncMonitor`` for sequences that may throw errors (iOS 18+). /// /// - Parameters: - /// - sequence: The asynchronous sequence of elements to observe. + /// - isolation: An optional actor isolation context to inherit. Defaults to `#isolation`. + /// - sequence: The asynchronous sequence of elements to observe. May throw errors. /// - block: A closure to execute for each element yielded by the sequence. + @available(iOS 18, macOS 15, *) + public init( + isolation: isolated (any Actor)? = #isolation, + sequence: Sequence, + performing block: @escaping (Element) async -> Void + ) where Sequence.Element == Element { + self.task = Task { + _ = isolation // use capture trick to inherit isolation + + do { + for try await element in sequence { + await block(element) + } + } catch { + guard !Task.isCancelled else { return } + } + } + } + + /// Creates an ``AsyncMonitor`` for iOS 17 compatibility. + /// + /// - Parameters: + /// - sequence: The asynchronous sequence of elements to observe. Must be `Sendable`. + /// - block: A `@Sendable` closure to execute for each element yielded by the sequence. @available(iOS, introduced: 17, obsoleted: 18) @available(macOS, introduced: 14, obsoleted: 15) public init( @@ -65,7 +91,7 @@ public final class AsyncMonitor: Hashable, AsyncCancellable { // MARK: AsyncCancellable conformance - /// Cancels the underlying task monitoring the asynchronous sequence. + /// Cancels the underlying task. Safe to call multiple times and automatically called when deallocated. public func cancel() { task.cancel() } diff --git a/Sources/AsyncMonitor/AsyncSequence+Extensions.swift b/Sources/AsyncMonitor/AsyncSequence+Extensions.swift index 4743d39..3bf8273 100644 --- a/Sources/AsyncMonitor/AsyncSequence+Extensions.swift +++ b/Sources/AsyncMonitor/AsyncSequence+Extensions.swift @@ -2,11 +2,35 @@ public extension AsyncSequence where Element: Sendable, Failure == Never { /// Observes the elements yielded by this sequence and executes the given closure with each element. /// - /// This method preserves the actor isolation of the caller by default when `isolation` is not specified. + /// This method creates an ``AsyncMonitor`` that observes the sequence and preserves the caller's + /// actor isolation context by default. When called from a `@MainActor` context, the monitoring + /// block will also run on the main actor. /// /// - Parameters: - /// - isolation: An optional actor isolation context to inherit. Defaults to `#isolation`, preserving the caller's actor isolation. - /// - block: A closure that's executed with each yielded element. + /// - isolation: An optional actor isolation context to inherit. Defaults to `#isolation`, + /// preserving the caller's actor isolation. + /// - block: A closure that's executed with each yielded element. The closure runs with + /// the same actor isolation as the caller. + /// + /// - Returns: An ``AsyncMonitor`` that can be stored and cancelled as needed. + /// + /// ## Example + /// + /// ```swift + /// @MainActor class ViewModel { + /// var cancellables: Set = [] + /// + /// func startMonitoring() { + /// // Monitor runs on MainActor since caller is @MainActor + /// NotificationCenter.default + /// .notifications(named: .NSCalendarDayChanged) + /// .map(\.name) + /// .monitor { _ in + /// self.updateUI() // Safe to call @MainActor methods + /// }.store(in: &cancellables) + /// } + /// } + /// ``` func monitor( isolation: isolated (any Actor)? = #isolation, _ block: @escaping (Element) async -> Void @@ -14,15 +38,120 @@ public extension AsyncSequence where Element: Sendable, Failure == Never { AsyncMonitor(isolation: isolation, sequence: self, performing: block) } - /// Observes the elements yielded by this sequence and executes the given closure with each element the weakly-captured - /// context object. + /// Observes the elements yielded by this sequence and executes the given closure with each element and the weakly-captured context object. /// - /// This method preserves the actor isolation of the caller by default when `isolation` is not specified. + /// This method creates an ``AsyncMonitor`` that weakly captures the provided context object, preventing retain cycles. + /// If the context object is deallocated, the monitoring block will not be executed for subsequent elements. /// /// - Parameters: - /// - isolation: An optional actor isolation context to inherit. Defaults to `#isolation`, preserving the caller's actor isolation. - /// - context: The object to capture weakly for use within the closure. - /// - block: A closure that's executed with each yielded element, and the `context`. + /// - isolation: An optional actor isolation context to inherit. Defaults to `#isolation`, + /// preserving the caller's actor isolation. + /// - context: The object to capture weakly for use within the closure. This prevents retain cycles + /// when the context holds a reference to the monitor. + /// - block: A closure that's executed with the weakly-captured context and each yielded element. + /// The closure runs with the same actor isolation as the caller. + /// + /// - Returns: An ``AsyncMonitor`` that can be stored and cancelled as needed. + /// + /// ## Example + /// + /// ```swift + /// class DataManager { + /// var cancellables: Set = [] + /// + /// func startMonitoring() { + /// // Context is weakly captured, preventing retain cycle + /// dataStream + /// .monitor(context: self) { manager, data in + /// manager.process(data) // manager won't be nil here + /// }.store(in: &cancellables) + /// } + /// + /// func process(_ data: Data) { + /// // Process the data + /// } + /// } + /// ``` + func monitor( + isolation: isolated (any Actor)? = #isolation, + context: Context, + _ block: @escaping (Context, Element) async -> Void + ) -> AsyncMonitor { + AsyncMonitor(isolation: isolation, sequence: self) { [weak context] element in + guard let context else { return } + await block(context, element) + } + } +} + +@available(iOS 18, macOS 15, *) +public extension AsyncSequence where Element: Sendable { + /// Observes the elements yielded by this sequence and executes the given closure with each element. + /// + /// This method creates an ``AsyncMonitor`` that observes the sequence and preserves the caller's + /// actor isolation context by default. When called from a `@MainActor` context, the monitoring + /// block will also run on the main actor. + /// + /// This version handles sequences that may throw errors. If an error is thrown, it will be logged + /// and monitoring will stop. + /// + /// - Parameters: + /// - isolation: An optional actor isolation context to inherit. Defaults to `#isolation`, + /// preserving the caller's actor isolation. + /// - block: A closure that's executed with each yielded element. The closure runs with + /// the same actor isolation as the caller. + /// + /// - Returns: An ``AsyncMonitor`` that can be stored and cancelled as needed. + /// + /// ## Example + /// + /// ```swift + /// NotificationCenter.default + /// .notifications(named: .NSCalendarDayChanged) + /// .map(\.name) + /// .monitor { _ in + /// print("Day changed!") + /// }.store(in: &cancellables) + /// ``` + func monitor( + isolation: isolated (any Actor)? = #isolation, + _ block: @escaping (Element) async -> Void + ) -> AsyncMonitor { + AsyncMonitor(isolation: isolation, sequence: self, performing: block) + } + + /// Observes the elements yielded by this sequence and executes the given closure with each element and the weakly-captured context object. + /// + /// This method creates an ``AsyncMonitor`` that weakly captures the provided context object, preventing retain cycles. + /// If the context object is deallocated, the monitoring block will not be executed for subsequent elements. + /// + /// This version handles sequences that may throw errors. If an error is thrown, it will be logged + /// and monitoring will stop. + /// + /// - Parameters: + /// - isolation: An optional actor isolation context to inherit. Defaults to `#isolation`, + /// preserving the caller's actor isolation. + /// - context: The object to capture weakly for use within the closure. This prevents retain cycles + /// when the context holds a reference to the monitor. + /// - block: A closure that's executed with the weakly-captured context and each yielded element. + /// The closure runs with the same actor isolation as the caller. + /// + /// - Returns: An ``AsyncMonitor`` that can be stored and cancelled as needed. + /// + /// ## Example + /// + /// ```swift + /// class DataManager { + /// var cancellables: Set = [] + /// + /// func startMonitoring() { + /// notificationStream + /// .monitor(context: self) { manager, notification in + /// manager.handleNotification(notification) + /// }.store(in: &cancellables) + /// } + /// } + /// ``` func monitor( isolation: isolated (any Actor)? = #isolation, context: Context, @@ -38,22 +167,69 @@ 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. + /// Observes the elements yielded by this sequence and executes the given closure with each element (iOS 17 compatibility). + /// + /// This method provides backward compatibility for iOS 17. It requires both the sequence and its elements + /// to be `Sendable`, and uses a `@Sendable` closure for thread safety. /// /// - Parameters: - /// - block: A closure that's executed with each yielded element. + /// - block: A `@Sendable` closure that's executed with each yielded element. + /// + /// - Returns: An ``AsyncMonitor`` that can be stored and cancelled as needed. + /// + /// ## Example + /// + /// ```swift + /// let cancellable = sendableAsyncSequence.monitor { element in + /// print("Received: \(element)") + /// } + /// + /// // Store for automatic cleanup + /// cancellable.store(in: &cancellables) + /// ``` + /// + /// - Note: This method is deprecated in iOS 18+ in favour of ``monitor(isolation:_:)`` + /// which provides better actor isolation support. func monitor( _ block: @escaping @Sendable (Element) async -> Void ) -> AsyncMonitor { AsyncMonitor(sequence: self, performing: block) } - /// Observes the elements yielded by this sequence and executes the given closure with each element the weakly-captured - /// context object. + /// Observes the elements yielded by this sequence and executes the given closure with each element and the weakly-captured context object (iOS 17 compatibility). + /// + /// This method provides backward compatibility for iOS 17 with weak reference handling to prevent retain cycles. + /// It requires the context to be both `AnyObject` and `Sendable` for thread safety. /// /// - Parameters: - /// - context: The object to capture weakly for use within the closure. - /// - block: A closure that's executed with each yielded element, and the `context`. + /// - context: The object to capture weakly for use within the closure. Must be `Sendable` and will be + /// captured weakly to prevent retain cycles. + /// - block: A `@Sendable` closure that's executed with the weakly-captured context and each yielded element. + /// + /// - Returns: An ``AsyncMonitor`` that can be stored and cancelled as needed. + /// + /// ## Example + /// + /// ```swift + /// class SendableDataManager: Sendable { + /// var cancellables: Set = [] + /// + /// func startMonitoring() { + /// // Context is weakly captured, preventing retain cycle + /// sendableDataStream + /// .monitor(context: self) { manager, data in + /// manager.process(data) + /// }.store(in: &cancellables) + /// } + /// + /// func process(_ data: Data) { + /// // Process the data + /// } + /// } + /// ``` + /// + /// - Note: This method is deprecated in iOS 18+ in favour of ``monitor(isolation:context:_:)`` + /// which provides better actor isolation support. func monitor( context: Context, _ block: @escaping @Sendable (Context, Element) async -> Void diff --git a/Sources/AsyncMonitor/NSObject+AsyncKVO.swift b/Sources/AsyncMonitor/NSObject+AsyncKVO.swift index a43b11f..3ba3fe8 100644 --- a/Sources/AsyncMonitor/NSObject+AsyncKVO.swift +++ b/Sources/AsyncMonitor/NSObject+AsyncKVO.swift @@ -3,11 +3,40 @@ public import Foundation extension KeyPath: @unchecked @retroactive Sendable where Value: Sendable {} public extension NSObjectProtocol where Self: NSObject { - /// Returns an `AsyncSequence` of `Value`s for all changes to the given key path on this object. + /// Returns an `AsyncSequence` of values for all changes to the given key path on this object. + /// + /// This method creates an `AsyncStream` that yields the current value of the specified key path + /// whenever it changes via Key-Value Observing (KVO). The stream automatically manages the KVO + /// observation lifecycle and cleans up when the stream is terminated. /// /// - Parameters: - /// - keyPath: The key path to observe on this object. The value must be `Sendable`. + /// - keyPath: The key path to observe on this object. The value type must be `Sendable` + /// to ensure thread safety across async contexts. /// - options: KVO options to use for observation. Defaults to an empty set. + /// See `NSKeyValueObservingOptions` for available options. + /// + /// - Returns: An `AsyncStream` that yields the current value of the key path + /// whenever it changes. + /// + /// ## Example + /// + /// ```swift + /// let progress = Progress(totalUnitCount: 100) + /// + /// for await fraction in progress.values(for: \.fractionCompleted) { + /// print("Progress: \(fraction.formatted(.percent))") + /// if fraction >= 1.0 { break } + /// } + /// ``` + /// + /// ## Thread Safety + /// + /// The returned stream is thread-safe and can be consumed from any actor context. + /// The KVO observation token is automatically retained by the stream and released + /// when the stream terminates. + /// + /// - Important: The observed object must remain alive for the duration of the observation. + /// If the object is deallocated, the stream will terminate. func values( for keyPath: KeyPath, options: NSKeyValueObservingOptions = [] @@ -27,12 +56,48 @@ public extension NSObjectProtocol where Self: NSObject { @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`. + /// Observes changes to the specified key path on the object and executes a handler for each change. + /// + /// This method combines KVO observation with ``AsyncMonitor`` to provide a convenient way to + /// monitor object property changes. It creates an ``AsyncMonitor`` that observes the key path + /// and preserves the caller's actor isolation context. /// /// - Parameters: - /// - keyPath: The key path to observe on this object. The value must be `Sendable`. + /// - keyPath: The key path to observe on this object. The value type must be `Sendable` + /// to ensure thread safety across async contexts. /// - options: KVO options to use for observation. Defaults to an empty set. - /// - changeHandler: A closure that's executed with each new value. + /// See `NSKeyValueObservingOptions` for available options. + /// - changeHandler: A closure that's executed with each new value. The closure runs with + /// the same actor isolation as the caller. + /// + /// - Returns: An ``AsyncCancellable`` that can be stored and cancelled as needed. + /// + /// ## Example + /// + /// ```swift + /// @MainActor class ProgressView: UIView { + /// var cancellables: Set = [] + /// + /// func observeProgress(_ progress: Progress) { + /// // Handler runs on MainActor since caller is @MainActor + /// progress.monitorValues(for: \.fractionCompleted) { [weak self] fraction in + /// self?.updateProgressBar(fraction) + /// }.store(in: &cancellables) + /// } + /// + /// func updateProgressBar(_ fraction: Double) { + /// // Update UI safely on MainActor + /// } + /// } + /// ``` + /// + /// ## Usage with KVO Options + /// + /// ```swift + /// object.monitorValues(for: \.property, options: [.initial, .new]) { newValue in + /// print("Property changed to: \(newValue)") + /// } + /// ``` func monitorValues( for keyPath: KeyPath, options: NSKeyValueObservingOptions = [], @@ -46,12 +111,36 @@ 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`. + /// Observes changes to the specified key path on the object and executes a handler for each change (iOS 17 compatibility). + /// + /// This method provides backward compatibility for iOS 17. It combines KVO observation with ``AsyncMonitor`` + /// and requires a `@Sendable` closure for thread safety. /// /// - Parameters: - /// - keyPath: The key path to observe on this object. The value must be `Sendable`. + /// - keyPath: The key path to observe on this object. The value type must be `Sendable` + /// to ensure thread safety across async contexts. /// - options: KVO options to use for observation. Defaults to an empty set. - /// - changeHandler: A closure that's executed with each new value. + /// See `NSKeyValueObservingOptions` for available options. + /// - changeHandler: A `@Sendable` closure that's executed with each new value. + /// + /// - Returns: An ``AsyncCancellable`` that can be stored and cancelled as needed. + /// + /// ## Example + /// + /// ```swift + /// class ProgressObserver { + /// var cancellables: Set = [] + /// + /// func observeProgress(_ progress: Progress) { + /// progress.monitorValues(for: \.fractionCompleted) { fraction in + /// print("Progress: \(fraction.formatted(.percent))") + /// }.store(in: &cancellables) + /// } + /// } + /// ``` + /// + /// - Note: This method is deprecated in iOS 18+ in favour of the non-`@Sendable` version + /// which provides better actor isolation support. func monitorValues( for keyPath: KeyPath, options: NSKeyValueObservingOptions = [], diff --git a/Tests/AsyncMonitorTests/AsyncCancellableTests.swift b/Tests/AsyncMonitorTests/AsyncCancellableTests.swift index 5bcb47d..7fd5fb3 100644 --- a/Tests/AsyncMonitorTests/AsyncCancellableTests.swift +++ b/Tests/AsyncMonitorTests/AsyncCancellableTests.swift @@ -2,7 +2,7 @@ import Testing @MainActor class AsyncCancellableTests { - var cancellables = Set() + var cancellables: Set = [] @Test func storeInsertsIntoSetAndKeepsSubjectAlive() throws { var subject: TestCancellable? = TestCancellable() diff --git a/Tests/AsyncMonitorTests/ReadmeExamples.swift b/Tests/AsyncMonitorTests/ReadmeExamples.swift index f24242d..2f8a2a8 100644 --- a/Tests/AsyncMonitorTests/ReadmeExamples.swift +++ b/Tests/AsyncMonitorTests/ReadmeExamples.swift @@ -15,7 +15,7 @@ class SimplestVersion { } final class WithContext: Sendable { - nonisolated(unsafe) var cancellables = Set() + nonisolated(unsafe) var cancellables: Set = [] init() { NotificationCenter.default @@ -36,7 +36,7 @@ final class WithContext: Sendable { @preconcurrency import Combine class CombineExample { - var cancellables = Set() + var cancellables: Set = [] init() { Timer.publish(every: 1.0, on: .main, in: .common) @@ -44,15 +44,14 @@ class CombineExample { .values .monitor { date in print("Timer fired at \(date)") - } - .store(in: &cancellables) + }.store(in: &cancellables) } } // MARK: - KVO class KVOExample { - var cancellables = Set() + var cancellables: Set = [] init() { let progress = Progress(totalUnitCount: 42)