mirror of
https://github.com/samsonjs/AsyncMonitor.git
synced 2026-03-25 08:25:47 +00:00
Document everything
This commit is contained in:
parent
52b10585ab
commit
5e8be95547
9 changed files with 466 additions and 50 deletions
|
|
@ -58,7 +58,7 @@ Working with Combine publishers is trivial thanks to [`AnyPublisher.values`][val
|
||||||
@preconcurrency import Combine
|
@preconcurrency import Combine
|
||||||
|
|
||||||
class CombineExample {
|
class CombineExample {
|
||||||
var cancellables = Set<AnyAsyncCancellable>()
|
var cancellables: Set<AnyAsyncCancellable> = []
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
Timer.publish(every: 1.0, on: .main, in: .common)
|
Timer.publish(every: 1.0, on: .main, in: .common)
|
||||||
|
|
@ -66,8 +66,7 @@ class CombineExample {
|
||||||
.values
|
.values
|
||||||
.monitor { date in
|
.monitor { date in
|
||||||
print("Timer fired at \(date)")
|
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
|
```swift
|
||||||
class KVOExample {
|
class KVOExample {
|
||||||
var cancellables = Set<AnyAsyncCancellable>()
|
var cancellables: Set<AnyAsyncCancellable> = []
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let progress = Progress(totalUnitCount: 42)
|
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))%")
|
print("Progress is \(fraction.formatted(.percent))%")
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
/// Type-erasing wrapper for ``AsyncCancellable`` that ties its instance lifetime to cancellation. In other words, when you release
|
/// Type-erasing wrapper for ``AsyncCancellable`` that automatically cancels when deallocated.
|
||||||
/// an instance of ``AnyAsyncCancellable`` and it's deallocated then it automatically cancels its given ``AsyncCancellable``.
|
///
|
||||||
|
/// `AnyAsyncCancellable` provides automatic cancellation when deallocated, making it safe to store
|
||||||
|
/// cancellables without explicitly managing their lifecycle.
|
||||||
|
///
|
||||||
public class AnyAsyncCancellable: AsyncCancellable {
|
public class AnyAsyncCancellable: AsyncCancellable {
|
||||||
lazy var id = ObjectIdentifier(self)
|
lazy var id = ObjectIdentifier(self)
|
||||||
|
|
||||||
let canceller: () -> Void
|
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<AC: AsyncCancellable>(cancellable: AC) {
|
public init<AC: AsyncCancellable>(cancellable: AC) {
|
||||||
canceller = { cancellable.cancel() }
|
canceller = { cancellable.cancel() }
|
||||||
}
|
}
|
||||||
|
|
@ -15,6 +24,7 @@ public class AnyAsyncCancellable: AsyncCancellable {
|
||||||
|
|
||||||
// MARK: AsyncCancellable conformance
|
// MARK: AsyncCancellable conformance
|
||||||
|
|
||||||
|
/// Cancels the wrapped cancellable. Safe to call multiple times and automatically called on deallocation.
|
||||||
public func cancel() {
|
public func cancel() {
|
||||||
canceller()
|
canceller()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
/// Represents an async operation that can be cancelled.
|
/// 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 {
|
public protocol AsyncCancellable: AnyObject, Hashable {
|
||||||
/// Cancels the operation.
|
/// Cancels the operation. Safe to call multiple times.
|
||||||
func cancel()
|
func cancel()
|
||||||
|
|
||||||
/// Stores this cancellable in the given set, using the type-erasing wrapper ``AnyAsyncCancellable``. This method has a
|
/// Stores this cancellable in the given set using ``AnyAsyncCancellable``.
|
||||||
/// default implementation and you typically shouldn't need to override it.
|
///
|
||||||
|
/// - Parameter set: The set to store the wrapped cancellable in.
|
||||||
func store(in set: inout Set<AnyAsyncCancellable>)
|
func store(in set: inout Set<AnyAsyncCancellable>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
112
Sources/AsyncMonitor/AsyncMonitor.docc/AsyncMonitor.md
Normal file
112
Sources/AsyncMonitor/AsyncMonitor.docc/AsyncMonitor.md
Normal file
|
|
@ -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<AnyAsyncCancellable> = []
|
||||||
|
|
||||||
|
sequence.monitor { element in
|
||||||
|
// Handle element
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context-Aware Monitoring
|
||||||
|
|
||||||
|
Prevent retain cycles with weak context:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class DataController {
|
||||||
|
var cancellables: Set<AnyAsyncCancellable> = []
|
||||||
|
|
||||||
|
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:)``
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
/// A monitor that observes an asynchronous sequence and invokes the given block for each received element.
|
/// 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
|
/// `AsyncMonitor` wraps the observation of an async sequence in a `Task`, providing automatic cancellation
|
||||||
/// something sendable before calling `monitor` on the sequence. e.g.
|
/// and memory management. Elements must be `Sendable`. For notifications, map to something sendable:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```swift
|
||||||
/// NotificationCenter.default
|
/// NotificationCenter.default
|
||||||
/// .notifications(named: .NSCalendarDayChanged)
|
/// .notifications(named: .NSCalendarDayChanged)
|
||||||
/// .map(\.name)
|
/// .map(\.name)
|
||||||
/// .monitor { _ in whatever() }
|
/// .monitor { _ in print("Day changed!") }
|
||||||
/// .store(in: &cancellables)
|
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// On iOS 18+, preserves the caller's actor isolation context by default.
|
||||||
|
///
|
||||||
public final class AsyncMonitor: Hashable, AsyncCancellable {
|
public final class AsyncMonitor: Hashable, AsyncCancellable {
|
||||||
let task: Task<Void, Never>
|
let task: Task<Void, Never>
|
||||||
|
|
||||||
/// 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:
|
/// - Parameters:
|
||||||
/// - isolation: An optional actor isolation context to inherit.
|
/// - isolation: An optional actor isolation context to inherit. Defaults to `#isolation`.
|
||||||
/// Defaults to `#isolation`, preserving the caller's actor isolation.
|
|
||||||
/// - sequence: The asynchronous sequence of elements to observe.
|
/// - sequence: The asynchronous sequence of elements to observe.
|
||||||
/// - block: A closure to execute for each element yielded by the sequence.
|
/// - block: A closure to execute for each element yielded by the sequence.
|
||||||
@available(iOS 18, macOS 15, *)
|
@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:
|
/// - 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.
|
/// - block: A closure to execute for each element yielded by the sequence.
|
||||||
|
@available(iOS 18, macOS 15, *)
|
||||||
|
public init<Element: Sendable, Sequence: AsyncSequence>(
|
||||||
|
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(iOS, introduced: 17, obsoleted: 18)
|
||||||
@available(macOS, introduced: 14, obsoleted: 15)
|
@available(macOS, introduced: 14, obsoleted: 15)
|
||||||
public init<Element: Sendable, Sequence>(
|
public init<Element: Sendable, Sequence>(
|
||||||
|
|
@ -65,7 +91,7 @@ public final class AsyncMonitor: Hashable, AsyncCancellable {
|
||||||
|
|
||||||
// MARK: AsyncCancellable conformance
|
// 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() {
|
public func cancel() {
|
||||||
task.cancel()
|
task.cancel()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,35 @@
|
||||||
public extension AsyncSequence where Element: Sendable, Failure == Never {
|
public extension AsyncSequence where Element: Sendable, Failure == Never {
|
||||||
/// 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.
|
||||||
///
|
///
|
||||||
/// 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:
|
/// - 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`,
|
||||||
/// - block: A closure that's executed with each yielded element.
|
/// 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<AnyAsyncCancellable> = []
|
||||||
|
///
|
||||||
|
/// 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(
|
func monitor(
|
||||||
isolation: isolated (any Actor)? = #isolation,
|
isolation: isolated (any Actor)? = #isolation,
|
||||||
_ block: @escaping (Element) async -> Void
|
_ block: @escaping (Element) async -> Void
|
||||||
|
|
@ -14,15 +38,120 @@ public extension AsyncSequence where Element: Sendable, Failure == Never {
|
||||||
AsyncMonitor(isolation: isolation, sequence: self, performing: block)
|
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
|
/// Observes the elements yielded by this sequence and executes the given closure with each element and the weakly-captured context object.
|
||||||
/// 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:
|
/// - 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`,
|
||||||
/// - context: The object to capture weakly for use within the closure.
|
/// preserving the caller's actor isolation.
|
||||||
/// - block: A closure that's executed with each yielded element, and the `context`.
|
/// - 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<AnyAsyncCancellable> = []
|
||||||
|
///
|
||||||
|
/// 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<Context: AnyObject>(
|
||||||
|
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<AnyAsyncCancellable> = []
|
||||||
|
///
|
||||||
|
/// func startMonitoring() {
|
||||||
|
/// notificationStream
|
||||||
|
/// .monitor(context: self) { manager, notification in
|
||||||
|
/// manager.handleNotification(notification)
|
||||||
|
/// }.store(in: &cancellables)
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
func monitor<Context: AnyObject>(
|
func monitor<Context: AnyObject>(
|
||||||
isolation: isolated (any Actor)? = #isolation,
|
isolation: isolated (any Actor)? = #isolation,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
@ -38,22 +167,69 @@ public extension AsyncSequence where Element: Sendable, Failure == Never {
|
||||||
@available(iOS, introduced: 17, obsoleted: 18)
|
@available(iOS, introduced: 17, obsoleted: 18)
|
||||||
@available(macOS, introduced: 14, obsoleted: 15)
|
@available(macOS, introduced: 14, obsoleted: 15)
|
||||||
public extension AsyncSequence where Self: Sendable, Element: Sendable {
|
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:
|
/// - 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(
|
func monitor(
|
||||||
_ block: @escaping @Sendable (Element) async -> Void
|
_ block: @escaping @Sendable (Element) async -> Void
|
||||||
) -> AsyncMonitor {
|
) -> AsyncMonitor {
|
||||||
AsyncMonitor(sequence: self, performing: block)
|
AsyncMonitor(sequence: self, performing: block)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Observes the elements yielded by this sequence and executes the given closure with each element the weakly-captured
|
/// Observes the elements yielded by this sequence and executes the given closure with each element and the weakly-captured context object (iOS 17 compatibility).
|
||||||
/// context object.
|
///
|
||||||
|
/// 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:
|
/// - Parameters:
|
||||||
/// - context: The object to capture weakly for use within the closure.
|
/// - context: The object to capture weakly for use within the closure. Must be `Sendable` and will be
|
||||||
/// - block: A closure that's executed with each yielded element, and the `context`.
|
/// 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<AnyAsyncCancellable> = []
|
||||||
|
///
|
||||||
|
/// 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: AnyObject & Sendable>(
|
func monitor<Context: AnyObject & Sendable>(
|
||||||
context: Context,
|
context: Context,
|
||||||
_ block: @escaping @Sendable (Context, Element) async -> Void
|
_ block: @escaping @Sendable (Context, Element) async -> Void
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,40 @@ public import Foundation
|
||||||
extension KeyPath: @unchecked @retroactive Sendable where Value: Sendable {}
|
extension KeyPath: @unchecked @retroactive Sendable where Value: Sendable {}
|
||||||
|
|
||||||
public extension NSObjectProtocol where Self: NSObject {
|
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:
|
/// - 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.
|
/// - options: KVO options to use for observation. Defaults to an empty set.
|
||||||
|
/// See `NSKeyValueObservingOptions` for available options.
|
||||||
|
///
|
||||||
|
/// - Returns: An `AsyncStream<Value>` 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<Value: Sendable>(
|
func values<Value: Sendable>(
|
||||||
for keyPath: KeyPath<Self, Value>,
|
for keyPath: KeyPath<Self, Value>,
|
||||||
options: NSKeyValueObservingOptions = []
|
options: NSKeyValueObservingOptions = []
|
||||||
|
|
@ -27,12 +56,48 @@ public extension NSObjectProtocol where Self: NSObject {
|
||||||
|
|
||||||
@available(iOS 18, macOS 15, *)
|
@available(iOS 18, macOS 15, *)
|
||||||
public extension NSObjectProtocol where Self: NSObject {
|
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:
|
/// - 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.
|
/// - 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<AnyAsyncCancellable> = []
|
||||||
|
///
|
||||||
|
/// 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<Value: Sendable>(
|
func monitorValues<Value: Sendable>(
|
||||||
for keyPath: KeyPath<Self, Value>,
|
for keyPath: KeyPath<Self, Value>,
|
||||||
options: NSKeyValueObservingOptions = [],
|
options: NSKeyValueObservingOptions = [],
|
||||||
|
|
@ -46,12 +111,36 @@ public extension NSObjectProtocol where Self: NSObject {
|
||||||
@available(iOS, introduced: 17, obsoleted: 18)
|
@available(iOS, introduced: 17, obsoleted: 18)
|
||||||
@available(macOS, introduced: 14, obsoleted: 15)
|
@available(macOS, introduced: 14, obsoleted: 15)
|
||||||
public extension NSObjectProtocol where Self: NSObject {
|
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:
|
/// - 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.
|
/// - 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<AnyAsyncCancellable> = []
|
||||||
|
///
|
||||||
|
/// 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<Value: Sendable>(
|
func monitorValues<Value: Sendable>(
|
||||||
for keyPath: KeyPath<Self, Value>,
|
for keyPath: KeyPath<Self, Value>,
|
||||||
options: NSKeyValueObservingOptions = [],
|
options: NSKeyValueObservingOptions = [],
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
@MainActor class AsyncCancellableTests {
|
@MainActor class AsyncCancellableTests {
|
||||||
var cancellables = Set<AnyAsyncCancellable>()
|
var cancellables: Set<AnyAsyncCancellable> = []
|
||||||
|
|
||||||
@Test func storeInsertsIntoSetAndKeepsSubjectAlive() throws {
|
@Test func storeInsertsIntoSetAndKeepsSubjectAlive() throws {
|
||||||
var subject: TestCancellable? = TestCancellable()
|
var subject: TestCancellable? = TestCancellable()
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class SimplestVersion {
|
||||||
}
|
}
|
||||||
|
|
||||||
final class WithContext: Sendable {
|
final class WithContext: Sendable {
|
||||||
nonisolated(unsafe) var cancellables = Set<AnyAsyncCancellable>()
|
nonisolated(unsafe) var cancellables: Set<AnyAsyncCancellable> = []
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
NotificationCenter.default
|
NotificationCenter.default
|
||||||
|
|
@ -36,7 +36,7 @@ final class WithContext: Sendable {
|
||||||
@preconcurrency import Combine
|
@preconcurrency import Combine
|
||||||
|
|
||||||
class CombineExample {
|
class CombineExample {
|
||||||
var cancellables = Set<AnyAsyncCancellable>()
|
var cancellables: Set<AnyAsyncCancellable> = []
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
Timer.publish(every: 1.0, on: .main, in: .common)
|
Timer.publish(every: 1.0, on: .main, in: .common)
|
||||||
|
|
@ -44,15 +44,14 @@ class CombineExample {
|
||||||
.values
|
.values
|
||||||
.monitor { date in
|
.monitor { date in
|
||||||
print("Timer fired at \(date)")
|
print("Timer fired at \(date)")
|
||||||
}
|
}.store(in: &cancellables)
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - KVO
|
// MARK: - KVO
|
||||||
|
|
||||||
class KVOExample {
|
class KVOExample {
|
||||||
var cancellables = Set<AnyAsyncCancellable>()
|
var cancellables: Set<AnyAsyncCancellable> = []
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let progress = Progress(totalUnitCount: 42)
|
let progress = Progress(totalUnitCount: 42)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue