From a43dd347dd351382804547d44b48732e3e66f9c7 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Tue, 29 Apr 2025 08:53:24 -0700 Subject: [PATCH] Rename to NotificationSmuggler --- Package.swift | 12 +-- Readme.md | 17 ++-- .../BetterNotification.swift | 91 ------------------- Sources/NotificationSmuggling/Smuggled.swift | 27 ++++++ Sources/NotificationSmuggling/Smuggling.swift | 66 ++++++++++++++ .../HitchhikersNotification.swift | 5 - .../HitchhikersNotification.swift | 5 + .../SmuggledTests.swift | 13 +++ .../SmugglingTests.swift} | 55 +++++------ 9 files changed, 148 insertions(+), 143 deletions(-) delete mode 100644 Sources/BetterNotification/BetterNotification.swift create mode 100644 Sources/NotificationSmuggling/Smuggled.swift create mode 100644 Sources/NotificationSmuggling/Smuggling.swift delete mode 100644 Tests/BetterNotificationTests/HitchhikersNotification.swift create mode 100644 Tests/NotificationSmugglingTests/HitchhikersNotification.swift create mode 100644 Tests/NotificationSmugglingTests/SmuggledTests.swift rename Tests/{BetterNotificationTests/BetterNotificationTests.swift => NotificationSmugglingTests/SmugglingTests.swift} (67%) diff --git a/Package.swift b/Package.swift index 1e4b0a5..95b8478 100644 --- a/Package.swift +++ b/Package.swift @@ -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"] ), ] ) diff --git a/Readme.md b/Readme.md index 4f51c31..4a6c3f9 100644 --- a/Readme.md +++ b/Readme.md @@ -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 diff --git a/Sources/BetterNotification/BetterNotification.swift b/Sources/BetterNotification/BetterNotification.swift deleted file mode 100644 index 05882b4..0000000 --- a/Sources/BetterNotification/BetterNotification.swift +++ /dev/null @@ -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( - _ 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? { - 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( - for betterType: BN.Type - ) -> any AsyncSequence { - 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( - for betterType: BN.Type - ) -> AnyPublisher { - publisher(for: betterType.notificationName) - .compactMap { $0.better() } - .eraseToAnyPublisher() - } -} diff --git a/Sources/NotificationSmuggling/Smuggled.swift b/Sources/NotificationSmuggling/Smuggled.swift new file mode 100644 index 0000000..5403fe2 --- /dev/null +++ b/Sources/NotificationSmuggling/Smuggled.swift @@ -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 } +} diff --git a/Sources/NotificationSmuggling/Smuggling.swift b/Sources/NotificationSmuggling/Smuggling.swift new file mode 100644 index 0000000..ea578ef --- /dev/null +++ b/Sources/NotificationSmuggling/Smuggling.swift @@ -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: 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? { + 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( + for contrabandType: Contraband.Type + ) -> any AsyncSequence { + 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( + for contrabandType: Contraband.Type + ) -> AnyPublisher { + publisher(for: contrabandType.notificationName) + .compactMap { $0.smuggled() } + .eraseToAnyPublisher() + } +} diff --git a/Tests/BetterNotificationTests/HitchhikersNotification.swift b/Tests/BetterNotificationTests/HitchhikersNotification.swift deleted file mode 100644 index ddc3a4e..0000000 --- a/Tests/BetterNotificationTests/HitchhikersNotification.swift +++ /dev/null @@ -1,5 +0,0 @@ -import BetterNotification - -struct HitchhikersNotification: BetterNotification, Equatable { - let answer: Int -} diff --git a/Tests/NotificationSmugglingTests/HitchhikersNotification.swift b/Tests/NotificationSmugglingTests/HitchhikersNotification.swift new file mode 100644 index 0000000..d2d8dc0 --- /dev/null +++ b/Tests/NotificationSmugglingTests/HitchhikersNotification.swift @@ -0,0 +1,5 @@ +import NotificationSmuggler + +struct HitchhikersNotification: Smuggled, Equatable, Sendable { + let answer: Int +} diff --git a/Tests/NotificationSmugglingTests/SmuggledTests.swift b/Tests/NotificationSmugglingTests/SmuggledTests.swift new file mode 100644 index 0000000..a9aa2b7 --- /dev/null +++ b/Tests/NotificationSmugglingTests/SmuggledTests.swift @@ -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") + } +} diff --git a/Tests/BetterNotificationTests/BetterNotificationTests.swift b/Tests/NotificationSmugglingTests/SmugglingTests.swift similarity index 67% rename from Tests/BetterNotificationTests/BetterNotificationTests.swift rename to Tests/NotificationSmugglingTests/SmugglingTests.swift index 7caff40..2cf0428 100644 --- a/Tests/BetterNotificationTests/BetterNotificationTests.swift +++ b/Tests/NotificationSmugglingTests/SmugglingTests.swift @@ -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()