├── .github └── workflows │ ├── docc.yml │ └── test.yml ├── .gitignore ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ ├── TypedNotifications.xcscheme │ └── TypedNotificationsTests.xcscheme ├── Examples ├── TypedNotification_iOS │ └── TypedNotification_iOS │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ └── TypedNotification_iOSApp.swift └── iOS Example │ ├── iOS Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── iOS Example.xcscheme │ └── iOS Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── Util.swift │ └── iOS_ExampleApp.swift ├── LICENSE ├── Package.swift ├── Package@swift-5.9.swift ├── README.md ├── Sources ├── TypedNotifications │ ├── CoreData │ │ └── NSManagedObjectContext.swift │ ├── NotificagtionMacro.swift │ ├── NotificationCenter+Extensions.swift │ ├── TypedNotification.swift │ ├── TypedNotificationCenter+Combine.swift │ ├── TypedNotificationCenter.swift │ ├── TypedNotificationDefinition.swift │ ├── TypedNotifications.docc │ │ └── TypedNotifications.md │ └── UIKit │ │ ├── UIApplication.swift │ │ ├── UIResponder.swift │ │ └── UIScene.swift └── TypedNotificationsMacro │ ├── NotificationMacro.swift │ └── TypedNotificationsMacros.swift ├── Tests ├── TypedNotificationsMacroTests │ └── NotificationMacroTest.swift └── TypedNotificationsTests │ └── TypedNotificationsTests.swift └── TypedNotification.xcworkspace ├── contents.xcworkspacedata └── xcshareddata ├── IDEWorkspaceChecks.plist └── swiftpm └── Package.resolved /.github/workflows/docc.yml: -------------------------------------------------------------------------------- 1 | name: DocC 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | DocC: 9 | runs-on: macos-15 10 | env: 11 | DEVELOPER_DIR: "/Applications/Xcode_16.2.app/Contents/Developer" 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Build DocC 15 | run: | 16 | swift package --allow-writing-to-directory ./docs generate-documentation \ 17 | --target TypedNotifications \ 18 | --disable-indexing \ 19 | --output-path ./docs \ 20 | --transform-for-static-hosting \ 21 | --hosting-base-path typed-notifications 22 | - uses: actions/upload-pages-artifact@v3 23 | id: docs 24 | with: 25 | path: docs 26 | DeployDocC: 27 | needs: DocC 28 | permissions: 29 | pages: write 30 | id-token: write 31 | environment: 32 | name: github-pages 33 | url: ${{ steps.deployment.outputs.page_url }} 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Deploy to GitHub Pages 37 | id: docs 38 | uses: actions/deploy-pages@v4 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | swift_version: 14 | - "5.9" 15 | - "5.10.0" 16 | - "6.0" 17 | runs-on: macos-latest 18 | steps: 19 | - uses: SwiftyLab/setup-swift@latest 20 | with: 21 | swift-version: ${{ matrix.swift_version }} 22 | - uses: actions/checkout@v4 23 | - name: Run tests 24 | run: swift test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Generated by gibo (https://github.com/simonwhitaker/gibo) 2 | ### https://raw.github.com/github/gitignore/4488915eec0b3a45b5c63ead28f286819c0917de/Swift.gitignore 3 | 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | 8 | ## User settings 9 | xcuserdata/ 10 | 11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | .swiftpm/configuration/registries.json 51 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 52 | 53 | .build/ 54 | 55 | # CocoaPods 56 | # 57 | # We recommend against adding the Pods directory to your .gitignore. However 58 | # you should judge for yourself, the pros and cons are mentioned at: 59 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 60 | # 61 | # Pods/ 62 | # 63 | # Add this line if you want to avoid checking in source code from the Xcode workspace 64 | # *.xcworkspace 65 | 66 | # Carthage 67 | # 68 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 69 | # Carthage/Checkouts 70 | 71 | Carthage/Build/ 72 | 73 | # Accio dependency management 74 | Dependencies/ 75 | .accio/ 76 | 77 | # fastlane 78 | # 79 | # It is recommended to not store the screenshots in the git repo. 80 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 81 | # For more information about the recommended setup visit: 82 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 83 | 84 | fastlane/report.xml 85 | fastlane/Preview.html 86 | fastlane/screenshots/**/*.png 87 | fastlane/test_output 88 | 89 | # Code Injection 90 | # 91 | # After new code Injection tools there's a generated folder /iOSInjectionProject 92 | # https://github.com/johnno1962/injectionforxcode 93 | 94 | iOSInjectionProject/ 95 | 96 | 97 | ### https://raw.github.com/github/gitignore/4488915eec0b3a45b5c63ead28f286819c0917de/Global/Xcode.gitignore 98 | 99 | ## User settings 100 | xcuserdata/ 101 | 102 | ## Xcode 8 and earlier 103 | *.xcscmblueprint 104 | *.xccheckout 105 | 106 | 107 | ### https://raw.github.com/github/gitignore/4488915eec0b3a45b5c63ead28f286819c0917de/Global/macOS.gitignore 108 | 109 | # General 110 | .DS_Store 111 | .AppleDouble 112 | .LSOverride 113 | 114 | # Icon must end with two \r 115 | Icon 116 | 117 | # Thumbnails 118 | ._* 119 | 120 | # Files that might appear in the root of a volume 121 | .DocumentRevisions-V100 122 | .fseventsd 123 | .Spotlight-V100 124 | .TemporaryItems 125 | .Trashes 126 | .VolumeIcon.icns 127 | .com.apple.timemachine.donotpresent 128 | 129 | # Directories potentially created on remote AFP share 130 | .AppleDB 131 | .AppleDesktop 132 | Network Trash Folder 133 | Temporary Items 134 | .apdisk 135 | 136 | 137 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/TypedNotifications.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/TypedNotificationsTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Examples/TypedNotification_iOS/TypedNotification_iOS/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/TypedNotification_iOS/TypedNotification_iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/TypedNotification_iOS/TypedNotification_iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/TypedNotification_iOS/TypedNotification_iOS/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | var body: some View { 5 | VStack { 6 | Image(systemName: "globe") 7 | .imageScale(.large) 8 | .foregroundStyle(.tint) 9 | Text("Hello, world!") 10 | } 11 | .padding() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/TypedNotification_iOS/TypedNotification_iOS/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/TypedNotification_iOS/TypedNotification_iOS/TypedNotification_iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct TypedNotification_iOSApp: App { 5 | 6 | @UIApplicationDelegateAdaptor(AppDelegate.self) 7 | var appDelegate 8 | 9 | var body: some Scene { 10 | WindowGroup { 11 | ContentView() 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 187E45FC2B0E23E400BB20DD /* TypedNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = 187E45FB2B0E23E400BB20DD /* TypedNotifications */; }; 11 | 18D464C12AAC6B900073F946 /* iOS_ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D464C02AAC6B900073F946 /* iOS_ExampleApp.swift */; }; 12 | 18D464C32AAC6B900073F946 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D464C22AAC6B900073F946 /* ContentView.swift */; }; 13 | 18D464C52AAC6B910073F946 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 18D464C42AAC6B910073F946 /* Assets.xcassets */; }; 14 | 18D464C82AAC6B910073F946 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 18D464C72AAC6B910073F946 /* Preview Assets.xcassets */; }; 15 | 18D464D22AAC6BBF0073F946 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D464D12AAC6BBF0073F946 /* AppDelegate.swift */; }; 16 | 18D464D42AACBAA40073F946 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D464D32AACBAA40073F946 /* Util.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 18D464BD2AAC6B900073F946 /* iOS Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 18D464C02AAC6B900073F946 /* iOS_ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOS_ExampleApp.swift; sourceTree = ""; }; 22 | 18D464C22AAC6B900073F946 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 23 | 18D464C42AAC6B910073F946 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | 18D464C72AAC6B910073F946 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 25 | 18D464D12AAC6BBF0073F946 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | 18D464D32AACBAA40073F946 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 18D464BA2AAC6B900073F946 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | 187E45FC2B0E23E400BB20DD /* TypedNotifications in Frameworks */, 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 18D464B42AAC6B900073F946 = { 42 | isa = PBXGroup; 43 | children = ( 44 | 18D464BF2AAC6B900073F946 /* iOS Example */, 45 | 18D464BE2AAC6B900073F946 /* Products */, 46 | 18D464CE2AAC6BA90073F946 /* Frameworks */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | 18D464BE2AAC6B900073F946 /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 18D464BD2AAC6B900073F946 /* iOS Example.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | 18D464BF2AAC6B900073F946 /* iOS Example */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 18D464C02AAC6B900073F946 /* iOS_ExampleApp.swift */, 62 | 18D464D12AAC6BBF0073F946 /* AppDelegate.swift */, 63 | 18D464C22AAC6B900073F946 /* ContentView.swift */, 64 | 18D464D32AACBAA40073F946 /* Util.swift */, 65 | 18D464C42AAC6B910073F946 /* Assets.xcassets */, 66 | 18D464C62AAC6B910073F946 /* Preview Content */, 67 | ); 68 | path = "iOS Example"; 69 | sourceTree = ""; 70 | }; 71 | 18D464C62AAC6B910073F946 /* Preview Content */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 18D464C72AAC6B910073F946 /* Preview Assets.xcassets */, 75 | ); 76 | path = "Preview Content"; 77 | sourceTree = ""; 78 | }; 79 | 18D464CE2AAC6BA90073F946 /* Frameworks */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | ); 83 | name = Frameworks; 84 | sourceTree = ""; 85 | }; 86 | /* End PBXGroup section */ 87 | 88 | /* Begin PBXNativeTarget section */ 89 | 18D464BC2AAC6B900073F946 /* iOS Example */ = { 90 | isa = PBXNativeTarget; 91 | buildConfigurationList = 18D464CB2AAC6B910073F946 /* Build configuration list for PBXNativeTarget "iOS Example" */; 92 | buildPhases = ( 93 | 18D464B92AAC6B900073F946 /* Sources */, 94 | 18D464BA2AAC6B900073F946 /* Frameworks */, 95 | 18D464BB2AAC6B900073F946 /* Resources */, 96 | ); 97 | buildRules = ( 98 | ); 99 | dependencies = ( 100 | ); 101 | name = "iOS Example"; 102 | packageProductDependencies = ( 103 | 187E45FB2B0E23E400BB20DD /* TypedNotifications */, 104 | ); 105 | productName = "iOS Example"; 106 | productReference = 18D464BD2AAC6B900073F946 /* iOS Example.app */; 107 | productType = "com.apple.product-type.application"; 108 | }; 109 | /* End PBXNativeTarget section */ 110 | 111 | /* Begin PBXProject section */ 112 | 18D464B52AAC6B900073F946 /* Project object */ = { 113 | isa = PBXProject; 114 | attributes = { 115 | BuildIndependentTargetsInParallel = 1; 116 | LastSwiftUpdateCheck = 1500; 117 | LastUpgradeCheck = 1500; 118 | TargetAttributes = { 119 | 18D464BC2AAC6B900073F946 = { 120 | CreatedOnToolsVersion = 15.0; 121 | }; 122 | }; 123 | }; 124 | buildConfigurationList = 18D464B82AAC6B900073F946 /* Build configuration list for PBXProject "iOS Example" */; 125 | compatibilityVersion = "Xcode 14.0"; 126 | developmentRegion = en; 127 | hasScannedForEncodings = 0; 128 | knownRegions = ( 129 | en, 130 | Base, 131 | ); 132 | mainGroup = 18D464B42AAC6B900073F946; 133 | productRefGroup = 18D464BE2AAC6B900073F946 /* Products */; 134 | projectDirPath = ""; 135 | projectRoot = ""; 136 | targets = ( 137 | 18D464BC2AAC6B900073F946 /* iOS Example */, 138 | ); 139 | }; 140 | /* End PBXProject section */ 141 | 142 | /* Begin PBXResourcesBuildPhase section */ 143 | 18D464BB2AAC6B900073F946 /* Resources */ = { 144 | isa = PBXResourcesBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | 18D464C82AAC6B910073F946 /* Preview Assets.xcassets in Resources */, 148 | 18D464C52AAC6B910073F946 /* Assets.xcassets in Resources */, 149 | ); 150 | runOnlyForDeploymentPostprocessing = 0; 151 | }; 152 | /* End PBXResourcesBuildPhase section */ 153 | 154 | /* Begin PBXSourcesBuildPhase section */ 155 | 18D464B92AAC6B900073F946 /* Sources */ = { 156 | isa = PBXSourcesBuildPhase; 157 | buildActionMask = 2147483647; 158 | files = ( 159 | 18D464C32AAC6B900073F946 /* ContentView.swift in Sources */, 160 | 18D464C12AAC6B900073F946 /* iOS_ExampleApp.swift in Sources */, 161 | 18D464D22AAC6BBF0073F946 /* AppDelegate.swift in Sources */, 162 | 18D464D42AACBAA40073F946 /* Util.swift in Sources */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXSourcesBuildPhase section */ 167 | 168 | /* Begin XCBuildConfiguration section */ 169 | 18D464C92AAC6B910073F946 /* Debug */ = { 170 | isa = XCBuildConfiguration; 171 | buildSettings = { 172 | ALWAYS_SEARCH_USER_PATHS = NO; 173 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 174 | CLANG_ANALYZER_NONNULL = YES; 175 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 176 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 177 | CLANG_ENABLE_MODULES = YES; 178 | CLANG_ENABLE_OBJC_ARC = YES; 179 | CLANG_ENABLE_OBJC_WEAK = YES; 180 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 181 | CLANG_WARN_BOOL_CONVERSION = YES; 182 | CLANG_WARN_COMMA = YES; 183 | CLANG_WARN_CONSTANT_CONVERSION = YES; 184 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 185 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 186 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 187 | CLANG_WARN_EMPTY_BODY = YES; 188 | CLANG_WARN_ENUM_CONVERSION = YES; 189 | CLANG_WARN_INFINITE_RECURSION = YES; 190 | CLANG_WARN_INT_CONVERSION = YES; 191 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 192 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 193 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 194 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 195 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 196 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 197 | CLANG_WARN_STRICT_PROTOTYPES = YES; 198 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 199 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 200 | CLANG_WARN_UNREACHABLE_CODE = YES; 201 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 202 | COPY_PHASE_STRIP = NO; 203 | DEBUG_INFORMATION_FORMAT = dwarf; 204 | ENABLE_STRICT_OBJC_MSGSEND = YES; 205 | ENABLE_TESTABILITY = YES; 206 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 207 | GCC_C_LANGUAGE_STANDARD = gnu17; 208 | GCC_DYNAMIC_NO_PIC = NO; 209 | GCC_NO_COMMON_BLOCKS = YES; 210 | GCC_OPTIMIZATION_LEVEL = 0; 211 | GCC_PREPROCESSOR_DEFINITIONS = ( 212 | "DEBUG=1", 213 | "$(inherited)", 214 | ); 215 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 216 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 217 | GCC_WARN_UNDECLARED_SELECTOR = YES; 218 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 219 | GCC_WARN_UNUSED_FUNCTION = YES; 220 | GCC_WARN_UNUSED_VARIABLE = YES; 221 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 222 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 223 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 224 | MTL_FAST_MATH = YES; 225 | ONLY_ACTIVE_ARCH = YES; 226 | SDKROOT = iphoneos; 227 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 228 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 229 | }; 230 | name = Debug; 231 | }; 232 | 18D464CA2AAC6B910073F946 /* Release */ = { 233 | isa = XCBuildConfiguration; 234 | buildSettings = { 235 | ALWAYS_SEARCH_USER_PATHS = NO; 236 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 237 | CLANG_ANALYZER_NONNULL = YES; 238 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 240 | CLANG_ENABLE_MODULES = YES; 241 | CLANG_ENABLE_OBJC_ARC = YES; 242 | CLANG_ENABLE_OBJC_WEAK = YES; 243 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 244 | CLANG_WARN_BOOL_CONVERSION = YES; 245 | CLANG_WARN_COMMA = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 250 | CLANG_WARN_EMPTY_BODY = YES; 251 | CLANG_WARN_ENUM_CONVERSION = YES; 252 | CLANG_WARN_INFINITE_RECURSION = YES; 253 | CLANG_WARN_INT_CONVERSION = YES; 254 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 256 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 257 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 258 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 259 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 260 | CLANG_WARN_STRICT_PROTOTYPES = YES; 261 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 262 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 263 | CLANG_WARN_UNREACHABLE_CODE = YES; 264 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 265 | COPY_PHASE_STRIP = NO; 266 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 267 | ENABLE_NS_ASSERTIONS = NO; 268 | ENABLE_STRICT_OBJC_MSGSEND = YES; 269 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 270 | GCC_C_LANGUAGE_STANDARD = gnu17; 271 | GCC_NO_COMMON_BLOCKS = YES; 272 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 273 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 274 | GCC_WARN_UNDECLARED_SELECTOR = YES; 275 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 276 | GCC_WARN_UNUSED_FUNCTION = YES; 277 | GCC_WARN_UNUSED_VARIABLE = YES; 278 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 279 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 280 | MTL_ENABLE_DEBUG_INFO = NO; 281 | MTL_FAST_MATH = YES; 282 | SDKROOT = iphoneos; 283 | SWIFT_COMPILATION_MODE = wholemodule; 284 | VALIDATE_PRODUCT = YES; 285 | }; 286 | name = Release; 287 | }; 288 | 18D464CC2AAC6B910073F946 /* Debug */ = { 289 | isa = XCBuildConfiguration; 290 | buildSettings = { 291 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 292 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 293 | CODE_SIGN_STYLE = Automatic; 294 | CURRENT_PROJECT_VERSION = 1; 295 | DEVELOPMENT_ASSET_PATHS = "\"iOS Example/Preview Content\""; 296 | DEVELOPMENT_TEAM = 79Q7G522V8; 297 | ENABLE_PREVIEWS = YES; 298 | GENERATE_INFOPLIST_FILE = YES; 299 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 300 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 301 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 302 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 303 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 304 | LD_RUNPATH_SEARCH_PATHS = ( 305 | "$(inherited)", 306 | "@executable_path/Frameworks", 307 | ); 308 | MARKETING_VERSION = 1.0; 309 | PRODUCT_BUNDLE_IDENTIFIER = "net.matsuji.iOS-Example"; 310 | PRODUCT_NAME = "$(TARGET_NAME)"; 311 | SWIFT_EMIT_LOC_STRINGS = YES; 312 | SWIFT_VERSION = 5.0; 313 | TARGETED_DEVICE_FAMILY = "1,2"; 314 | }; 315 | name = Debug; 316 | }; 317 | 18D464CD2AAC6B910073F946 /* Release */ = { 318 | isa = XCBuildConfiguration; 319 | buildSettings = { 320 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 321 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 322 | CODE_SIGN_STYLE = Automatic; 323 | CURRENT_PROJECT_VERSION = 1; 324 | DEVELOPMENT_ASSET_PATHS = "\"iOS Example/Preview Content\""; 325 | DEVELOPMENT_TEAM = 79Q7G522V8; 326 | ENABLE_PREVIEWS = YES; 327 | GENERATE_INFOPLIST_FILE = YES; 328 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 329 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 330 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 331 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 332 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 333 | LD_RUNPATH_SEARCH_PATHS = ( 334 | "$(inherited)", 335 | "@executable_path/Frameworks", 336 | ); 337 | MARKETING_VERSION = 1.0; 338 | PRODUCT_BUNDLE_IDENTIFIER = "net.matsuji.iOS-Example"; 339 | PRODUCT_NAME = "$(TARGET_NAME)"; 340 | SWIFT_EMIT_LOC_STRINGS = YES; 341 | SWIFT_VERSION = 5.0; 342 | TARGETED_DEVICE_FAMILY = "1,2"; 343 | }; 344 | name = Release; 345 | }; 346 | /* End XCBuildConfiguration section */ 347 | 348 | /* Begin XCConfigurationList section */ 349 | 18D464B82AAC6B900073F946 /* Build configuration list for PBXProject "iOS Example" */ = { 350 | isa = XCConfigurationList; 351 | buildConfigurations = ( 352 | 18D464C92AAC6B910073F946 /* Debug */, 353 | 18D464CA2AAC6B910073F946 /* Release */, 354 | ); 355 | defaultConfigurationIsVisible = 0; 356 | defaultConfigurationName = Release; 357 | }; 358 | 18D464CB2AAC6B910073F946 /* Build configuration list for PBXNativeTarget "iOS Example" */ = { 359 | isa = XCConfigurationList; 360 | buildConfigurations = ( 361 | 18D464CC2AAC6B910073F946 /* Debug */, 362 | 18D464CD2AAC6B910073F946 /* Release */, 363 | ); 364 | defaultConfigurationIsVisible = 0; 365 | defaultConfigurationName = Release; 366 | }; 367 | /* End XCConfigurationList section */ 368 | 369 | /* Begin XCSwiftPackageProductDependency section */ 370 | 187E45FB2B0E23E400BB20DD /* TypedNotifications */ = { 371 | isa = XCSwiftPackageProductDependency; 372 | productName = TypedNotifications; 373 | }; 374 | /* End XCSwiftPackageProductDependency section */ 375 | }; 376 | rootObject = 18D464B52AAC6B900073F946 /* Project object */; 377 | } 378 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example.xcodeproj/xcshareddata/xcschemes/iOS Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | import TypedNotifications 4 | 5 | final class AppDelegate: NSObject, UIApplicationDelegate { 6 | 7 | private var cancellable: [AnyCancellable] = [] 8 | 9 | override init() { 10 | super.init() 11 | 12 | // MARK: - UIApplication 13 | [ 14 | UIApplication.didBecomeActiveTypedNotification, 15 | UIApplication.didEnterBackgroundTypedNotification, 16 | UIApplication.willEnterForegroundTypedNotification, 17 | UIApplication.willResignActiveTypedNotification, 18 | UIApplication.willTerminateTypedNotification 19 | ].forEach { definition in 20 | TypedNotificationCenter.default.publisher(for: definition) 21 | .sink { printNotification($0) } 22 | .store(in: &cancellable) 23 | } 24 | 25 | // MARK: - UIScene 26 | [ 27 | UIScene.willConnectTypedNotification, 28 | UIScene.didActivateTypedNotification, 29 | UIScene.didDisconnectTypedNotification, 30 | UIScene.willEnterForegroundTypedNotification, 31 | UIScene.willDeactivateTypedNotification, 32 | UIScene.didEnterBackgroundTypedNotification 33 | ].forEach { definition in 34 | TypedNotificationCenter.default.publisher(for: definition) 35 | .sink { printNotification($0) } 36 | .store(in: &cancellable) 37 | } 38 | 39 | // MARK: - UIResponder 40 | [ 41 | UIResponder.keyboardWillShowTypedNotification, 42 | UIResponder.keyboardDidShowTypedNotification, 43 | UIResponder.keyboardWillHideTypedNotification, 44 | UIResponder.keyboardDidHideTypedNotification, 45 | UIResponder.keyboardWillChangeFrameTypedNotification, 46 | UIResponder.keyboardDidChangeFrameTypedNotification 47 | ].forEach { definition in 48 | TypedNotificationCenter.default.publisher(for: definition) 49 | .sink { notification in printNotification(notification) } 50 | .store(in: &cancellable) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | @State var text = "" 5 | 6 | var body: some View { 7 | VStack { 8 | Image(systemName: "globe") 9 | .imageScale(.large) 10 | .foregroundStyle(.tint) 11 | Text("Hello, world!") 12 | TextField("textfield", text: $text) 13 | } 14 | .padding() 15 | } 16 | } 17 | 18 | #Preview { 19 | ContentView() 20 | } 21 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example/Util.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import TypedNotifications 3 | 4 | func printNotification(_ notification: TypedNotification) { 5 | let text = """ 6 | Received Notification: 7 | ├─ name = \(notification.name.rawValue), 8 | ├─ storage = \(notification.storage), 9 | └─ object = \(notification.object.debugDescription) 10 | """ 11 | print(text) 12 | } 13 | 14 | func printNotification(_ notification: Notification) { 15 | let userInfo = notification.userInfo 16 | userInfo?.forEach({ key, value in 17 | print("\(key) = \(value)") 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /Examples/iOS Example/iOS Example/iOS_ExampleApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOS_ExampleApp: App { 5 | 6 | @UIApplicationDelegateAdaptor(AppDelegate.self) 7 | var appDelegate 8 | 9 | var body: some Scene { 10 | WindowGroup { 11 | ContentView() 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 matsuji 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import CompilerPluginSupport 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "typed-notifications", 8 | platforms: [.iOS(.v13), .macOS(.v10_15), .watchOS(.v6), .tvOS(.v13)], 9 | products: [ 10 | .library(name: "TypedNotifications", targets: ["TypedNotifications"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-syntax", from: "600.0.0"), 14 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 15 | .package(url: "https://github.com/mtj0928/userinfo-representable", from: "1.1.1") 16 | ], 17 | targets: [ 18 | .target( 19 | name: "TypedNotifications", 20 | dependencies: [ 21 | .product(name: "UserInfoRepresentable", package: "userinfo-representable"), 22 | "TypedNotificationsMacro" 23 | ] 24 | ), 25 | .macro( 26 | name: "TypedNotificationsMacro", 27 | dependencies: [ 28 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 29 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 30 | ] 31 | ), 32 | .testTarget(name: "TypedNotificationsTests", dependencies: ["TypedNotifications"]), 33 | .testTarget( 34 | name: "TypedNotificationsMacroTests", 35 | dependencies: [ 36 | "TypedNotificationsMacro", 37 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 38 | ] 39 | ) 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /Package@swift-5.9.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import CompilerPluginSupport 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "typed-notifications", 8 | platforms: [.iOS(.v13), .macOS(.v10_15), .watchOS(.v6), .tvOS(.v13)], 9 | products: [ 10 | .library(name: "TypedNotifications", targets: ["TypedNotifications"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), 14 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 15 | .package(url: "https://github.com/mtj0928/userinfo-representable", from: "1.0.2") 16 | ], 17 | targets: [ 18 | .target( 19 | name: "TypedNotifications", 20 | dependencies: [ 21 | .product(name: "UserInfoRepresentable", package: "userinfo-representable"), 22 | "TypedNotificationsMacro" 23 | ] 24 | ), 25 | .macro( 26 | name: "TypedNotificationsMacro", 27 | dependencies: [ 28 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 29 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 30 | ] 31 | ), 32 | .testTarget(name: "TypedNotificationsTests", dependencies: ["TypedNotifications"]), 33 | .testTarget( 34 | name: "TypedNotificationsMacroTests", 35 | dependencies: [ 36 | "TypedNotificationsMacro", 37 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 38 | ] 39 | ) 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typed-notifications 2 | This library attaches type-information to NotificationCenter. 3 | 4 | ## Example 5 | You can post and observe notifications in a type-safe manner. 6 | ```swift 7 | NotificationCenter.default 8 | .publisher(for: .userNameUpdate, object: user) 9 | .sink { notification in 10 | // Notifications can be received in a type safe manner. 11 | let storage: UserNameUpdateNotificationStorage = notification.storage 12 | let user: User? = notification.object 13 | // ... 14 | } 15 | 16 | extension TypedNotificationDefinition { 17 | @Notification 18 | static var userNameUpdate: TypedNotificationDefinition 19 | } 20 | 21 | @UserInfoRepresentable 22 | struct UserNameUpdateNotificationStorage { 23 | let oldName: String 24 | let newName: String 25 | } 26 | ``` 27 | 28 | ## How to Use 29 | Define a notification and how to encode/decode the userInfo in `TypedNotificationDefinition`. 30 | ```swift 31 | extension TypedNotificationDefinition { 32 | static var userNameWillUpdate: TypedNotificationDefinition { 33 | .init(name: "userWillUpdate") { newName in 34 | ["newName": newName] 35 | } decode: { userInfo in 36 | userInfo?["newName"] as? String ?? "" 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | And then, you can post/observe the notifications in type safe manner. 43 | ```swift 44 | // [Post] 45 | // Notifications can be posted in a type safe manner. 46 | let newName: String = ... 47 | let user: User = ... 48 | NotificationCenter.default.post(.userNameWillUpdate, storage: newName, object: user) 49 | 50 | // [Observation] 51 | NotificationCenter.default.publisher(for: .userNameWillUpdate, object: user) 52 | .sink { notification in 53 | // Notifications can be received in a type safe manner. 54 | let newName = notification.storage 55 | let user: User? = notification.object 56 | // ... 57 | } 58 | ``` 59 | 60 | ### Notification macro 61 | 62 | You can use `@Notification` macro, if `@UserInforRepresentable` macro is attached to your type. 63 | ```swift 64 | extension TypedNotificationDefinition { 65 | @Notification 66 | static var userNameUpdate: TypedNotificationDefinition 67 | } 68 | 69 | @UserInfoRepresentable 70 | struct UserNameUpdateNotificationStorage { 71 | let oldName: String 72 | let newName: String 73 | } 74 | ``` 75 | 76 | ## Pre-defined Notifications 77 | This repository contains frequent system notifications. 78 | - UIKit 79 | - [UIApplication](Sources/TypedNotifications/UIKit/UIApplication.swift) 80 | - [UIScene](Sources/TypedNotifications/UIKit/UIScene.swift) 81 | - [UIResponder](Sources/TypedNotifications/UIKit/UIResponder.swift) 82 | - Core Data 83 | - [NSManagedObjectContext](Sources/TypedNotifications/CoreData/NSManagedObjectContext.swift) 84 | 85 | Your PR adding new notifications is appreciated. Feel free to make a new PR. 86 | -------------------------------------------------------------------------------- /Sources/TypedNotifications/CoreData/NSManagedObjectContext.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | import UserInfoRepresentable 3 | 4 | extension NSManagedObjectContext { 5 | /// A notification that the context is about to save. 6 | @Notification(name: NSManagedObjectContext.willSaveObjectsNotification.rawValue) 7 | public static var willSaveObjectsTypedNotification: 8 | TypedNotificationDefinition 9 | 10 | /// A notification that posts when the context completes a save. 11 | @Notification(name: NSManagedObjectContext.didSaveObjectsNotification.rawValue) 12 | public static var didSaveObjectsTypedNotification: 13 | TypedNotificationDefinition 14 | 15 | /// A notification that posts when the context completes a save. 16 | @Notification(name: NSManagedObjectContext.didChangeObjectsNotification.rawValue) 17 | public static var didChangeObjectsTypedNotification: 18 | TypedNotificationDefinition 19 | } 20 | 21 | public struct NSManagedObjectNotificationUserInfo: UserInfoRepresentable { 22 | /// The set of objects that were inserted into the context. 23 | public var insertedObjects: Set 24 | 25 | /// The set of objects that were updated. 26 | public var updatedObjects: Set 27 | 28 | /// The set of objects that were marked for deletion during the previous event. 29 | public var deletedObjects: Set 30 | 31 | /// The set of objects that were refreshed but were not dirtied in the scope of this context. 32 | public var refreshedObjects: Set 33 | 34 | /// The set of objects that were invalidated. 35 | public var invalidatedObjects: Set 36 | 37 | public init( 38 | insertedObjects: Set, 39 | updatedObjects: Set, 40 | deletedObjects: Set, 41 | refreshedObjects: Set, 42 | invalidatedObjects: Set 43 | ) { 44 | self.insertedObjects = insertedObjects 45 | self.updatedObjects = updatedObjects 46 | self.deletedObjects = deletedObjects 47 | self.refreshedObjects = refreshedObjects 48 | self.invalidatedObjects = invalidatedObjects 49 | } 50 | 51 | public init(userInfo: [AnyHashable : Any]) throws { 52 | insertedObjects = userInfo[NSInsertedObjectsKey] as? Set ?? [] 53 | updatedObjects = userInfo[NSUpdatedObjectsKey] as? Set ?? [] 54 | deletedObjects = userInfo[NSDeletedObjectsKey] as? Set ?? [] 55 | refreshedObjects = userInfo[NSRefreshedObjectsKey] as? Set ?? [] 56 | invalidatedObjects = userInfo[NSInvalidatedObjectsKey] as? Set ?? [] 57 | } 58 | 59 | public func convertToUserInfo() -> [AnyHashable : Any] { 60 | [ 61 | NSInsertedObjectsKey: insertedObjects, 62 | NSUpdatedObjectsKey: updatedObjects, 63 | NSDeletedObjectsKey: deletedObjects, 64 | NSRefreshedObjectsKey: refreshedObjects, 65 | NSInvalidatedObjectsKey: invalidatedObjects, 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/TypedNotifications/NotificagtionMacro.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A macro for defining a notification whose notification name is attached property name. 4 | @attached(accessor) 5 | public macro Notification() = #externalMacro(module: "TypedNotificationsMacro", type: "NotificationMacro") 6 | 7 | /// A macro for defining a notification. 8 | /// 9 | /// - Parameter name: A name of the notification. 10 | @attached(accessor) 11 | public macro Notification(name: String) = #externalMacro(module: "TypedNotificationsMacro", type: "NotificationMacro") 12 | 13 | 14 | /// A macro for defining a notification. 15 | /// 16 | /// - Parameter name: A name of the notification. 17 | @attached(accessor) 18 | public macro Notification(name: Notification.Name) = #externalMacro(module: "TypedNotificationsMacro", type: "NotificationMacro") 19 | -------------------------------------------------------------------------------- /Sources/TypedNotifications/NotificationCenter+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NotificationCenter { 4 | public func post( 5 | _ definition: TypedNotificationDefinition, 6 | storage: Storage, 7 | object: Object? = nil 8 | ) { 9 | let userInfo = definition.encode(storage) 10 | let notification = Notification(name: definition.name, object: object, userInfo: userInfo) 11 | post(notification) 12 | } 13 | 14 | public func post( 15 | _ definition: TypedNotificationDefinition, 16 | object: Object? = nil 17 | ) { 18 | let notification = Notification(name: definition.name, object: object, userInfo: nil) 19 | post(notification) 20 | } 21 | } 22 | 23 | extension NotificationCenter { 24 | 25 | @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) 26 | public typealias AsyncNotifications = AsyncCompactMapSequence 27 | 28 | @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) 29 | public func notifications( 30 | for definition: TypedNotificationDefinition, 31 | object: Object? = nil 32 | ) -> AsyncNotifications> { 33 | notifications(named: definition.name, object: object) 34 | .compactMap { TypedNotification($0, basedOn: definition) } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/TypedNotifications/TypedNotification.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A type-safe notification. 4 | public struct TypedNotification { 5 | /// A name of the received notification 6 | public let name: Notification.Name 7 | 8 | /// A type-safe storage for values or objects related to this notification. 9 | /// 10 | /// This value is converted from `userInfo` of the notification. 11 | public let storage: Storage 12 | 13 | /// A type-safe object that the poster wishes to send to observers. 14 | public let object: Object? 15 | 16 | public init( 17 | name: Notification.Name, 18 | storage: Storage, 19 | object: Object? = nil 20 | ) { 21 | self.name = name 22 | self.storage = storage 23 | self.object = object 24 | } 25 | } 26 | 27 | extension TypedNotification { 28 | init?(_ notification: Notification, basedOn definition: TypedNotificationDefinition) { 29 | if Storage.self == Void.self, 30 | notification.userInfo?.isEmpty == false { 31 | assertionFailure("An expected type is Void, but userInfo contains values.") 32 | } 33 | 34 | do { 35 | let storage = try definition.decode(notification.userInfo) 36 | let object = notification.object as? Object 37 | self.init(name: notification.name, storage: storage, object: object) 38 | } catch { 39 | assertionFailure("Unexpected error occur: \(error.localizedDescription)") 40 | return nil 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/TypedNotifications/TypedNotificationCenter+Combine.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Combine) 2 | import Combine 3 | import Foundation 4 | 5 | extension TypedNotificationCenter { 6 | public func publisher( 7 | for definition: TypedNotificationDefinition, 8 | object: Object? = nil 9 | ) -> some Publisher, Never> { 10 | notificationCenter.publisher(for: definition.name, object: object) 11 | .compactMap { TypedNotification($0, basedOn: definition) } 12 | } 13 | } 14 | 15 | extension NotificationCenter { 16 | public func publisher( 17 | for definition: TypedNotificationDefinition, 18 | object: Object? = nil 19 | ) -> AnyPublisher, Never> { 20 | publisher(for: definition.name, object: object) 21 | .compactMap { TypedNotification($0, basedOn: definition) } 22 | .eraseToAnyPublisher() 23 | } 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /Sources/TypedNotifications/TypedNotificationCenter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | /// A wrapper of `NotificationCenter` attaching a type information to a `Notification`. 5 | public struct TypedNotificationCenter: Sendable { 6 | let notificationCenter: NotificationCenter 7 | 8 | public init(notificationCenter: NotificationCenter = NotificationCenter()) { 9 | self.notificationCenter = notificationCenter 10 | } 11 | 12 | public func post( 13 | _ definition: TypedNotificationDefinition, 14 | storage: Storage, 15 | object: Object? = nil 16 | ) { 17 | let userInfo = definition.encode(storage) 18 | let notification = Notification(name: definition.name, object: object, userInfo: userInfo) 19 | notificationCenter.post(notification) 20 | } 21 | 22 | public func post( 23 | _ definition: TypedNotificationDefinition, 24 | object: Object? = nil 25 | ) { 26 | let notification = Notification(name: definition.name, object: object, userInfo: nil) 27 | notificationCenter.post(notification) 28 | } 29 | } 30 | 31 | extension TypedNotificationCenter { 32 | 33 | @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) 34 | public typealias Notifications = AsyncCompactMapSequence 35 | 36 | @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) 37 | public func notifications( 38 | for definition: TypedNotificationDefinition, 39 | object: Object? = nil 40 | ) -> Notifications> { 41 | notificationCenter.notifications(named: definition.name, object: object) 42 | .compactMap { TypedNotification($0, basedOn: definition) } 43 | } 44 | } 45 | 46 | extension TypedNotificationCenter { 47 | public static let `default` = TypedNotificationCenter(notificationCenter: .default) 48 | } 49 | -------------------------------------------------------------------------------- /Sources/TypedNotifications/TypedNotificationDefinition.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UserInfoRepresentable 3 | 4 | public struct TypedNotificationDefinition: Sendable { 5 | public let name: Notification.Name 6 | public let encode: @Sendable (Storage) -> [AnyHashable: Any] 7 | public let decode: @Sendable ([AnyHashable: Any]?) throws -> Storage 8 | 9 | public init( 10 | name: Notification.Name, 11 | encode: @Sendable @escaping (Storage) -> [AnyHashable: Any], 12 | decode: @Sendable @escaping ([AnyHashable : Any]?) throws -> Storage 13 | ) { 14 | self.name = name 15 | self.encode = encode 16 | self.decode = decode 17 | } 18 | 19 | public init( 20 | name: String, 21 | encode: @Sendable @escaping (Storage) -> [AnyHashable: Any], 22 | decode: @Sendable @escaping ([AnyHashable : Any]?) throws -> Storage 23 | ) { 24 | self.name = Notification.Name(name) 25 | self.encode = encode 26 | self.decode = decode 27 | } 28 | 29 | public init(name: Notification.Name) where Storage == Void { 30 | self.name = name 31 | self.encode = { _ in [:] } 32 | self.decode = { _ in } 33 | } 34 | 35 | public init(name: String) where Storage == Void { 36 | self.name = Notification.Name(name) 37 | self.encode = { _ in [:] } 38 | self.decode = { _ in } 39 | } 40 | 41 | 42 | public init(name: Notification.Name) where Storage: UserInfoRepresentable { 43 | self.name = name 44 | self.encode = { storage in storage.convertToUserInfo() } 45 | self.decode = { userInfo in try Storage(userInfo: userInfo ?? [:]) } 46 | } 47 | 48 | public init(name: String) where Storage: UserInfoRepresentable { 49 | self.name = Notification.Name(name) 50 | self.encode = { storage in storage.convertToUserInfo() } 51 | self.decode = { userInfo in try Storage(userInfo: userInfo ?? [:]) } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/TypedNotifications/TypedNotifications.docc/TypedNotifications.md: -------------------------------------------------------------------------------- 1 | # ``TypedNotifications`` 2 | 3 | A library attaching type-information to `NotificationCenter` 4 | 5 | ## Overview 6 | This library attaches type-information to `NotificationCenter`. 7 | You can post and observe notifications in a type-safe manner. 8 | 9 | ```swift 10 | TypedNotificationCenter.default 11 | .publisher(for: .userNameUpdate, object: user) 12 | .sink { notification in 13 | // Notifications can be received in a type safe manner. 14 | let storage: UserNameUpdateNotificationStorage = notification.storage 15 | let user: User? = notification.object 16 | // ... 17 | } 18 | 19 | extension TypedNotificationDefinition { 20 | @Notification 21 | static var userNameUpdate: TypedNotificationDefinition 22 | } 23 | 24 | @UserInfoRepresentable 25 | struct UserNameUpdateNotificationStorage { 26 | let oldName: String 27 | let newName: String 28 | } 29 | ``` 30 | 31 | ## Usage 32 | Define a notification and how to encode/decode the userInfo in `TypedNotificationDefinition`. 33 | ```swift 34 | extension TypedNotificationDefinition { 35 | static var userNameUpdate: TypedNotificationDefinition { 36 | .init(name: "userNameUpdate") { newName in 37 | ["newName": newName] 38 | } decode: { userInfo in 39 | userInfo?["newName"] as? String ?? "" 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | And then, you can post/observe the notifications in type safe manner. 46 | ```swift 47 | // [Post] 48 | // Notifications can be posted in a type safe manner. 49 | let newName: String = ... 50 | let user: User = ... 51 | TypedNotificationCenter.default.post(.userNameUpdate, storage: newName, object: user) 52 | 53 | // [Observation] 54 | TypedNotificationCenter.default 55 | .publisher(for: .userNameUpdate, object: user) 56 | .sink { notification in 57 | // Notifications can be received in a type safe manner. 58 | let newName = notification.storage 59 | let user: User? = notification.object 60 | // ... 61 | } 62 | ``` 63 | 64 | ### Notification macro 65 | 66 | You can use `@Notification` macro, if `@UserInforRepresentable` macro is attached to your type. 67 | ```swift 68 | extension TypedNotificationDefinition { 69 | @Notification 70 | static var userNameUpdate: TypedNotificationDefinition 71 | } 72 | 73 | @UserInfoRepresentable 74 | struct UserNameUpdateNotificationStorage { 75 | let oldName: String 76 | let newName: String 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /Sources/TypedNotifications/UIKit/UIApplication.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(visionOS) || os(tvOS) || targetEnvironment(macCatalyst) 2 | import UIKit 3 | 4 | extension UIApplication { 5 | @Notification(name: UIApplication.didBecomeActiveNotification) 6 | public static var didBecomeActiveTypedNotification: TypedNotificationDefinition 7 | 8 | @Notification(name: UIApplication.didEnterBackgroundNotification) 9 | public static var didEnterBackgroundTypedNotification: TypedNotificationDefinition 10 | 11 | @Notification(name: UIApplication.willEnterForegroundNotification) 12 | public static var willEnterForegroundTypedNotification: TypedNotificationDefinition 13 | 14 | @Notification(name: UIApplication.willResignActiveNotification) 15 | public static var willResignActiveTypedNotification: TypedNotificationDefinition 16 | 17 | @Notification(name: UIApplication.willTerminateNotification) 18 | public static var willTerminateTypedNotification: TypedNotificationDefinition 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /Sources/TypedNotifications/UIKit/UIResponder.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || targetEnvironment(macCatalyst) 2 | import UserInfoRepresentable 3 | import UIKit 4 | 5 | extension UIResponder { 6 | @Notification(name: UIResponder.keyboardWillShowNotification) 7 | public static var keyboardWillShowTypedNotification: TypedNotificationDefinition 8 | 9 | @Notification(name: UIResponder.keyboardDidShowNotification) 10 | public static var keyboardDidShowTypedNotification: TypedNotificationDefinition 11 | 12 | @Notification(name: UIResponder.keyboardWillHideNotification) 13 | public static var keyboardWillHideTypedNotification: TypedNotificationDefinition 14 | 15 | @Notification(name: UIResponder.keyboardDidHideNotification) 16 | public static var keyboardDidHideTypedNotification: TypedNotificationDefinition 17 | 18 | @Notification(name: UIResponder.keyboardWillChangeFrameNotification) 19 | public static var keyboardWillChangeFrameTypedNotification: TypedNotificationDefinition 20 | 21 | @Notification(name: UIResponder.keyboardDidChangeFrameNotification) 22 | public static var keyboardDidChangeFrameTypedNotification: TypedNotificationDefinition 23 | } 24 | 25 | public struct KeyboardNotificationStorage: UserInfoRepresentable { 26 | /// A `Bool` value that indicates whether the keyboard belongs to the current app. 27 | public let isLocal: Bool? 28 | 29 | /// The keyboard’s frame at the beginning of its animation. 30 | public let frameBegin: CGRect? 31 | 32 | /// The keyboard’s frame at the end of its animation. 33 | public let frameEnd: CGRect? 34 | 35 | /// The animation curve that the system uses to animate the keyboard onto or off the screen. 36 | public let animationOptions: UIView.AnimationOptions? 37 | 38 | /// The duration of the keyboard animation in seconds. 39 | public let animationDuration: TimeInterval? 40 | 41 | public init( 42 | isLocal: Bool?, 43 | frameBegin: CGRect?, 44 | frameEnd: CGRect?, 45 | animationOptions: UIView.AnimationOptions?, 46 | animationDuration: Double? 47 | ) { 48 | self.isLocal = isLocal 49 | self.frameBegin = frameBegin 50 | self.frameEnd = frameEnd 51 | self.animationOptions = animationOptions 52 | self.animationDuration = animationDuration 53 | } 54 | 55 | public init(userInfo: [AnyHashable: Any]) { 56 | self.isLocal = userInfo[UIResponder.keyboardIsLocalUserInfoKey] as? Bool 57 | self.frameBegin = userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect 58 | self.frameEnd = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect 59 | 60 | let rawAnimationOptions = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt 61 | self.animationOptions = rawAnimationOptions.map { UIView.AnimationOptions(rawValue: $0 << 16) } 62 | self.animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double 63 | } 64 | 65 | public func convertToUserInfo() -> [AnyHashable : Any] { 66 | let candidate: [AnyHashable: Any?] = [ 67 | UIResponder.keyboardIsLocalUserInfoKey: isLocal, 68 | UIResponder.keyboardFrameBeginUserInfoKey: frameBegin, 69 | UIResponder.keyboardFrameEndUserInfoKey: frameEnd, 70 | UIResponder.keyboardAnimationCurveUserInfoKey: animationOptions.map { $0.rawValue >> 16 }, 71 | UIResponder.keyboardAnimationDurationUserInfoKey: animationDuration 72 | ] 73 | return candidate.compactMapValues { $0 } 74 | } 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /Sources/TypedNotifications/UIKit/UIScene.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(visionOS) || os(tvOS) || targetEnvironment(macCatalyst) 2 | import UIKit 3 | 4 | extension UIScene { 5 | @Notification(name: UIScene.willConnectNotification) 6 | public static var willConnectTypedNotification: TypedNotificationDefinition 7 | 8 | @Notification(name: UIScene.didActivateNotification) 9 | public static var didActivateTypedNotification: TypedNotificationDefinition 10 | 11 | @Notification(name: UIScene.didDisconnectNotification) 12 | public static var didDisconnectTypedNotification: TypedNotificationDefinition 13 | 14 | @Notification(name: UIScene.willEnterForegroundNotification) 15 | public static var willEnterForegroundTypedNotification: TypedNotificationDefinition 16 | 17 | @Notification(name: UIScene.willDeactivateNotification) 18 | public static var willDeactivateTypedNotification: TypedNotificationDefinition 19 | 20 | @Notification(name: UIScene.didEnterBackgroundNotification) 21 | public static var didEnterBackgroundTypedNotification: TypedNotificationDefinition 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /Sources/TypedNotificationsMacro/NotificationMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | 5 | public struct NotificationMacro: AccessorMacro { 6 | public static func expansion( 7 | of node: AttributeSyntax, 8 | providingAccessorsOf declaration: some DeclSyntaxProtocol, 9 | in context: some MacroExpansionContext 10 | ) throws -> [AccessorDeclSyntax] { 11 | guard let variableDeclSyntax = declaration.as(VariableDeclSyntax.self) else { 12 | context.addDiagnostics( 13 | from: MessageError(description: "@Notification supports only variable."), 14 | node: declaration 15 | ) 16 | return [] 17 | } 18 | 19 | guard let binding = variableDeclSyntax.bindings.first else { 20 | context.addDiagnostics( 21 | from: MessageError(description: "@Notification requires a binding"), 22 | node: variableDeclSyntax 23 | ) 24 | return [] 25 | } 26 | 27 | guard variableDeclSyntax.bindings.count == 1 else { 28 | context.addDiagnostics( 29 | from: MessageError(description: "@Notification doesn't support multiple binding"), 30 | node: variableDeclSyntax.bindings 31 | ) 32 | return [] 33 | } 34 | 35 | guard let type = binding.typeAnnotation?.type else { 36 | context.addDiagnostics( 37 | from: MessageError(description: "@Notification requires type annotation."), 38 | node: binding 39 | ) 40 | return [] 41 | } 42 | 43 | let name: any SyntaxProtocol 44 | if let labeledNameNode = node.arguments?.as(LabeledExprListSyntax.self)?.first?.expression { 45 | name = labeledNameNode 46 | } else if let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier { 47 | name = StringLiteralExprSyntax(content: identifier.text) 48 | } else { 49 | context.addDiagnostics( 50 | from: MessageError(description: "No name information."), 51 | node: binding 52 | ) 53 | return [] 54 | } 55 | 56 | return [ 57 | """ 58 | get { 59 | \(type)(name: \(name)) 60 | } 61 | """ 62 | ] 63 | } 64 | } 65 | 66 | struct MessageError: Error, CustomStringConvertible { 67 | var description: String 68 | } 69 | -------------------------------------------------------------------------------- /Sources/TypedNotificationsMacro/TypedNotificationsMacros.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntaxMacros 3 | 4 | @main 5 | struct TypedNotificationsMacroPlugin: CompilerPlugin { 6 | let providingMacros: [Macro.Type] = [ 7 | NotificationMacro.self 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /Tests/TypedNotificationsMacroTests/NotificationMacroTest.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntaxMacros 2 | import SwiftSyntaxMacrosTestSupport 3 | import XCTest 4 | 5 | #if canImport(TypedNotificationsMacro) 6 | import TypedNotificationsMacro 7 | 8 | private let testMacros: [String: Macro.Type] = [ 9 | "Notification": NotificationMacro.self, 10 | ] 11 | #endif 12 | 13 | final class NotificationMacroTests: XCTestCase { 14 | 15 | func testNotificationMacro() throws { 16 | #if canImport(TypedNotificationsMacro) 17 | assertMacroExpansion( 18 | """ 19 | @Notification 20 | static var userWillUpdate: TypedNotificationDefinition 21 | """, 22 | expandedSource: 23 | """ 24 | static var userWillUpdate: TypedNotificationDefinition { 25 | get { 26 | TypedNotificationDefinition(name: "userWillUpdate") 27 | } 28 | } 29 | """, 30 | macros: testMacros 31 | ) 32 | #else 33 | throw XCTSkip("macros are only supported when running tests for the host platform") 34 | #endif 35 | } 36 | 37 | func testNotificationWithGivenNameMacro() throws { 38 | #if canImport(TypedNotificationsMacro) 39 | assertMacroExpansion( 40 | """ 41 | @Notification("custom") 42 | static var userWillUpdate: TypedNotificationDefinition 43 | """, 44 | expandedSource: 45 | """ 46 | static var userWillUpdate: TypedNotificationDefinition { 47 | get { 48 | TypedNotificationDefinition(name: "custom") 49 | } 50 | } 51 | """, 52 | macros: testMacros 53 | ) 54 | #else 55 | throw XCTSkip("macros are only supported when running tests for the host platform") 56 | #endif 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/TypedNotificationsTests/TypedNotificationsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import UserInfoRepresentable 3 | @testable import TypedNotifications 4 | 5 | final class TypedNotificationsTests: XCTestCase { 6 | 7 | @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) 8 | func testNotifications() async { 9 | let center = NotificationCenter() 10 | let expectationA = expectation(description: "Task is called") 11 | let expectationB = expectation(description: "receive a foo notification") 12 | Task { 13 | expectationA.fulfill() 14 | let notification = await center.notifications(for: .foo).first(where: { _ in true }) 15 | XCTAssertEqual(notification?.name.rawValue, "Foo") 16 | XCTAssertEqual(notification?.storage, 123) 17 | XCTAssertEqual(notification?.object?.value, "sender") 18 | expectationB.fulfill() 19 | } 20 | await fulfillment(of: [expectationA], timeout: 1) 21 | let foo = Foo(value: "sender") 22 | center.post(.foo, storage: 123, object: foo) 23 | await fulfillment(of: [expectationB], timeout: 1) 24 | } 25 | 26 | func testPublishers() { 27 | let center = NotificationCenter() 28 | let expectation = expectation(description: "receive a foo notification") 29 | let cancellable = center.publisher(for: .foo) 30 | .sink { notification in 31 | XCTAssertEqual(notification.name.rawValue, "Foo") 32 | XCTAssertEqual(notification.storage, 123) 33 | XCTAssertEqual(notification.object?.value, "sender") 34 | expectation.fulfill() 35 | } 36 | let foo = Foo(value: "sender") 37 | center.post(.foo, storage: 123, object: foo) 38 | wait(for: [expectation], timeout: 1) 39 | _ = cancellable 40 | } 41 | 42 | func testNoStorage() { 43 | let center = NotificationCenter() 44 | let expectation = expectation(description: "receive an empty notification") 45 | let cancellable = center.publisher(for: .noStorage) 46 | .sink { notification in 47 | XCTAssertEqual(notification.name.rawValue, "noStorage") 48 | XCTAssertEqual(notification.object?.value, "sender") 49 | expectation.fulfill() 50 | } 51 | let foo = Foo(value: "sender") 52 | center.post(name: .init("noStorage"), object: foo, userInfo: [:]) 53 | wait(for: [expectation], timeout: 1) 54 | _ = cancellable 55 | } 56 | 57 | func testCustomUserInfo() { 58 | let center = NotificationCenter() 59 | let expectation = expectation(description: "receive an empty notification") 60 | let cancellable = center.publisher(for: .customUserInfo) 61 | .sink { notification in 62 | XCTAssertEqual(notification.name.rawValue, "customUserInfo") 63 | XCTAssertEqual(notification.object?.value, "sender") 64 | XCTAssertEqual(notification.storage.value, "userInfo") 65 | expectation.fulfill() 66 | } 67 | let foo = Foo(value: "sender") 68 | center.post(.customUserInfo, storage: CustomUserInfo(value: "userInfo"), object: foo) 69 | wait(for: [expectation], timeout: 1) 70 | _ = cancellable 71 | } 72 | 73 | func testMacro() throws { 74 | #if canImport(TypedNotificationsMacro) 75 | let center = NotificationCenter() 76 | let expectation = expectation(description: "receive an empty notification") 77 | let cancellable = center.publisher(for: .macroNotification) 78 | .sink { notification in 79 | XCTAssertEqual(notification.name.rawValue, "macroNotification") 80 | XCTAssertEqual(notification.object?.value, "sender") 81 | expectation.fulfill() 82 | } 83 | let foo = Foo(value: "sender") 84 | center.post(.macroNotification, object: foo) 85 | wait(for: [expectation], timeout: 1) 86 | _ = cancellable 87 | #else 88 | throw XCTSkip("Macro is not support on the current test environment.") 89 | #endif 90 | } 91 | 92 | func testMacroWithName() throws { 93 | #if canImport(TypedNotificationsMacro) 94 | let center = NotificationCenter() 95 | let expectation = expectation(description: "receive an empty notification") 96 | let cancellable = center.publisher(for: .macroNotificationWithName) 97 | .sink { notification in 98 | XCTAssertEqual(notification.name.rawValue, "customName") 99 | XCTAssertEqual(notification.object?.value, "sender") 100 | XCTAssertEqual(notification.storage.value, "userInfo") 101 | expectation.fulfill() 102 | } 103 | let foo = Foo(value: "sender") 104 | center.post(.macroNotificationWithName, storage: CustomUserInfo(value: "userInfo"), object: foo) 105 | wait(for: [expectation], timeout: 1) 106 | _ = cancellable 107 | #else 108 | throw XCTSkip("Macro is not support on the current test environment.") 109 | #endif 110 | } 111 | } 112 | 113 | final class Foo: Sendable { 114 | let value: String 115 | 116 | init(value: String) { 117 | self.value = value 118 | } 119 | } 120 | 121 | extension TypedNotificationDefinition { 122 | static var foo: TypedNotificationDefinition { 123 | .init(name: "Foo") { number in 124 | ["number": number] 125 | } decode: { userInfo in 126 | userInfo!["number"] as! Int 127 | } 128 | } 129 | 130 | static var noStorage: TypedNotificationDefinition { 131 | .init(name: "noStorage") 132 | } 133 | 134 | static var customUserInfo: TypedNotificationDefinition { 135 | .init(name: "customUserInfo") 136 | } 137 | 138 | @Notification 139 | static var macroNotification: TypedNotificationDefinition 140 | 141 | @Notification(name: "customName") 142 | static var macroNotificationWithName: TypedNotificationDefinition 143 | } 144 | 145 | 146 | @UserInfoRepresentable 147 | struct CustomUserInfo { 148 | let value: String 149 | 150 | init(value: String) { 151 | self.value = value 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /TypedNotification.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TypedNotification.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TypedNotification.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-docc-plugin", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-docc-plugin", 7 | "state" : { 8 | "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", 9 | "version" : "1.3.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-docc-symbolkit", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-docc-symbolkit", 16 | "state" : { 17 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 18 | "version" : "1.0.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-syntax", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-syntax.git", 25 | "state" : { 26 | "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", 27 | "version" : "509.0.2" 28 | } 29 | }, 30 | { 31 | "identity" : "userinfo-representable", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/mtj0928/userinfo-representable", 34 | "state" : { 35 | "revision" : "c7644d3e0c40cd9bf886f978d6916e03166e937c", 36 | "version" : "1.0.0" 37 | } 38 | } 39 | ], 40 | "version" : 2 41 | } 42 | --------------------------------------------------------------------------------