mirror of
https://github.com/samsonjs/AsyncMonitor.git
synced 2026-04-26 14:47:44 +00:00
Flesh out the readme and add more tests
This commit is contained in:
parent
bc05e17c92
commit
7d1e4564ff
13 changed files with 289 additions and 68 deletions
121
Readme.md
121
Readme.md
|
|
@ -12,6 +12,87 @@ It uses a Swift `Task` to ensure that all resources are properly cleaned up when
|
||||||
|
|
||||||
That's it. It's pretty trivial. I just got tired of writing it over and over, mainly for notifications. You still have to map your `Notification`s to something sendable.
|
That's it. It's pretty trivial. I just got tired of writing it over and over, mainly for notifications. You still have to map your `Notification`s to something sendable.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The simplest example uses a closure that receives the notification. The closure is async so you can await in there if you need to.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import AsyncMonitor
|
||||||
|
|
||||||
|
class SimplestVersion {
|
||||||
|
let cancellable = NotificationCenter.default
|
||||||
|
.notifications(named: .NSCalendarDayChanged)
|
||||||
|
.map(\.name)
|
||||||
|
.monitor { _ in
|
||||||
|
print("The date is now \(Date.now)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This example uses the context parameter to avoid reference cycles with `self`.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class WithContext {
|
||||||
|
var cancellables = Set<AnyAsyncCancellable>()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
NotificationCenter.default
|
||||||
|
.notifications(named: .NSCalendarDayChanged)
|
||||||
|
.map(\.name)
|
||||||
|
.monitor(context: self) { _self, _ in
|
||||||
|
_self.dayChanged()
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dayChanged() {
|
||||||
|
print("The date is now \(Date.now)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combine
|
||||||
|
|
||||||
|
Working with Combine publishers is trivial thanks to [`AnyPublisher.values`][values].
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class CombineExample {
|
||||||
|
var cancellables = Set<AnyAsyncCancellable>()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Timer.publish(every: 1.0, on: .main, in: .common)
|
||||||
|
.autoconnect()
|
||||||
|
.values
|
||||||
|
.monitor { date in
|
||||||
|
print("Timer fired at \(date)")
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[values]: https://developer.apple.com/documentation/combine/anypublisher/values-3s2uy
|
||||||
|
|
||||||
|
### Key-Value Observing (KVO) extension
|
||||||
|
|
||||||
|
When you need to observe an object that uses [KVO][] there's an extension method you can use to monitor it:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class KVOExample {
|
||||||
|
var cancellables = Set<AnyAsyncCancellable>()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let progress = Progress(totalUnitCount: 42)
|
||||||
|
progress.values(for: \.fractionCompleted) { fraction in
|
||||||
|
print("Progress is \(fraction.formatted(.percent))%")
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[KVO]: https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/KVO.html
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
The only way to install this package is with Swift Package Manager (SPM). Please [file a new issue][] or submit a pull-request if you want to use something else.
|
The only way to install this package is with Swift Package Manager (SPM). Please [file a new issue][] or submit a pull-request if you want to use something else.
|
||||||
|
|
@ -36,46 +117,6 @@ When you're integrating this using SPM on its own then add this to the list of d
|
||||||
|
|
||||||
and then add `"AsyncMonitor"` to the list of dependencies in your target as well.
|
and then add `"AsyncMonitor"` to the list of dependencies in your target as well.
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
The simplest example uses a closure that receives the notification:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import AsyncMonitor
|
|
||||||
|
|
||||||
class SimplestVersion {
|
|
||||||
let cancellable = NotificationCenter.default
|
|
||||||
.notifications(named: .NSCalendarDayChanged).map(\.name)
|
|
||||||
.monitor { _ in
|
|
||||||
print("The date is now \(Date.now)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This example uses the context parameter to avoid reference cycles with `self`:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import AsyncMonitor
|
|
||||||
|
|
||||||
class WithContext {
|
|
||||||
var cancellables = Set<AnyAsyncCancellable>()
|
|
||||||
|
|
||||||
init() {
|
|
||||||
NotificationCenter.default
|
|
||||||
.notifications(named: .NSCalendarDayChanged).map(\.name)
|
|
||||||
.monitor(context: self) { _self, _ in
|
|
||||||
_self.dayChanged()
|
|
||||||
}.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dayChanged() {
|
|
||||||
print("The date is now \(Date.now)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The closure is async so you can await in there if you need to.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2025 [Sami Samhuri](https://samhuri.net) <sami@samhuri.net>. Released under the terms of the [MIT License][MIT].
|
Copyright © 2025 [Sami Samhuri](https://samhuri.net) <sami@samhuri.net>. Released under the terms of the [MIT License][MIT].
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
/// Type-erasing wrapper for ``AsyncCancellable`` that ties its instance lifetime to cancellation. In other words, when you release
|
/// 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``.
|
/// an instance of ``AnyAsyncCancellable`` and it's deallocated then it automatically cancels its given ``AsyncCancellable``.
|
||||||
public class AnyAsyncCancellable: AsyncCancellable {
|
public class AnyAsyncCancellable: AsyncCancellable {
|
||||||
|
lazy var id = ObjectIdentifier(self)
|
||||||
|
|
||||||
let canceller: () -> Void
|
let canceller: () -> Void
|
||||||
|
|
||||||
public init<AC: AsyncCancellable>(cancellable: AC) {
|
public init<AC: AsyncCancellable>(cancellable: AC) {
|
||||||
|
|
@ -16,4 +18,14 @@ public class AnyAsyncCancellable: AsyncCancellable {
|
||||||
public func cancel() {
|
public func cancel() {
|
||||||
canceller()
|
canceller()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Hashable conformance
|
||||||
|
|
||||||
|
public static func == (lhs: AnyAsyncCancellable, rhs: AnyAsyncCancellable) -> Bool {
|
||||||
|
lhs.id == rhs.id
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,3 @@ public extension AsyncCancellable {
|
||||||
set.insert(AnyAsyncCancellable(cancellable: self))
|
set.insert(AnyAsyncCancellable(cancellable: self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Hashable conformance
|
|
||||||
|
|
||||||
public extension AsyncCancellable {
|
|
||||||
static func == (lhs: Self, rhs: Self) -> Bool {
|
|
||||||
ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(ObjectIdentifier(self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// NotificationCenter.default
|
/// NotificationCenter.default
|
||||||
/// .notifications(named: .NSCalendarDayChanged).map(\.name)
|
/// .notifications(named: .NSCalendarDayChanged)
|
||||||
|
/// .map(\.name)
|
||||||
/// .monitor { _ in whatever() }
|
/// .monitor { _ in whatever() }
|
||||||
/// .store(in: &cancellables)
|
/// .store(in: &cancellables)
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -43,4 +44,14 @@ public final class AsyncMonitor: Hashable, AsyncCancellable {
|
||||||
public func cancel() {
|
public func cancel() {
|
||||||
task.cancel()
|
task.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Hashable conformance
|
||||||
|
|
||||||
|
public static func == (lhs: AsyncMonitor, rhs: AsyncMonitor) -> Bool {
|
||||||
|
lhs.task == rhs.task
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(task)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,12 @@ public extension NSObjectProtocol where Self: NSObject {
|
||||||
let token = self.observe(keyPath, options: options) { object, _ in
|
let token = self.observe(keyPath, options: options) { object, _ in
|
||||||
continuation.yield(object[keyPath: keyPath])
|
continuation.yield(object[keyPath: keyPath])
|
||||||
}
|
}
|
||||||
|
let locker = TokenLocker(token: token)
|
||||||
|
continuation.onTermination = { _ in
|
||||||
|
locker.clear()
|
||||||
|
}
|
||||||
return stream.monitor { value in
|
return stream.monitor { value in
|
||||||
_ = token // keep this alive
|
_ = locker // keep this alive
|
||||||
changeHandler(value)
|
changeHandler(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
Sources/AsyncMonitor/TokenLocker.swift
Normal file
16
Sources/AsyncMonitor/TokenLocker.swift
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class TokenLocker: @unchecked Sendable {
|
||||||
|
private let lock = NSLock()
|
||||||
|
private var unsafeToken: NSKeyValueObservation?
|
||||||
|
|
||||||
|
init(token: NSKeyValueObservation) {
|
||||||
|
unsafeToken = token
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear() {
|
||||||
|
lock.withLock {
|
||||||
|
unsafeToken = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Tests/AsyncMonitorTests/AnyAsyncCancellableTests.swift
Normal file
22
Tests/AsyncMonitorTests/AnyAsyncCancellableTests.swift
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
@testable import AsyncMonitor
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@MainActor class AnyAsyncCancellableTests {
|
||||||
|
var subject: AnyAsyncCancellable!
|
||||||
|
|
||||||
|
@Test func cancelsWhenReleased() {
|
||||||
|
let cancellable = TestCancellable()
|
||||||
|
subject = AnyAsyncCancellable(cancellable: cancellable)
|
||||||
|
#expect(!cancellable.isCancelled)
|
||||||
|
subject = nil
|
||||||
|
#expect(cancellable.isCancelled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func cancelsWhenCancelled() {
|
||||||
|
let cancellable = TestCancellable()
|
||||||
|
subject = AnyAsyncCancellable(cancellable: cancellable)
|
||||||
|
#expect(!cancellable.isCancelled)
|
||||||
|
subject.cancel()
|
||||||
|
#expect(cancellable.isCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Tests/AsyncMonitorTests/AsyncCancellableTests.swift
Normal file
17
Tests/AsyncMonitorTests/AsyncCancellableTests.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
@testable import AsyncMonitor
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@MainActor class AsyncCancellableTests {
|
||||||
|
var cancellables = Set<AnyAsyncCancellable>()
|
||||||
|
|
||||||
|
@Test func storeInsertsIntoSetAndKeepsSubjectAlive() throws {
|
||||||
|
var subject: TestCancellable? = TestCancellable()
|
||||||
|
weak var weakSubject: TestCancellable? = subject
|
||||||
|
try #require(subject).store(in: &cancellables)
|
||||||
|
#expect(cancellables.count == 1)
|
||||||
|
subject = nil
|
||||||
|
#expect(weakSubject != nil)
|
||||||
|
cancellables.removeAll()
|
||||||
|
#expect(weakSubject == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,10 +10,12 @@ class AsyncMonitorTests {
|
||||||
|
|
||||||
@Test func callsBlockWhenNotificationsArePosted() async throws {
|
@Test func callsBlockWhenNotificationsArePosted() async throws {
|
||||||
await withCheckedContinuation { [center, name] continuation in
|
await withCheckedContinuation { [center, name] continuation in
|
||||||
subject = center.notifications(named: name).map(\.name).monitor { receivedName in
|
subject = center.notifications(named: name)
|
||||||
#expect(name == receivedName)
|
.map(\.name)
|
||||||
continuation.resume()
|
.monitor { receivedName in
|
||||||
}
|
#expect(name == receivedName)
|
||||||
|
continuation.resume()
|
||||||
|
}
|
||||||
Task {
|
Task {
|
||||||
center.post(name: name, object: nil)
|
center.post(name: name, object: nil)
|
||||||
}
|
}
|
||||||
|
|
@ -21,9 +23,11 @@ class AsyncMonitorTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func doesNotCallBlockWhenOtherNotificationsArePosted() async throws {
|
@Test func doesNotCallBlockWhenOtherNotificationsArePosted() async throws {
|
||||||
subject = center.notifications(named: name).map(\.name).monitor { receivedName in
|
subject = center.notifications(named: name)
|
||||||
Issue.record("Called for irrelevant notification \(receivedName)")
|
.map(\.name)
|
||||||
}
|
.monitor { receivedName in
|
||||||
|
Issue.record("Called for irrelevant notification \(receivedName)")
|
||||||
|
}
|
||||||
Task { [center] in
|
Task { [center] in
|
||||||
center.post(name: Notification.Name("something else"), object: nil)
|
center.post(name: Notification.Name("something else"), object: nil)
|
||||||
}
|
}
|
||||||
|
|
@ -31,9 +35,11 @@ class AsyncMonitorTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test @MainActor func stopsCallingBlockWhenDeallocated() async throws {
|
@Test @MainActor func stopsCallingBlockWhenDeallocated() async throws {
|
||||||
subject = center.notifications(named: name).map(\.name).monitor { _ in
|
subject = center.notifications(named: name)
|
||||||
Issue.record("Called after deallocation")
|
.map(\.name)
|
||||||
}
|
.monitor { _ in
|
||||||
|
Issue.record("Called after deallocation")
|
||||||
|
}
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
subject = nil
|
subject = nil
|
||||||
|
|
@ -51,7 +57,8 @@ class AsyncMonitorTests {
|
||||||
init(center: NotificationCenter, deinitHook: @escaping () -> Void) {
|
init(center: NotificationCenter, deinitHook: @escaping () -> Void) {
|
||||||
self.deinitHook = deinitHook
|
self.deinitHook = deinitHook
|
||||||
let name = Notification.Name("irrelevant name")
|
let name = Notification.Name("irrelevant name")
|
||||||
cancellable = center.notifications(named: name).map(\.name)
|
cancellable = center.notifications(named: name)
|
||||||
|
.map(\.name)
|
||||||
.monitor(context: self) { _, _ in }
|
.monitor(context: self) { _, _ in }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,7 +80,8 @@ class AsyncMonitorTests {
|
||||||
|
|
||||||
@Test func stopsCallingBlockWhenContextIsDeallocated() async throws {
|
@Test func stopsCallingBlockWhenContextIsDeallocated() async throws {
|
||||||
var context: NSObject? = NSObject()
|
var context: NSObject? = NSObject()
|
||||||
subject = center.notifications(named: name).map(\.name)
|
subject = center.notifications(named: name)
|
||||||
|
.map(\.name)
|
||||||
.monitor(context: context!) { context, receivedName in
|
.monitor(context: context!) { context, receivedName in
|
||||||
Issue.record("Called after context was deallocated")
|
Issue.record("Called after context was deallocated")
|
||||||
}
|
}
|
||||||
|
|
@ -83,4 +91,21 @@ class AsyncMonitorTests {
|
||||||
}
|
}
|
||||||
try await Task.sleep(for: .milliseconds(10))
|
try await Task.sleep(for: .milliseconds(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func equatable() throws {
|
||||||
|
let subject = AsyncMonitor(sequence: AsyncStream.just(42)) { _ in }
|
||||||
|
#expect(subject == subject)
|
||||||
|
#expect(subject != AsyncMonitor(sequence: AsyncStream.just(42)) { _ in })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func hashable() throws {
|
||||||
|
let subjects = (1...100).map { _ in
|
||||||
|
AsyncMonitor(sequence: AsyncStream.just(42)) { _ in }
|
||||||
|
}
|
||||||
|
var hashValues: Set<Int> = []
|
||||||
|
for subject in subjects {
|
||||||
|
hashValues.insert(subject.hashValue)
|
||||||
|
}
|
||||||
|
#expect(hashValues.count == subjects.count)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
Tests/AsyncMonitorTests/AsyncSequence+Just.swift
Normal file
9
Tests/AsyncMonitorTests/AsyncSequence+Just.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension AsyncSequence where Element: Sendable {
|
||||||
|
static func just(_ value: Element) -> AsyncStream<Element> {
|
||||||
|
AsyncStream { continuation in
|
||||||
|
continuation.yield(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Tests/AsyncMonitorTests/NSObject+AsyncKVOTests.swift
Normal file
21
Tests/AsyncMonitorTests/NSObject+AsyncKVOTests.swift
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
@testable import AsyncMonitor
|
||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
class AsyncKVOTests {
|
||||||
|
var subject: Progress? = Progress(totalUnitCount: 42)
|
||||||
|
var cancellable: (any AsyncCancellable)?
|
||||||
|
|
||||||
|
@Test func yieldsChanges() async throws {
|
||||||
|
let subject = try #require(subject)
|
||||||
|
var values = [Double]()
|
||||||
|
cancellable = subject.values(for: \.fractionCompleted) { progress in
|
||||||
|
values.append(progress)
|
||||||
|
}
|
||||||
|
for _ in 1...3 {
|
||||||
|
subject.completedUnitCount += 1
|
||||||
|
await Task.yield()
|
||||||
|
}
|
||||||
|
#expect(values.count == 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
@testable import AsyncMonitor
|
@testable import AsyncMonitor
|
||||||
|
|
||||||
|
// MARK: Basics
|
||||||
|
|
||||||
class SimplestVersion {
|
class SimplestVersion {
|
||||||
let cancellable = NotificationCenter.default
|
let cancellable = NotificationCenter.default
|
||||||
.notifications(named: .NSCalendarDayChanged).map(\.name)
|
.notifications(named: .NSCalendarDayChanged)
|
||||||
|
.map(\.name)
|
||||||
.monitor { _ in
|
.monitor { _ in
|
||||||
print("The date is now \(Date.now)")
|
print("The date is now \(Date.now)")
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +17,8 @@ class WithContext {
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
NotificationCenter.default
|
NotificationCenter.default
|
||||||
.notifications(named: .NSCalendarDayChanged).map(\.name)
|
.notifications(named: .NSCalendarDayChanged)
|
||||||
|
.map(\.name)
|
||||||
.monitor(context: self) { _self, _ in
|
.monitor(context: self) { _self, _ in
|
||||||
_self.dayChanged()
|
_self.dayChanged()
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
|
|
@ -24,3 +28,34 @@ class WithContext {
|
||||||
print("The date is now \(Date.now)")
|
print("The date is now \(Date.now)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Combine
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class CombineExample {
|
||||||
|
var cancellables = Set<AnyAsyncCancellable>()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Timer.publish(every: 1.0, on: .main, in: .common)
|
||||||
|
.autoconnect()
|
||||||
|
.values
|
||||||
|
.monitor { date in
|
||||||
|
print("Timer fired at \(date)")
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - KVO
|
||||||
|
|
||||||
|
class KVOExample {
|
||||||
|
var cancellables = Set<AnyAsyncCancellable>()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let progress = Progress(totalUnitCount: 42)
|
||||||
|
progress.values(for: \.fractionCompleted) { fraction in
|
||||||
|
print("Progress is \(fraction.formatted(.percent))%")
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
20
Tests/AsyncMonitorTests/TestCancellable.swift
Normal file
20
Tests/AsyncMonitorTests/TestCancellable.swift
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import AsyncMonitor
|
||||||
|
|
||||||
|
class TestCancellable: AsyncCancellable {
|
||||||
|
lazy var id = ObjectIdentifier(self)
|
||||||
|
var isCancelled = false
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
isCancelled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Hashable conformance
|
||||||
|
|
||||||
|
public static func == (lhs: TestCancellable, rhs: TestCancellable) -> Bool {
|
||||||
|
lhs.id == rhs.id
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue