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
let package = Package(
name: "BetterNotification",
name: "NotificationSmuggler",
platforms: [
.iOS(.v18),
.macOS(.v15),
@ -12,17 +12,17 @@ let package = Package(
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "BetterNotification",
targets: ["BetterNotification"]),
name: "NotificationSmuggler",
targets: ["NotificationSmuggler"]),
],
targets: [
// 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.
.target(
name: "BetterNotification"),
name: "NotificationSmuggler"),
.testTarget(
name: "BetterNotificationTests",
dependencies: ["BetterNotification"]
name: "NotificationSmugglerTests",
dependencies: ["NotificationSmuggler"]
),
]
)

View file

@ -1,8 +1,8 @@
# BetterNotification
# NotificationSmuggler
[![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%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%3Dswift-versions)](https://swiftpackageindex.com/samsonjs/NotificationSmuggler)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fsamsonjs%2FNotificationSmuggler%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/samsonjs/NotificationSmuggler)
## Overview
@ -14,12 +14,9 @@ tktk
## 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.
[BetterNotification.swift]: https://github.com/samsonjs/BetterNotification/blob/main/Sources/BetterNotification/BetterNotification.swift
[file a new issue]: https://github.com/samsonjs/BetterNotification/issues/new
[file a new issue]: https://github.com/samsonjs/NotificationSmuggler/issues/new
### Supported Platforms
@ -27,17 +24,17 @@ This package is supported on iOS 16.0+ and macOS 12.0+.
### 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)
When you're integrating this using SPM on its own then add this to the list of dependencies your Package.swift file:
```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

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