Add support for iOS 17 and macOS 14

This commit is contained in:
Sami Samhuri 2025-05-25 17:43:01 -07:00
parent 77130baceb
commit ef4083dc64
No known key found for this signature in database
5 changed files with 92 additions and 17 deletions

View file

@ -5,8 +5,8 @@ import PackageDescription
let package = Package( let package = Package(
name: "AsyncMonitor", name: "AsyncMonitor",
platforms: [ platforms: [
.iOS(.v18), .iOS(.v17),
.macOS(.v15), .macOS(.v14),
], ],
products: [ products: [
.library( .library(

View file

@ -20,6 +20,7 @@ public final class AsyncMonitor: Hashable, AsyncCancellable {
/// Defaults to `#isolation`, preserving the caller's actor 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, *)
public init<Element: Sendable>( public init<Element: Sendable>(
isolation: isolated (any Actor)? = #isolation, isolation: isolated (any Actor)? = #isolation,
sequence: any AsyncSequence<Element, Never>, sequence: any AsyncSequence<Element, Never>,
@ -34,6 +35,29 @@ public final class AsyncMonitor: Hashable, AsyncCancellable {
} }
} }
/// Creates an ``AsyncMonitor`` that observes the provided asynchronous sequence.
///
/// - Parameters:
/// - 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)
public init<Element: Sendable, Sequence>(
sequence: sending Sequence,
@_inheritActorContext performing block: @escaping @Sendable (Element) async -> Void
) where Sequence: AsyncSequence, Element == Sequence.Element {
self.task = Task {
do {
for try await element in sequence {
await block(element)
}
} catch {
guard !Task.isCancelled else { return }
print("Error iterating over sequence: \(error)")
}
}
}
deinit { deinit {
cancel() cancel()
} }

View file

@ -1,3 +1,4 @@
@available(iOS 18, *)
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.
/// ///
@ -33,3 +34,32 @@ public extension AsyncSequence where Element: Sendable, Failure == Never {
} }
} }
} }
@available(iOS, introduced: 17, obsoleted: 18)
public extension AsyncSequence where Self: Sendable, Element: Sendable {
/// Observes the elements yielded by this sequence and executes the given closure with each element.
///
/// - Parameters:
/// - block: A closure that's executed with each yielded element.
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.
///
/// - 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`.
func monitor<Context: AnyObject & Sendable>(
context: Context,
_ block: @escaping @Sendable (Context, Element) async -> Void
) -> AsyncMonitor {
AsyncMonitor(sequence: self) { [weak context] element in
guard let context else { return }
await block(context, element)
}
}
}

View file

@ -2,6 +2,30 @@ 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 {
/// Returns an `AsyncSequence` of `Value`s for all changes to the given key path on this object.
///
/// - Parameters:
/// - keyPath: The key path to observe on this object. The value must be `Sendable`.
/// - options: KVO options to use for observation. Defaults to an empty set.
func values<Value: Sendable>(
for keyPath: KeyPath<Self, Value>,
options: NSKeyValueObservingOptions = []
) -> AsyncStream<Value> {
let (stream, continuation) = AsyncStream<Value>.makeStream()
let token: NSKeyValueObservation? = self.observe(keyPath, options: options) { object, _ in
continuation.yield(object[keyPath: keyPath])
}
// A nice side-effect of this is that the stream retains the token automatically.
let locker = ValueLocker(value: token)
continuation.onTermination = { _ in
locker.modify { $0 = nil }
}
return stream
}
}
@available(iOS 18, *)
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 asynchronously yields each value. Values must be `Sendable`.
/// ///
@ -17,25 +41,22 @@ public extension NSObjectProtocol where Self: NSObject {
values(for: keyPath, options: options) values(for: keyPath, options: options)
.monitor(changeHandler) .monitor(changeHandler)
} }
}
/// Returns an `AsyncSequence` of `Value`s for all changes to the given key path on this object. @available(iOS, introduced: 17, obsoleted: 18)
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`.
/// ///
/// - 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 must be `Sendable`.
/// - options: KVO options to use for observation. Defaults to an empty set. /// - options: KVO options to use for observation. Defaults to an empty set.
func values<Value: Sendable>( /// - changeHandler: A closure that's executed with each new value.
func monitorValues<Value: Sendable>(
for keyPath: KeyPath<Self, Value>, for keyPath: KeyPath<Self, Value>,
options: NSKeyValueObservingOptions = [] options: NSKeyValueObservingOptions = [],
) -> some AsyncSequence<Value, Never> { changeHandler: @escaping @Sendable (Value) -> Void
let (stream, continuation) = AsyncStream<Value>.makeStream() ) -> any AsyncCancellable {
let token: NSKeyValueObservation? = self.observe(keyPath, options: options) { object, _ in values(for: keyPath, options: options)
continuation.yield(object[keyPath: keyPath]) .monitor(changeHandler)
}
// A nice side-effect of this is that the stream retains the token automatically.
let locker = ValueLocker(value: token)
continuation.onTermination = { _ in
locker.modify { $0 = nil }
}
return stream
} }
} }

View file

@ -27,7 +27,7 @@ class AsyncKVOTests {
#expect(values.count == total) #expect(values.count == total)
} }
// It's important that the test or the progress-observing task are not on the same actor, so // It's important that the test and the progress-observing task are not on the same actor, so
// we make the test @MainActor and observe progress values on another actor. Otherwise it's a // we make the test @MainActor and observe progress values on another actor. Otherwise it's a
// deadlock. // deadlock.
@Test(.timeLimit(.minutes(1))) @Test(.timeLimit(.minutes(1)))