mirror of
https://github.com/samsonjs/NotificationSmuggler.git
synced 2026-04-27 14:57:38 +00:00
Rename to NotificationSmuggler
This commit is contained in:
parent
59effef18f
commit
a43dd347dd
9 changed files with 148 additions and 143 deletions
|
|
@ -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"]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
17
Readme.md
17
Readme.md
|
|
@ -1,8 +1,8 @@
|
||||||
# BetterNotification
|
# NotificationSmuggler
|
||||||
|
|
||||||
[](https://0dependencies.dev)
|
[](https://0dependencies.dev)
|
||||||
[](https://swiftpackageindex.com/samsonjs/BetterNotification)
|
[](https://swiftpackageindex.com/samsonjs/NotificationSmuggler)
|
||||||
[](https://swiftpackageindex.com/samsonjs/BetterNotification)
|
[](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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
Sources/NotificationSmuggling/Smuggled.swift
Normal file
27
Sources/NotificationSmuggling/Smuggled.swift
Normal 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 }
|
||||||
|
}
|
||||||
66
Sources/NotificationSmuggling/Smuggling.swift
Normal file
66
Sources/NotificationSmuggling/Smuggling.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import BetterNotification
|
|
||||||
|
|
||||||
struct HitchhikersNotification: BetterNotification, Equatable {
|
|
||||||
let answer: Int
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import NotificationSmuggler
|
||||||
|
|
||||||
|
struct HitchhikersNotification: Smuggled, Equatable, Sendable {
|
||||||
|
let answer: Int
|
||||||
|
}
|
||||||
13
Tests/NotificationSmugglingTests/SmuggledTests.swift
Normal file
13
Tests/NotificationSmugglingTests/SmuggledTests.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in a new issue