mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
Write a post about AsyncMonitor and NotificationSmuggler
This commit is contained in:
parent
eba6e2c12f
commit
46a355cfba
1 changed files with 144 additions and 0 deletions
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
Author: Sami Samhuri
|
||||
Title: Type-safe notifications and async stream monitoring with Swift 6
|
||||
Date: 6th June, 2025
|
||||
Timestamp: 2025-06-06T14:27:11-07:00
|
||||
Tags: Swift, iOS, notifications, async, concurrency, AsyncMonitor, NotificationSmuggler
|
||||
---
|
||||
|
||||
Swift 6 concurrency checking made handling notifications without warnings kinda tedious. The old Combine approach doesn't work with `@Sendable` closures and manually managing tasks gets repetitive. I made a couple of tiny Swift packages to help out with the situation: [AsyncMonitor](https://github.com/samsonjs/AsyncMonitor) which wraps task management, and [NotificationSmuggler](https://github.com/samsonjs/NotificationSmuggler) which adds a type-safe interface on top of `Notification` and `NotificationCenter`.
|
||||
|
||||
## The manual approach
|
||||
|
||||
Here's what I was writing over and over:
|
||||
|
||||
```swift
|
||||
// Causes Sendable warnings in Swift 6
|
||||
let task = Task { [weak self] in
|
||||
for await notification in NotificationCenter.default.notifications(named: .NSCalendarDayChanged) {
|
||||
guard let self else { return }
|
||||
await self.handleDayChange()
|
||||
}
|
||||
}
|
||||
// Store it somewhere, hope you remember to cancel it...
|
||||
```
|
||||
|
||||
And when you have a bunch of these then you wind up with lots of properties to track them and other boilerplate.
|
||||
|
||||
## AsyncMonitor
|
||||
|
||||
[AsyncMonitor](https://github.com/samsonjs/AsyncMonitor) wraps the task lifecycle. Instead of managing tasks manually you use streams like Combine publishers:
|
||||
|
||||
```swift
|
||||
import AsyncMonitor
|
||||
|
||||
let cancellable = NotificationCenter.default
|
||||
.notifications(named: .NSCalendarDayChanged)
|
||||
.map(\.name)
|
||||
.monitor { _ in
|
||||
print("The date is now \(Date.now)")
|
||||
}
|
||||
```
|
||||
|
||||
The `monitor` method creates a `Task` internally and handles cleanup when the cancellable goes out of scope. The closure is async so you can await in it.
|
||||
|
||||
You're free to do the usual `[weak self]` and `guard let self else { return }` dance, but there's a variant that accepts a context parameter that's automatically weakified. Your closure receives a strong reference:
|
||||
|
||||
```swift
|
||||
NotificationCenter.default
|
||||
.notifications(named: .NSCalendarDayChanged)
|
||||
.map(\.name)
|
||||
.monitor(context: self) { _self, _ in
|
||||
_self.dayChanged()
|
||||
}.store(in: &cancellables)
|
||||
```
|
||||
|
||||
The monitor finishes automatically after finding a nil context.
|
||||
|
||||
### KVO
|
||||
|
||||
Sometimes you need KVO and the old ways are best. There's a KVO extension that bridges to async sequences:
|
||||
|
||||
```swift
|
||||
// AVPlayer's Combine publisher for rate doesn't publish all the values
|
||||
player.monitorValues(for: \.rate) { rate in
|
||||
print("Player rate: \(rate)")
|
||||
}.store(in: &cancellables)
|
||||
```
|
||||
|
||||
### What about Combine?
|
||||
|
||||
Combine works but doesn't mesh well with Swift 6 concurrency. The `@Sendable` requirements make it annoying. You can write the Task code manually but it gets repetitive when you have a lot of observers.
|
||||
|
||||
## NotificationSmuggler
|
||||
|
||||
[NotificationSmuggler](https://github.com/samsonjs/NotificationSmuggler) solves a different problem: type-safe notifications. No more dumpster diving in `userInfo`.
|
||||
|
||||
Define your contraband:
|
||||
|
||||
```swift
|
||||
import NotificationSmuggler
|
||||
|
||||
struct ProjectExportComplete: Smuggled, Sendable {
|
||||
let projectID: String
|
||||
let outputURL: URL
|
||||
}
|
||||
```
|
||||
|
||||
The `Smuggled` protocol generates a unique notification name and userInfo key from your type name.
|
||||
|
||||
Smuggle your illicit goods like so:
|
||||
|
||||
```swift
|
||||
NotificationCenter.default.smuggle(ProjectExportComplete(projectID: "project-123", outputURL: exportURL))
|
||||
|
||||
// which is short for
|
||||
|
||||
NotificationCenter.default.post(.smuggle(ProjectExportComplete(projectID: "project-123", outputURL: exportURL)))
|
||||
```
|
||||
|
||||
And on the other side it's as easy as any other notification using an extension on `NotificationCenter`:
|
||||
|
||||
```swift
|
||||
for await notification in NotificationCenter.default.notifications(for: ProjectExportComplete.self) {
|
||||
print("Project \(notification.projectID) exported to \(notification.outputURL)")
|
||||
}
|
||||
```
|
||||
|
||||
## Using them together
|
||||
|
||||
They work well together. Here's a more full example:
|
||||
|
||||
```swift
|
||||
import AsyncMonitor
|
||||
import NotificationSmuggler
|
||||
|
||||
struct BackupCompleteNotification: Smuggled, Sendable {
|
||||
let success: Bool
|
||||
let totalSize: Int64
|
||||
}
|
||||
|
||||
NotificationCenter.default
|
||||
.notifications(for: BackupCompleteNotification.self)
|
||||
.monitor(context: self) { _self, notification in
|
||||
_self.updateBackupStatus(success: notification.success)
|
||||
}.store(in: &cancellables)
|
||||
|
||||
// elsewhere
|
||||
|
||||
NotificationCenter.default.smuggle(BackupCompleteNotification(success: true, totalSize: 42))
|
||||
```
|
||||
|
||||
## That's it
|
||||
|
||||
Both libraries are small and focused. AsyncMonitor is about 100 lines, NotificationSmuggler is smaller. Zero dependencies.
|
||||
|
||||
AsyncMonitor requires iOS 17. It supports both iOS 17 and iOS 18 with different initializers due to changes in inheriting actor isolation.
|
||||
|
||||
- [AsyncMonitor on GitHub](https://github.com/samsonjs/AsyncMonitor)
|
||||
- [AsyncMonitor on Swift Package Index](https://swiftpackageindex.com/samsonjs/AsyncMonitor)
|
||||
|
||||
NotificationSmuggler requires iOS 17.
|
||||
|
||||
- [NotificationSmuggler on GitHub](https://github.com/samsonjs/NotificationSmuggler)
|
||||
- [NotificationSmuggler on Swift Package Index](https://swiftpackageindex.com/samsonjs/NotificationSmuggler)
|
||||
Loading…
Reference in a new issue