Rename to NotificationSmuggler

This commit is contained in:
Sami Samhuri 2025-04-29 08:53:24 -07:00
parent 59effef18f
commit a43dd347dd
No known key found for this signature in database
9 changed files with 148 additions and 143 deletions

View file

@ -4,7 +4,7 @@
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "BetterNotification", name: "NotificationSmuggler",
platforms: [ platforms: [
.iOS(.v18), .iOS(.v18),
.macOS(.v15), .macOS(.v15),
@ -12,17 +12,17 @@ let package = Package(
products: [ products: [
// Products define the executables and libraries a package produces, making them visible to other packages. // Products define the executables and libraries a package produces, making them visible to other packages.
.library( .library(
name: "BetterNotification", name: "NotificationSmuggler",
targets: ["BetterNotification"]), targets: ["NotificationSmuggler"]),
], ],
targets: [ targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite. // Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies. // Targets can depend on other targets in this package and products from dependencies.
.target( .target(
name: "BetterNotification"), name: "NotificationSmuggler"),
.testTarget( .testTarget(
name: "BetterNotificationTests", name: "NotificationSmugglerTests",
dependencies: ["BetterNotification"] dependencies: ["NotificationSmuggler"]
), ),
] ]
) )

View file

@ -1,8 +1,8 @@
# BetterNotification # NotificationSmuggler
[![0 dependencies!](https://0dependencies.dev/0dependencies.svg)](https://0dependencies.dev) [![0 dependencies!](https://0dependencies.dev/0dependencies.svg)](https://0dependencies.dev)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fsamsonjs%2FBetterNotification%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/samsonjs/BetterNotification) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fsamsonjs%2FNotificationSmuggler%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/samsonjs/NotificationSmuggler)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fsamsonjs%2FBetterNotification%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/samsonjs/BetterNotification) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fsamsonjs%2FNotificationSmuggler%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/samsonjs/NotificationSmuggler)
## Overview ## Overview
@ -14,12 +14,9 @@ tktk
## Installation ## Installation
Honestly you should probably just copy [BetterNotification.swift]() into your project.
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.
[BetterNotification.swift]: https://github.com/samsonjs/BetterNotification/blob/main/Sources/BetterNotification/BetterNotification.swift [file a new issue]: https://github.com/samsonjs/NotificationSmuggler/issues/new
[file a new issue]: https://github.com/samsonjs/BetterNotification/issues/new
### Supported Platforms ### Supported Platforms
@ -27,17 +24,17 @@ This package is supported on iOS 16.0+ and macOS 12.0+.
### Xcode ### Xcode
When you're integrating this into an app with Xcode then go to your project's Package Dependencies and enter the URL `https://github.com/samsonjs/BetterNotification` and then go through the usual flow for adding packages. When you're integrating this into an app with Xcode then go to your project's Package Dependencies and enter the URL `https://github.com/samsonjs/NotificationSmuggler` and then go through the usual flow for adding packages.
### Swift Package Manager (SPM) ### Swift Package Manager (SPM)
When you're integrating this using SPM on its own then add this to the list of dependencies your Package.swift file: When you're integrating this using SPM on its own then add this to the list of dependencies your Package.swift file:
```swift ```swift
.package(url: "https://github.com/samsonjs/BetterNotification.git", .upToNextMajor(from: "0.1.0")) .package(url: "https://github.com/samsonjs/NotificationSmuggler.git", .upToNextMajor(from: "0.1.0"))
``` ```
and then add `"BetterNotification"` to the list of dependencies in your target as well. and then add `"NotificationSmuggler"` to the list of dependencies in your target as well.
## License ## License

View file

@ -1,91 +0,0 @@
import Combine
import Foundation
/// A marker protocol for types that represent notifications with associated data. Conforming types gain a default notification name and
/// user info key. They can be used with extension methods like ``NotificationCenter.notifications(for:)`` and
/// ``NotificationCenter.publisher(for:)`` which automatically extract and cast this type from user info.
///
/// When posting better notifications you can use ``Notification.better(:object:)`` to build up a notification with the correct
/// user info automatically.
public protocol BetterNotification {}
public extension BetterNotification {
/// The notification name associated with the conforming type.
///
/// Uses the type's name to create a unique raw value in the format:
/// `"BetterNotification:{SelfType}"`.
static var notificationName: Notification.Name {
Notification.Name(rawValue: "BetterNotification:\(Self.self)")
}
/// The key used in the notification's `userInfo` dictionary to store the notification value.
///
/// Matches the raw value of `notificationName`.
static var userInfoKey: String { notificationName.rawValue }
}
// MARK: -
public extension Notification {
/// Creates a `Notification` instance for the given `BetterNotification` value.
///
/// - Parameters:
/// - betterNotification: The `BetterNotification` value to send.
/// - object: An optional sender object.
/// - Returns: A configured `Notification` with `name`, `object`, and `userInfo`.
static func better<BN: BetterNotification>(
_ betterNotification: BN,
object: Any? = nil
) -> Notification {
Notification(
name: BN.notificationName,
object: object,
userInfo: [BN.userInfoKey: betterNotification]
)
}
/// Extracts a `BetterNotification` value of the specified type from this `Notification`'s `userInfo`.
///
/// - Returns: The extracted `BetterNotification` value when found, otherwise `nil`.
func better<BN: BetterNotification>() -> BN? {
guard let object = userInfo?[BN.userInfoKey] else {
NSLog("[\(BN.self)] Object missing from userInfo[\"\(BN.userInfoKey)\"]")
return nil
}
guard let betterNotification = object as? BN else {
NSLog("[\(BN.self)] Failed to cast \(object) to \(BN.self)")
return nil
}
return betterNotification
}
}
// MARK: -
public extension NotificationCenter {
/// Returns an `AsyncSequence` of notifications of a specific `BetterNotification` type.
///
/// Each element of the sequence is a `BetterNotification` value.
///
/// - Parameter betterType: The `BetterNotification` type to observe..
/// - Returns: An `AsyncSequence` of `BetterNotification` values.
func notifications<BN: BetterNotification>(
for betterType: BN.Type
) -> any AsyncSequence<BN, Never> {
notifications(named: BN.notificationName)
.compactMap { $0.better() }
}
/// Returns a Combine publisher that emits `BetterNotification` values of the given type.
///
/// - Parameter betterType: The `BetterNotification` type to observe.
/// - Returns: A publisher emitting `BetterNotification` values.
func publisher<BN: BetterNotification>(
for betterType: BN.Type
) -> AnyPublisher<BN, Never> {
publisher(for: betterType.notificationName)
.compactMap { $0.better() }
.eraseToAnyPublisher()
}
}

View file

@ -0,0 +1,27 @@
import Foundation
/// A marker protocol for types that represent notifications with associated data. Conforming types gain a default notification name and
/// user info key to facilitate smuggling. They can be used with extension methods like
/// ``NotificationCenter.notifications(for:)`` and ``NotificationCenter.publisher(for:)`` which
/// automatically extract and cast this type from user info.
///
/// If you want to extract the contraband manually you can use the extension method ``Notification.smuggled()``.
///
/// When smuggling notifications you can use ``Notification.smuggle(:object:)`` to build up a notification with the correct
/// user info automatically.
public protocol Smuggled {}
public extension Smuggled {
/// The notification name associated with the conforming type.
///
/// Uses the type's name to create a unique raw value in the format:
/// `"NotificationSmuggler:{SelfType}"`.
static var notificationName: Notification.Name {
Notification.Name(rawValue: "NotificationSmuggler:\(Self.self)")
}
/// The key used in the notification's `userInfo` dictionary to store the notification value.
///
/// Matches the raw value of `notificationName`.
static var userInfoKey: String { notificationName.rawValue }
}

View file

@ -0,0 +1,66 @@
import Combine
import Foundation
public extension Notification {
/// Creates a `Notification` instance that smuggles the given `Smuggled` value.
///
/// - Parameters:
/// - contraband: The `Smuggled` value to send.
/// - object: An optional sender object.
/// - Returns: A configured `Notification` with `name`, `object`, and `userInfo`.
static func smuggle<Contraband: Smuggled>(
_ contraband: Contraband,
object: Any? = nil
) -> Notification {
Notification(
name: Contraband.notificationName,
object: object,
userInfo: [Contraband.userInfoKey: contraband]
)
}
/// Extracts a `Smuggled` value (contraband) of the specified type from this `Notification`'s `userInfo`.
///
/// - Returns: The extracted `Smuggled` value when found, otherwise `nil`.
func smuggled<Contraband: Smuggled>() -> Contraband? {
guard let instance = userInfo?[Contraband.userInfoKey] else {
NSLog("[\(Contraband.self)] Value not found in userInfo[\"\(Contraband.userInfoKey)\"]")
return nil
}
guard let contraband = instance as? Contraband else {
NSLog("[\(Contraband.self)] Failed to cast \(instance) as \(Contraband.self)")
return nil
}
return contraband
}
}
// MARK: -
public extension NotificationCenter {
/// Returns an `AsyncSequence` of notifications of a specific `Smuggled` type.
///
/// Each element of the sequence is a `Smuggled` value.
///
/// - Parameter contrabandType: The `Smuggled` type to observe..
/// - Returns: An `AsyncSequence` of `NotificationSmuggler` values.
func notifications<Contraband: Smuggled>(
for contrabandType: Contraband.Type
) -> any AsyncSequence<Contraband, Never> {
notifications(named: Contraband.notificationName)
.compactMap { $0.smuggled() }
}
/// Returns a Combine publisher that emits `Smuggled` values of the given type.
///
/// - Parameter contrabandType: The `Smuggled` type to observe.
/// - Returns: A publisher emitting `Smuggled` values.
func publisher<Contraband: Smuggled>(
for contrabandType: Contraband.Type
) -> AnyPublisher<Contraband, Never> {
publisher(for: contrabandType.notificationName)
.compactMap { $0.smuggled() }
.eraseToAnyPublisher()
}
}

View file

@ -1,5 +0,0 @@
import BetterNotification
struct HitchhikersNotification: BetterNotification, Equatable {
let answer: Int
}

View file

@ -0,0 +1,5 @@
import NotificationSmuggler
struct HitchhikersNotification: Smuggled, Equatable, Sendable {
let answer: Int
}

View file

@ -0,0 +1,13 @@
import Foundation
@testable import NotificationSmuggler
import Testing
struct SmuggledTests {
@Test func notificationName() {
#expect(HitchhikersNotification.notificationName.rawValue == "NotificationSmuggler:HitchhikersNotification")
}
@Test func userInfoKey() {
#expect(HitchhikersNotification.userInfoKey == "NotificationSmuggler:HitchhikersNotification")
}
}

View file

@ -1,49 +1,42 @@
@testable import BetterNotification
import Combine import Combine
import Foundation import Foundation
@testable import NotificationSmuggler
import Testing import Testing
struct BetterNotificationTests { struct SmugglingTests {
@Test func notificationName() {
#expect(HitchhikersNotification.notificationName.rawValue == "BetterNotification:HitchhikersNotification")
}
@Test func userInfoKey() { // MARK: Notification extensions
#expect(HitchhikersNotification.userInfoKey == "BetterNotification:HitchhikersNotification")
}
// MARK: - Notification extensions
@Test func buildNotification() { @Test func buildNotification() {
let hitchhikers = HitchhikersNotification(answer: 42) let contraband = HitchhikersNotification(answer: 42)
let notification = Notification.better(hitchhikers) let notification = Notification.smuggle(contraband)
#expect(notification.name == HitchhikersNotification.notificationName) #expect(notification.name == HitchhikersNotification.notificationName)
#expect(notification.userInfo?.count == 1) #expect(notification.userInfo?.count == 1)
let key = HitchhikersNotification.userInfoKey let key = HitchhikersNotification.userInfoKey
let userInfoValue = notification.userInfo?[key] as? HitchhikersNotification let userInfoValue = notification.userInfo?[key] as? HitchhikersNotification
#expect(userInfoValue == hitchhikers) #expect(userInfoValue == contraband)
} }
@Test func extractBetterPayload() { @Test func extractContraband() {
let hitchhikers = HitchhikersNotification(answer: 42) let contraband = HitchhikersNotification(answer: 42)
let notification = Notification.better(hitchhikers) let notification = Notification.smuggle(contraband)
let extracted: HitchhikersNotification? = notification.better() let extracted: HitchhikersNotification? = notification.smuggled()
#expect(extracted == hitchhikers) #expect(extracted == contraband)
} }
@Test func extractBetterPayloadFailsOnWrongType() { @Test func extractContrabandFailsOnWrongType() {
let imposter = Notification( let imposter = Notification(
name: HitchhikersNotification.notificationName, name: HitchhikersNotification.notificationName,
object: nil, object: nil,
userInfo: [HitchhikersNotification.userInfoKey: "imposter"] userInfo: [HitchhikersNotification.userInfoKey: "imposter"]
) )
let extracted: HitchhikersNotification? = imposter.better() let extracted: HitchhikersNotification? = imposter.smuggled()
#expect(extracted == nil) #expect(extracted == nil)
} }
@Test func extractBetterPayloadFailsOnMissingPayload() { @Test func extractContrabandFailsOnMissingPayload() {
let wrongNotification = Notification(name: HitchhikersNotification.notificationName) let incompleteNotification = Notification(name: HitchhikersNotification.notificationName)
let extracted: HitchhikersNotification? = wrongNotification.better() let extracted: HitchhikersNotification? = incompleteNotification.smuggled()
#expect(extracted == nil) #expect(extracted == nil)
} }
@ -58,16 +51,16 @@ struct BetterNotificationTests {
let center = NotificationCenter() let center = NotificationCenter()
nonisolated(unsafe) var received = false nonisolated(unsafe) var received = false
Task { Task {
for await hitchhikers in center.notifications(for: HitchhikersNotification.self) { for await contraband in center.notifications(for: HitchhikersNotification.self) {
#expect(hitchhikers.answer == 42) #expect(contraband.answer == 42)
received = true received = true
break break
} }
} }
await Task.yield() await Task.yield()
let hitchhikers = HitchhikersNotification(answer: 42) let contraband = HitchhikersNotification(answer: 42)
center.post(.better(hitchhikers)) center.post(.smuggle(contraband))
while !received { await Task.yield() } while !received { await Task.yield() }
} }
@ -107,8 +100,8 @@ struct BetterNotificationTests {
}.store(in: &cancellables) }.store(in: &cancellables)
await Task.yield() await Task.yield()
let hitchhikers = HitchhikersNotification(answer: 42) let contraband = HitchhikersNotification(answer: 42)
center.post(.better(hitchhikers)) center.post(.smuggle(contraband))
while !received { await Task.yield() } while !received { await Task.yield() }
} }
@ -118,8 +111,8 @@ struct BetterNotificationTests {
let center = NotificationCenter() let center = NotificationCenter()
nonisolated(unsafe) var received = false nonisolated(unsafe) var received = false
center.publisher(for: HitchhikersNotification.self) center.publisher(for: HitchhikersNotification.self)
.sink { hitchhikers in .sink { contraband in
#expect(hitchhikers.answer == 42) #expect(contraband.answer == 42)
received = true received = true
}.store(in: &cancellables) }.store(in: &cancellables)
await Task.yield() await Task.yield()