Skip to content

Commit ef4083d

Browse files
committed
Add support for iOS 17 and macOS 14
1 parent 77130ba commit ef4083d

File tree

5 files changed

+92
-17
lines changed

5 files changed

+92
-17
lines changed

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import PackageDescription
55
let package = Package(
66
name: "AsyncMonitor",
77
platforms: [
8-
.iOS(.v18),
9-
.macOS(.v15),
8+
.iOS(.v17),
9+
.macOS(.v14),
1010
],
1111
products: [
1212
.library(

Sources/AsyncMonitor/AsyncMonitor.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public final class AsyncMonitor: Hashable, AsyncCancellable {
2020
/// Defaults to `#isolation`, preserving the caller's actor isolation.
2121
/// - sequence: The asynchronous sequence of elements to observe.
2222
/// - block: A closure to execute for each element yielded by the sequence.
23+
@available(iOS 18, *)
2324
public init<Element: Sendable>(
2425
isolation: isolated (any Actor)? = #isolation,
2526
sequence: any AsyncSequence<Element, Never>,
@@ -34,6 +35,29 @@ public final class AsyncMonitor: Hashable, AsyncCancellable {
3435
}
3536
}
3637

38+
/// Creates an ``AsyncMonitor`` that observes the provided asynchronous sequence.
39+
///
40+
/// - Parameters:
41+
/// - sequence: The asynchronous sequence of elements to observe.
42+
/// - block: A closure to execute for each element yielded by the sequence.
43+
@available(iOS, introduced: 17, obsoleted: 18)
44+
public init<Element: Sendable, Sequence>(
45+
sequence: sending Sequence,
46+
@_inheritActorContext performing block: @escaping @Sendable (Element) async -> Void
47+
) where Sequence: AsyncSequence, Element == Sequence.Element {
48+
self.task = Task {
49+
do {
50+
for try await element in sequence {
51+
await block(element)
52+
}
53+
} catch {
54+
guard !Task.isCancelled else { return }
55+
56+
print("Error iterating over sequence: \(error)")
57+
}
58+
}
59+
}
60+
3761
deinit {
3862
cancel()
3963
}

Sources/AsyncMonitor/AsyncSequence+Extensions.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@available(iOS 18, *)
12
public extension AsyncSequence where Element: Sendable, Failure == Never {
23
/// Observes the elements yielded by this sequence and executes the given closure with each element.
34
///
@@ -33,3 +34,32 @@ public extension AsyncSequence where Element: Sendable, Failure == Never {
3334
}
3435
}
3536
}
37+
38+
@available(iOS, introduced: 17, obsoleted: 18)
39+
public extension AsyncSequence where Self: Sendable, Element: Sendable {
40+
/// Observes the elements yielded by this sequence and executes the given closure with each element.
41+
///
42+
/// - Parameters:
43+
/// - block: A closure that's executed with each yielded element.
44+
func monitor(
45+
_ block: @escaping @Sendable (Element) async -> Void
46+
) -> AsyncMonitor {
47+
AsyncMonitor(sequence: self, performing: block)
48+
}
49+
50+
/// Observes the elements yielded by this sequence and executes the given closure with each element the weakly-captured
51+
/// context object.
52+
///
53+
/// - Parameters:
54+
/// - context: The object to capture weakly for use within the closure.
55+
/// - block: A closure that's executed with each yielded element, and the `context`.
56+
func monitor<Context: AnyObject & Sendable>(
57+
context: Context,
58+
_ block: @escaping @Sendable (Context, Element) async -> Void
59+
) -> AsyncMonitor {
60+
AsyncMonitor(sequence: self) { [weak context] element in
61+
guard let context else { return }
62+
await block(context, element)
63+
}
64+
}
65+
}

Sources/AsyncMonitor/NSObject+AsyncKVO.swift

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,30 @@ public import Foundation
22

33
extension KeyPath: @unchecked @retroactive Sendable where Value: Sendable {}
44

5+
public extension NSObjectProtocol where Self: NSObject {
6+
/// Returns an `AsyncSequence` of `Value`s for all changes to the given key path on this object.
7+
///
8+
/// - Parameters:
9+
/// - keyPath: The key path to observe on this object. The value must be `Sendable`.
10+
/// - options: KVO options to use for observation. Defaults to an empty set.
11+
func values<Value: Sendable>(
12+
for keyPath: KeyPath<Self, Value>,
13+
options: NSKeyValueObservingOptions = []
14+
) -> AsyncStream<Value> {
15+
let (stream, continuation) = AsyncStream<Value>.makeStream()
16+
let token: NSKeyValueObservation? = self.observe(keyPath, options: options) { object, _ in
17+
continuation.yield(object[keyPath: keyPath])
18+
}
19+
// A nice side-effect of this is that the stream retains the token automatically.
20+
let locker = ValueLocker(value: token)
21+
continuation.onTermination = { _ in
22+
locker.modify { $0 = nil }
23+
}
24+
return stream
25+
}
26+
}
27+
28+
@available(iOS 18, *)
529
public extension NSObjectProtocol where Self: NSObject {
630
/// Observes changes to the specified key path on the object and asynchronously yields each value. Values must be `Sendable`.
731
///
@@ -17,25 +41,22 @@ public extension NSObjectProtocol where Self: NSObject {
1741
values(for: keyPath, options: options)
1842
.monitor(changeHandler)
1943
}
44+
}
2045

21-
/// Returns an `AsyncSequence` of `Value`s for all changes to the given key path on this object.
46+
@available(iOS, introduced: 17, obsoleted: 18)
47+
public extension NSObjectProtocol where Self: NSObject {
48+
/// Observes changes to the specified key path on the object and asynchronously yields each value. Values must be `Sendable`.
2249
///
2350
/// - Parameters:
2451
/// - keyPath: The key path to observe on this object. The value must be `Sendable`.
2552
/// - options: KVO options to use for observation. Defaults to an empty set.
26-
func values<Value: Sendable>(
53+
/// - changeHandler: A closure that's executed with each new value.
54+
func monitorValues<Value: Sendable>(
2755
for keyPath: KeyPath<Self, Value>,
28-
options: NSKeyValueObservingOptions = []
29-
) -> some AsyncSequence<Value, Never> {
30-
let (stream, continuation) = AsyncStream<Value>.makeStream()
31-
let token: NSKeyValueObservation? = self.observe(keyPath, options: options) { object, _ in
32-
continuation.yield(object[keyPath: keyPath])
33-
}
34-
// A nice side-effect of this is that the stream retains the token automatically.
35-
let locker = ValueLocker(value: token)
36-
continuation.onTermination = { _ in
37-
locker.modify { $0 = nil }
38-
}
39-
return stream
56+
options: NSKeyValueObservingOptions = [],
57+
changeHandler: @escaping @Sendable (Value) -> Void
58+
) -> any AsyncCancellable {
59+
values(for: keyPath, options: options)
60+
.monitor(changeHandler)
4061
}
4162
}

Tests/AsyncMonitorTests/NSObject+AsyncKVOTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class AsyncKVOTests {
2727
#expect(values.count == total)
2828
}
2929

30-
// It's important that the test or the progress-observing task are not on the same actor, so
30+
// It's important that the test and the progress-observing task are not on the same actor, so
3131
// we make the test @MainActor and observe progress values on another actor. Otherwise it's a
3232
// deadlock.
3333
@Test(.timeLimit(.minutes(1)))

0 commit comments

Comments
 (0)