├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── docc.yml ├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── SystemNotification.xcscheme ├── Demo ├── Demo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Demo.xcscheme │ └── xcuserdata │ │ └── danielsaidi.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── Demo │ ├── ContentView.swift │ ├── Demo.entitlements │ ├── DemoApp.swift │ ├── Localizable.xcstrings │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── Resources │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon-Vision.solidimagestack │ │ ├── Back.solidimagestacklayer │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Icon-visionOS-Back.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Front.solidimagestacklayer │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Icon-visionOS-Front.png │ │ │ └── Contents.json │ │ └── Middle.solidimagestacklayer │ │ │ ├── Content.imageset │ │ │ ├── Contents.json │ │ │ └── Icon-visionOS-Middle.png │ │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-iOS-1024.png │ │ ├── Icon-macOS-1024.png │ │ ├── Icon-macOS-128.png │ │ ├── Icon-macOS-16.png │ │ ├── Icon-macOS-256.png │ │ ├── Icon-macOS-32.png │ │ ├── Icon-macOS-512.png │ │ └── Icon-macOS-64.png │ └── Contents.json │ └── Image+Demo.swift ├── LICENSE ├── Package.swift ├── README.md ├── RELEASE_NOTES.md ├── Resources ├── Demo.gif └── Icon.png ├── Sources └── SystemNotification │ ├── Overlays │ ├── SystemNotificationAppKitOverlay.swift │ └── SystemNotificationUIKitOverlay.swift │ ├── SystemNotification.docc │ ├── Articles │ │ ├── Demo-Article.md │ │ └── Getting Started.md │ ├── Resources │ │ ├── Logo.png │ │ └── Page.png │ └── SystemNotification.md │ ├── SystemNotification.swift │ ├── SystemNotificationConfiguration.swift │ ├── SystemNotificationContext.swift │ ├── SystemNotificationEdge.swift │ ├── SystemNotificationMessage+Predefined.swift │ ├── SystemNotificationMessage.swift │ ├── SystemNotificationMessageStyle.swift │ ├── SystemNotificationStyle.swift │ └── View+SystemNotification.swift ├── Tests └── SystemNotificationTests │ └── SystemNotificationTests.swift ├── package_version.sh └── scripts ├── build.sh ├── chmod.sh ├── docc.sh ├── framework.sh ├── git_default_branch.sh ├── package_docc.sh ├── package_framework.sh ├── package_name.sh ├── package_version.sh ├── sync_from.sh ├── test.sh ├── version.sh ├── version_bump.sh ├── version_number.sh ├── version_validate_git.sh └── version_validate_target.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [danielsaidi] 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow builds and tests the project. 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Build Runner 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | jobs: 13 | build: 14 | runs-on: macos-15 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: maxim-lobanov/setup-xcode@v1 18 | with: 19 | xcode-version: latest-stable 20 | - name: Build all platforms 21 | run: bash scripts/build.sh ${{ github.event.repository.name }} 22 | - name: Test iOS 23 | run: bash scripts/test.sh ${{ github.event.repository.name }} 24 | -------------------------------------------------------------------------------- /.github/workflows/docc.yml: -------------------------------------------------------------------------------- 1 | # This workflow builds publish DocC docs to GitHub Pages. 2 | # Source: https://maxxfrazer.medium.com/deploying-docc-with-github-actions-218c5ca6cad5 3 | # Sample: https://github.com/AgoraIO-Community/VideoUIKit-iOS/blob/main/.github/workflows/deploy_docs.yml 4 | 5 | name: DocC Runner 6 | 7 | on: 8 | push: 9 | branches: ["main"] 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow one concurrent deployment 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | deploy: 24 | environment: 25 | name: github-pages 26 | url: ${{ steps.deployment.outputs.page_url }} 27 | runs-on: macos-15 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | - id: pages 32 | name: Setup Pages 33 | uses: actions/configure-pages@v4 34 | - name: Select Xcode version 35 | uses: maxim-lobanov/setup-xcode@v1 36 | with: 37 | xcode-version: latest-stable 38 | - name: Build DocC 39 | run: bash scripts/docc.sh ${{ github.event.repository.name }} 40 | - name: Upload artifact 41 | uses: actions/upload-pages-artifact@v3 42 | with: 43 | path: '.build/docs-iOS' 44 | - id: deployment 45 | name: Deploy to GitHub Pages 46 | uses: actions/deploy-pages@v4 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | .swiftpm/ 5 | xcuserdata/ 6 | DerivedData/ -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - line_length 4 | - vertical_whitespace 5 | 6 | identifier_name: 7 | excluded: 8 | - id -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/SystemNotification.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 60; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A954CAF7297553B8008C1E41 /* SystemNotification in Frameworks */ = {isa = PBXBuildFile; productRef = A954CAF6297553B8008C1E41 /* SystemNotification */; }; 11 | A9578C232CB2BE04008F777C /* SystemNotification in Frameworks */ = {isa = PBXBuildFile; productRef = A9578C222CB2BE04008F777C /* SystemNotification */; }; 12 | A95F1E562975412D00383605 /* DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95F1E552975412D00383605 /* DemoApp.swift */; }; 13 | A95F1E582975412D00383605 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95F1E572975412D00383605 /* ContentView.swift */; }; 14 | A95F1E5A2975412E00383605 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A95F1E592975412E00383605 /* Assets.xcassets */; }; 15 | A95F1E5E2975412E00383605 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A95F1E5D2975412E00383605 /* Preview Assets.xcassets */; }; 16 | A95F1E6E29754B2A00383605 /* Image+Demo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95F1E6D29754B2A00383605 /* Image+Demo.swift */; }; 17 | A9D1F4FA2BD7D19300E1C3D9 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A9D1F4F92BD7D19300E1C3D9 /* Localizable.xcstrings */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | A95F1E522975412D00383605 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | A95F1E552975412D00383605 /* DemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoApp.swift; sourceTree = ""; }; 23 | A95F1E572975412D00383605 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 24 | A95F1E592975412E00383605 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | A95F1E5B2975412E00383605 /* Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Demo.entitlements; sourceTree = ""; }; 26 | A95F1E5D2975412E00383605 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 27 | A95F1E6D29754B2A00383605 /* Image+Demo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Image+Demo.swift"; sourceTree = ""; }; 28 | A9D1F4F92BD7D19300E1C3D9 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | A95F1E4F2975412D00383605 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | A9578C232CB2BE04008F777C /* SystemNotification in Frameworks */, 37 | A954CAF7297553B8008C1E41 /* SystemNotification in Frameworks */, 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | A95F1E492975412D00383605 = { 45 | isa = PBXGroup; 46 | children = ( 47 | A95F1E542975412D00383605 /* Demo */, 48 | A95F1E532975412D00383605 /* Products */, 49 | A95F1E7229754BDB00383605 /* Frameworks */, 50 | ); 51 | sourceTree = ""; 52 | }; 53 | A95F1E532975412D00383605 /* Products */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | A95F1E522975412D00383605 /* Demo.app */, 57 | ); 58 | name = Products; 59 | sourceTree = ""; 60 | }; 61 | A95F1E542975412D00383605 /* Demo */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | A95F1E6C29754B1200383605 /* Resources */, 65 | A95F1E7129754BB400383605 /* Supporting Files */, 66 | A95F1E572975412D00383605 /* ContentView.swift */, 67 | A95F1E552975412D00383605 /* DemoApp.swift */, 68 | A95F1E5C2975412E00383605 /* Preview Content */, 69 | ); 70 | path = Demo; 71 | sourceTree = ""; 72 | }; 73 | A95F1E5C2975412E00383605 /* Preview Content */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | A95F1E5D2975412E00383605 /* Preview Assets.xcassets */, 77 | ); 78 | path = "Preview Content"; 79 | sourceTree = ""; 80 | }; 81 | A95F1E6C29754B1200383605 /* Resources */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | A95F1E592975412E00383605 /* Assets.xcassets */, 85 | A95F1E6D29754B2A00383605 /* Image+Demo.swift */, 86 | ); 87 | path = Resources; 88 | sourceTree = ""; 89 | }; 90 | A95F1E7129754BB400383605 /* Supporting Files */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | A95F1E5B2975412E00383605 /* Demo.entitlements */, 94 | A9D1F4F92BD7D19300E1C3D9 /* Localizable.xcstrings */, 95 | ); 96 | name = "Supporting Files"; 97 | sourceTree = ""; 98 | }; 99 | A95F1E7229754BDB00383605 /* Frameworks */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | ); 103 | name = Frameworks; 104 | sourceTree = ""; 105 | }; 106 | /* End PBXGroup section */ 107 | 108 | /* Begin PBXNativeTarget section */ 109 | A95F1E512975412D00383605 /* Demo */ = { 110 | isa = PBXNativeTarget; 111 | buildConfigurationList = A95F1E612975412E00383605 /* Build configuration list for PBXNativeTarget "Demo" */; 112 | buildPhases = ( 113 | A95F1E4E2975412D00383605 /* Sources */, 114 | A95F1E4F2975412D00383605 /* Frameworks */, 115 | A95F1E502975412D00383605 /* Resources */, 116 | ); 117 | buildRules = ( 118 | ); 119 | dependencies = ( 120 | ); 121 | name = Demo; 122 | packageProductDependencies = ( 123 | A954CAF6297553B8008C1E41 /* SystemNotification */, 124 | A9578C222CB2BE04008F777C /* SystemNotification */, 125 | ); 126 | productName = Demo; 127 | productReference = A95F1E522975412D00383605 /* Demo.app */; 128 | productType = "com.apple.product-type.application"; 129 | }; 130 | /* End PBXNativeTarget section */ 131 | 132 | /* Begin PBXProject section */ 133 | A95F1E4A2975412D00383605 /* Project object */ = { 134 | isa = PBXProject; 135 | attributes = { 136 | BuildIndependentTargetsInParallel = 1; 137 | LastSwiftUpdateCheck = 1420; 138 | LastUpgradeCheck = 1530; 139 | TargetAttributes = { 140 | A95F1E512975412D00383605 = { 141 | CreatedOnToolsVersion = 14.2; 142 | }; 143 | }; 144 | }; 145 | buildConfigurationList = A95F1E4D2975412D00383605 /* Build configuration list for PBXProject "Demo" */; 146 | compatibilityVersion = "Xcode 14.0"; 147 | developmentRegion = en; 148 | hasScannedForEncodings = 0; 149 | knownRegions = ( 150 | en, 151 | Base, 152 | ); 153 | mainGroup = A95F1E492975412D00383605; 154 | packageReferences = ( 155 | A9578C212CB2BE04008F777C /* XCLocalSwiftPackageReference "../../systemnotification" */, 156 | ); 157 | productRefGroup = A95F1E532975412D00383605 /* Products */; 158 | projectDirPath = ""; 159 | projectRoot = ""; 160 | targets = ( 161 | A95F1E512975412D00383605 /* Demo */, 162 | ); 163 | }; 164 | /* End PBXProject section */ 165 | 166 | /* Begin PBXResourcesBuildPhase section */ 167 | A95F1E502975412D00383605 /* Resources */ = { 168 | isa = PBXResourcesBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | A95F1E5E2975412E00383605 /* Preview Assets.xcassets in Resources */, 172 | A9D1F4FA2BD7D19300E1C3D9 /* Localizable.xcstrings in Resources */, 173 | A95F1E5A2975412E00383605 /* Assets.xcassets in Resources */, 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXResourcesBuildPhase section */ 178 | 179 | /* Begin PBXSourcesBuildPhase section */ 180 | A95F1E4E2975412D00383605 /* Sources */ = { 181 | isa = PBXSourcesBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | A95F1E582975412D00383605 /* ContentView.swift in Sources */, 185 | A95F1E562975412D00383605 /* DemoApp.swift in Sources */, 186 | A95F1E6E29754B2A00383605 /* Image+Demo.swift in Sources */, 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | /* End PBXSourcesBuildPhase section */ 191 | 192 | /* Begin XCBuildConfiguration section */ 193 | A95F1E5F2975412E00383605 /* Debug */ = { 194 | isa = XCBuildConfiguration; 195 | buildSettings = { 196 | ALWAYS_SEARCH_USER_PATHS = NO; 197 | CLANG_ANALYZER_NONNULL = YES; 198 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 199 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 200 | CLANG_ENABLE_MODULES = YES; 201 | CLANG_ENABLE_OBJC_ARC = YES; 202 | CLANG_ENABLE_OBJC_WEAK = YES; 203 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 204 | CLANG_WARN_BOOL_CONVERSION = YES; 205 | CLANG_WARN_COMMA = YES; 206 | CLANG_WARN_CONSTANT_CONVERSION = YES; 207 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 208 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 209 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 210 | CLANG_WARN_EMPTY_BODY = YES; 211 | CLANG_WARN_ENUM_CONVERSION = YES; 212 | CLANG_WARN_INFINITE_RECURSION = YES; 213 | CLANG_WARN_INT_CONVERSION = YES; 214 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 215 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 216 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 217 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 218 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 219 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 220 | CLANG_WARN_STRICT_PROTOTYPES = YES; 221 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 222 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 223 | CLANG_WARN_UNREACHABLE_CODE = YES; 224 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 225 | COPY_PHASE_STRIP = NO; 226 | DEAD_CODE_STRIPPING = YES; 227 | DEBUG_INFORMATION_FORMAT = dwarf; 228 | ENABLE_STRICT_OBJC_MSGSEND = YES; 229 | ENABLE_TESTABILITY = YES; 230 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 231 | GCC_C_LANGUAGE_STANDARD = gnu11; 232 | GCC_DYNAMIC_NO_PIC = NO; 233 | GCC_NO_COMMON_BLOCKS = YES; 234 | GCC_OPTIMIZATION_LEVEL = 0; 235 | GCC_PREPROCESSOR_DEFINITIONS = ( 236 | "DEBUG=1", 237 | "$(inherited)", 238 | ); 239 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 240 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 241 | GCC_WARN_UNDECLARED_SELECTOR = YES; 242 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 243 | GCC_WARN_UNUSED_FUNCTION = YES; 244 | GCC_WARN_UNUSED_VARIABLE = YES; 245 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 246 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 247 | MACOSX_DEPLOYMENT_TARGET = 12.0; 248 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 249 | MTL_FAST_MATH = YES; 250 | ONLY_ACTIVE_ARCH = YES; 251 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 252 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 253 | }; 254 | name = Debug; 255 | }; 256 | A95F1E602975412E00383605 /* Release */ = { 257 | isa = XCBuildConfiguration; 258 | buildSettings = { 259 | ALWAYS_SEARCH_USER_PATHS = NO; 260 | CLANG_ANALYZER_NONNULL = YES; 261 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 262 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 263 | CLANG_ENABLE_MODULES = YES; 264 | CLANG_ENABLE_OBJC_ARC = YES; 265 | CLANG_ENABLE_OBJC_WEAK = YES; 266 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 267 | CLANG_WARN_BOOL_CONVERSION = YES; 268 | CLANG_WARN_COMMA = YES; 269 | CLANG_WARN_CONSTANT_CONVERSION = YES; 270 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 271 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 272 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 273 | CLANG_WARN_EMPTY_BODY = YES; 274 | CLANG_WARN_ENUM_CONVERSION = YES; 275 | CLANG_WARN_INFINITE_RECURSION = YES; 276 | CLANG_WARN_INT_CONVERSION = YES; 277 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 279 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 280 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 281 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 282 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 283 | CLANG_WARN_STRICT_PROTOTYPES = YES; 284 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 285 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 286 | CLANG_WARN_UNREACHABLE_CODE = YES; 287 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 288 | COPY_PHASE_STRIP = NO; 289 | DEAD_CODE_STRIPPING = YES; 290 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 291 | ENABLE_NS_ASSERTIONS = NO; 292 | ENABLE_STRICT_OBJC_MSGSEND = YES; 293 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 294 | GCC_C_LANGUAGE_STANDARD = gnu11; 295 | GCC_NO_COMMON_BLOCKS = YES; 296 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 297 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 298 | GCC_WARN_UNDECLARED_SELECTOR = YES; 299 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 300 | GCC_WARN_UNUSED_FUNCTION = YES; 301 | GCC_WARN_UNUSED_VARIABLE = YES; 302 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 303 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 304 | MACOSX_DEPLOYMENT_TARGET = 12.0; 305 | MTL_ENABLE_DEBUG_INFO = NO; 306 | MTL_FAST_MATH = YES; 307 | SWIFT_COMPILATION_MODE = wholemodule; 308 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 309 | }; 310 | name = Release; 311 | }; 312 | A95F1E622975412E00383605 /* Debug */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; 316 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 317 | "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=xros*]" = "AppIcon-Vision"; 318 | "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=xrsimulator*]" = "AppIcon-Vision"; 319 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 320 | CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements; 321 | CODE_SIGN_STYLE = Manual; 322 | CURRENT_PROJECT_VERSION = 1; 323 | DEAD_CODE_STRIPPING = YES; 324 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; 325 | DEVELOPMENT_TEAM = ""; 326 | ENABLE_HARDENED_RUNTIME = YES; 327 | ENABLE_PREVIEWS = YES; 328 | GENERATE_INFOPLIST_FILE = YES; 329 | INFOPLIST_KEY_CFBundleDisplayName = SystemNotification; 330 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 331 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 332 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 333 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 334 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 335 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 336 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 337 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 338 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 339 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 340 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 341 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 342 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 343 | MARKETING_VERSION = 1.0; 344 | PRODUCT_BUNDLE_IDENTIFIER = com.danielsaidi.systemnotification.Demo; 345 | PRODUCT_NAME = "$(TARGET_NAME)"; 346 | PROVISIONING_PROFILE_SPECIFIER = ""; 347 | SDKROOT = auto; 348 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator driverkit iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; 349 | SUPPORTS_MACCATALYST = NO; 350 | SWIFT_EMIT_LOC_STRINGS = YES; 351 | SWIFT_VERSION = 5.0; 352 | TARGETED_DEVICE_FAMILY = "1,2,7"; 353 | }; 354 | name = Debug; 355 | }; 356 | A95F1E632975412E00383605 /* Release */ = { 357 | isa = XCBuildConfiguration; 358 | buildSettings = { 359 | ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; 360 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 361 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 362 | CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements; 363 | CODE_SIGN_STYLE = Manual; 364 | CURRENT_PROJECT_VERSION = 1; 365 | DEAD_CODE_STRIPPING = YES; 366 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; 367 | DEVELOPMENT_TEAM = ""; 368 | ENABLE_HARDENED_RUNTIME = YES; 369 | ENABLE_PREVIEWS = YES; 370 | GENERATE_INFOPLIST_FILE = YES; 371 | INFOPLIST_KEY_CFBundleDisplayName = SystemNotification; 372 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 373 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 374 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 375 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 376 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 377 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 378 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 379 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 380 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 381 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 382 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 383 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 384 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 385 | MARKETING_VERSION = 1.0; 386 | PRODUCT_BUNDLE_IDENTIFIER = com.danielsaidi.systemnotification.Demo; 387 | PRODUCT_NAME = "$(TARGET_NAME)"; 388 | PROVISIONING_PROFILE_SPECIFIER = ""; 389 | SDKROOT = auto; 390 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator driverkit iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; 391 | SUPPORTS_MACCATALYST = NO; 392 | SWIFT_EMIT_LOC_STRINGS = YES; 393 | SWIFT_VERSION = 5.0; 394 | TARGETED_DEVICE_FAMILY = "1,2,7"; 395 | }; 396 | name = Release; 397 | }; 398 | /* End XCBuildConfiguration section */ 399 | 400 | /* Begin XCConfigurationList section */ 401 | A95F1E4D2975412D00383605 /* Build configuration list for PBXProject "Demo" */ = { 402 | isa = XCConfigurationList; 403 | buildConfigurations = ( 404 | A95F1E5F2975412E00383605 /* Debug */, 405 | A95F1E602975412E00383605 /* Release */, 406 | ); 407 | defaultConfigurationIsVisible = 0; 408 | defaultConfigurationName = Release; 409 | }; 410 | A95F1E612975412E00383605 /* Build configuration list for PBXNativeTarget "Demo" */ = { 411 | isa = XCConfigurationList; 412 | buildConfigurations = ( 413 | A95F1E622975412E00383605 /* Debug */, 414 | A95F1E632975412E00383605 /* Release */, 415 | ); 416 | defaultConfigurationIsVisible = 0; 417 | defaultConfigurationName = Release; 418 | }; 419 | /* End XCConfigurationList section */ 420 | 421 | /* Begin XCLocalSwiftPackageReference section */ 422 | A9578C212CB2BE04008F777C /* XCLocalSwiftPackageReference "../../systemnotification" */ = { 423 | isa = XCLocalSwiftPackageReference; 424 | relativePath = ../../systemnotification; 425 | }; 426 | /* End XCLocalSwiftPackageReference section */ 427 | 428 | /* Begin XCSwiftPackageProductDependency section */ 429 | A954CAF6297553B8008C1E41 /* SystemNotification */ = { 430 | isa = XCSwiftPackageProductDependency; 431 | productName = SystemNotification; 432 | }; 433 | A9578C222CB2BE04008F777C /* SystemNotification */ = { 434 | isa = XCSwiftPackageProductDependency; 435 | productName = SystemNotification; 436 | }; 437 | /* End XCSwiftPackageProductDependency section */ 438 | }; 439 | rootObject = A95F1E4A2975412D00383605 /* Project object */; 440 | } 441 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/xcuserdata/danielsaidi.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Demo.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | A95F1E512975412D00383605 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo/Demo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Demo 4 | // 5 | // Created by Daniel Saidi on 2021-06-01. 6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import SystemNotification 11 | 12 | /// This is main screen is reused across all four tabs. 13 | /// 14 | /// The ``DemoApp`` has a shared ``SystemNotificationContext`` 15 | /// that it applies to the main app view. This makes the app 16 | /// able to show notifications above the tab view, so that a 17 | /// notification stays when switching tabs. 18 | /// 19 | /// Since notification styles and configurations are applied 20 | /// with view modifiers, we pass in the style if you want to 21 | /// change it. But remember that you then have to set it for 22 | /// every notification, since it persists the last value. In 23 | /// a real-world app, you should probably use separate state 24 | /// and modifiers for different types of notifications. 25 | /// 26 | /// > Important: Note how this view adds .systemNotification 27 | /// view modifiers to the sheet and full screen modals. This 28 | /// is needed, since a sheet and full screen modal creates a 29 | /// new view hierarchy. 30 | struct ContentView: View { 31 | 32 | init( 33 | style: Binding, 34 | isModal: Bool = false 35 | ) { 36 | self._style = style 37 | self.isModal = isModal 38 | } 39 | 40 | private let isModal: Bool 41 | 42 | @Binding 43 | var style: SystemNotificationStyle 44 | 45 | @Environment(\.dismiss) 46 | var dismiss 47 | 48 | @EnvironmentObject 49 | var notification: SystemNotificationContext 50 | 51 | @State 52 | var isCoverActive = false 53 | 54 | @State 55 | var isSilentModeOn = false 56 | 57 | @State 58 | var isSheetActive = false 59 | 60 | @StateObject 61 | var toast = SystemNotificationContext() 62 | 63 | var body: some View { 64 | List { 65 | Section("Section.Notifications") { 66 | Toggle(isOn: $isSilentModeOn) { 67 | label(.silentModeOff, "Toggle.SilentMode") 68 | } 69 | listItem(.flag, "Menu.LocalizedMessage", presentLocalizedMessage) 70 | listItem(.static, "Menu.CustomView", presentCustomView) 71 | } 72 | Section("Section.Predefined") { 73 | listItem(.error, "Menu.Error", presentError) 74 | listItem(.success, "Menu.Success", presentSuccess) 75 | listItem(.warning, "Menu.Warning", presentWarning) 76 | } 77 | Section("Section.Toasts") { 78 | listItem(.sheet, "Menu.BottomToast", presentBottomToast) 79 | 80 | } 81 | Section("Section.Modals") { 82 | listItem(.sheet, "Menu.Sheet", presentModalSheet) 83 | listItem(.cover, "Menu.Cover", presentModalCover) 84 | if isModal { 85 | listItem(.dismiss, "Dismiss", dismiss.callAsFunction) 86 | } 87 | } 88 | } 89 | .buttonStyle(.plain) 90 | .navigationTitle("SystemNotification") 91 | .sheet(isPresented: $isSheetActive) { 92 | ContentView(style: $style, isModal: true) 93 | .systemNotification(notification) 94 | .systemNotificationStyle(style) 95 | } 96 | #if os(iOS) 97 | .fullScreenCover(isPresented: $isCoverActive) { 98 | ContentView(style: $style, isModal: true) 99 | .systemNotification(notification) 100 | .systemNotificationStyle(style) 101 | } 102 | #endif 103 | .systemNotification(toast) 104 | .systemNotificationConfiguration(.standardToast) 105 | .onChange(of: isSilentModeOn) { _ in presentSilentMode() } 106 | } 107 | } 108 | 109 | private extension ContentView { 110 | 111 | func label(_ icon: Image, _ text: LocalizedStringKey) -> some View { 112 | Label { 113 | Text(text) 114 | } icon: { 115 | icon 116 | } 117 | } 118 | 119 | func listItem(_ icon: Image, _ text: LocalizedStringKey, _ action: @escaping () -> Void) -> some View { 120 | Button(action: action) { 121 | label(icon, text) 122 | .frame(maxWidth: .infinity, alignment: .leading) 123 | .contentShape(Rectangle()) 124 | } 125 | } 126 | } 127 | 128 | private extension ContentView { 129 | 130 | var flagView: some View { 131 | VStack(spacing: 0) { 132 | HStack(spacing: 0) { 133 | Color.blue 134 | .frame(width: 45) 135 | Color.yellow 136 | .frame(width: 15) 137 | Color.blue 138 | } 139 | .frame(height: 30) 140 | 141 | Color.yellow 142 | .frame(height: 15) 143 | 144 | HStack(spacing: 0) { 145 | Color.blue 146 | .frame(width: 45) 147 | Color.yellow 148 | .frame(width: 15) 149 | Color.blue 150 | } 151 | .frame(height: 30) 152 | } 153 | } 154 | 155 | func presentBottomToast() { 156 | toast.present { 157 | SystemNotificationMessage( 158 | title: "Message.Toast.Title", 159 | text: "Message.Toast.Text", 160 | style: .prominent(backgroundColor: .black) 161 | ) 162 | } 163 | } 164 | 165 | func presentCustomView() { 166 | notification.present( 167 | flagView 168 | ) 169 | } 170 | 171 | func presentError() { 172 | notification.presentMessage( 173 | .error( 174 | title: "Message.Error.Title", 175 | text: "Message.Error.Text" 176 | ) 177 | ) 178 | } 179 | 180 | func presentLocalizedMessage() { 181 | notification.present( 182 | SystemNotificationMessage( 183 | icon: Text("🇸🇪"), 184 | title: "Message.Localized.Title", 185 | text: "Message.Localized.Text" 186 | ) 187 | ) 188 | } 189 | 190 | func presentModalCover() { 191 | isCoverActive = true 192 | } 193 | 194 | func presentModalSheet() { 195 | isSheetActive = true 196 | } 197 | 198 | func presentSilentMode() { 199 | notification.presentMessage( 200 | .silentMode(isOn: isSilentModeOn) 201 | ) 202 | } 203 | 204 | func presentSuccess() { 205 | notification.presentMessage( 206 | .success( 207 | title: "Message.Success.Title", 208 | text: "Message.Success.Text" 209 | ) 210 | ) 211 | } 212 | 213 | func presentWarning() { 214 | notification.presentMessage( 215 | .warning( 216 | title: "Message.Warning.Title", 217 | text: "Message.Warning.Text" 218 | ) 219 | ) 220 | } 221 | } 222 | 223 | #Preview { 224 | 225 | struct Preview: View { 226 | 227 | @StateObject 228 | var notification = SystemNotificationContext() 229 | 230 | @State 231 | var style = SystemNotificationStyle.standard 232 | 233 | var body: some View { 234 | ContentView(style: $style) 235 | .systemNotification(notification) 236 | } 237 | } 238 | 239 | return Preview() 240 | } 241 | -------------------------------------------------------------------------------- /Demo/Demo/Demo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Demo/Demo/DemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoApp.swift 3 | // Demo 4 | // 5 | // Created by Daniel Saidi on 2023-01-16. 6 | // 7 | 8 | import SwiftUI 9 | import SystemNotification 10 | 11 | @main 12 | struct DemoApp: App { 13 | 14 | @StateObject 15 | private var context = SystemNotificationContext() 16 | 17 | @State 18 | private var style = SystemNotificationStyle.standard 19 | 20 | var body: some Scene { 21 | WindowGroup { 22 | content 23 | .systemNotification(context) // Context-based notifications are flexible 24 | .systemNotificationStyle(style) // This is how to set a global style 25 | .tint(.orange) 26 | } 27 | } 28 | } 29 | 30 | private extension DemoApp { 31 | 32 | /// This demo adds the context-based notification to all 33 | /// tabs, to make notifications display above all tabs. 34 | var content: some View { 35 | #if os(iOS) 36 | TabView { 37 | contentView.tabItem(1) 38 | contentView.tabItem(2) 39 | contentView.tabItem(3) 40 | contentView.tabItem(4) 41 | } 42 | #else 43 | contentView 44 | #endif 45 | } 46 | 47 | var contentView: some View { 48 | #if os(iOS) 49 | NavigationStack { 50 | ContentView(style: $style) 51 | } 52 | #else 53 | ContentView() 54 | #endif 55 | } 56 | } 57 | 58 | private extension View { 59 | 60 | func tabItem(_ index: Int) -> some View { 61 | self.tabItem { 62 | Label( 63 | "Tab \(index)", 64 | systemImage: "0\(index).circle") 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Demo/Demo/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "🇸🇪" : { 5 | 6 | }, 7 | "Dismiss" : { 8 | "localizations" : { 9 | "en" : { 10 | "stringUnit" : { 11 | "state" : "translated", 12 | "value" : "Dismiss" 13 | } 14 | } 15 | } 16 | }, 17 | "Menu.BottomToast" : { 18 | "localizations" : { 19 | "en" : { 20 | "stringUnit" : { 21 | "state" : "translated", 22 | "value" : "Present a bottom toast" 23 | } 24 | } 25 | } 26 | }, 27 | "Menu.Cover" : { 28 | "localizations" : { 29 | "en" : { 30 | "stringUnit" : { 31 | "state" : "translated", 32 | "value" : "Present a full screen cover" 33 | } 34 | } 35 | } 36 | }, 37 | "Menu.CustomView" : { 38 | "localizations" : { 39 | "en" : { 40 | "stringUnit" : { 41 | "state" : "translated", 42 | "value" : "Present a custom view" 43 | } 44 | } 45 | } 46 | }, 47 | "Menu.Error" : { 48 | "localizations" : { 49 | "en" : { 50 | "stringUnit" : { 51 | "state" : "translated", 52 | "value" : "Present an error message" 53 | } 54 | } 55 | } 56 | }, 57 | "Menu.LocalizedMessage" : { 58 | "localizations" : { 59 | "en" : { 60 | "stringUnit" : { 61 | "state" : "translated", 62 | "value" : "Present a localized message" 63 | } 64 | } 65 | } 66 | }, 67 | "Menu.Sheet" : { 68 | "localizations" : { 69 | "en" : { 70 | "stringUnit" : { 71 | "state" : "translated", 72 | "value" : "Present a sheet" 73 | } 74 | } 75 | } 76 | }, 77 | "Menu.Success" : { 78 | "localizations" : { 79 | "en" : { 80 | "stringUnit" : { 81 | "state" : "translated", 82 | "value" : "Present a success message" 83 | } 84 | } 85 | } 86 | }, 87 | "Menu.Warning" : { 88 | "localizations" : { 89 | "en" : { 90 | "stringUnit" : { 91 | "state" : "translated", 92 | "value" : "Present a warning" 93 | } 94 | } 95 | } 96 | }, 97 | "Message.Error.Text" : { 98 | "localizations" : { 99 | "en" : { 100 | "stringUnit" : { 101 | "state" : "translated", 102 | "value" : "Something went wrong" 103 | } 104 | } 105 | } 106 | }, 107 | "Message.Error.Title" : { 108 | "localizations" : { 109 | "en" : { 110 | "stringUnit" : { 111 | "state" : "translated", 112 | "value" : "Error!" 113 | } 114 | } 115 | } 116 | }, 117 | "Message.Localized.Text" : { 118 | "localizations" : { 119 | "en" : { 120 | "stringUnit" : { 121 | "state" : "translated", 122 | "value" : "This notification is localized" 123 | } 124 | } 125 | } 126 | }, 127 | "Message.Localized.Title" : { 128 | "localizations" : { 129 | "en" : { 130 | "stringUnit" : { 131 | "state" : "translated", 132 | "value" : "Localization Support!" 133 | } 134 | } 135 | } 136 | }, 137 | "Message.Success.Text" : { 138 | "localizations" : { 139 | "en" : { 140 | "stringUnit" : { 141 | "state" : "translated", 142 | "value" : "This was a big success" 143 | } 144 | } 145 | } 146 | }, 147 | "Message.Success.Title" : { 148 | "localizations" : { 149 | "en" : { 150 | "stringUnit" : { 151 | "state" : "translated", 152 | "value" : "Yay!" 153 | } 154 | } 155 | } 156 | }, 157 | "Message.Toast.Text" : { 158 | "localizations" : { 159 | "en" : { 160 | "stringUnit" : { 161 | "state" : "translated", 162 | "value" : "This black toast is presented from the bottom" 163 | } 164 | } 165 | } 166 | }, 167 | "Message.Toast.Title" : { 168 | "localizations" : { 169 | "en" : { 170 | "stringUnit" : { 171 | "state" : "translated", 172 | "value" : "Toastie!" 173 | } 174 | } 175 | } 176 | }, 177 | "Message.Warning.Text" : { 178 | "localizations" : { 179 | "en" : { 180 | "stringUnit" : { 181 | "state" : "translated", 182 | "value" : "Look at the orange color - this is a serious message!" 183 | } 184 | } 185 | } 186 | }, 187 | "Message.Warning.Title" : { 188 | "localizations" : { 189 | "en" : { 190 | "stringUnit" : { 191 | "state" : "translated", 192 | "value" : "Warning, warning!" 193 | } 194 | } 195 | } 196 | }, 197 | "Section.Modals" : { 198 | "localizations" : { 199 | "en" : { 200 | "stringUnit" : { 201 | "state" : "translated", 202 | "value" : "Modals" 203 | } 204 | } 205 | } 206 | }, 207 | "Section.Notifications" : { 208 | "localizations" : { 209 | "en" : { 210 | "stringUnit" : { 211 | "state" : "translated", 212 | "value" : "Notifications" 213 | } 214 | } 215 | } 216 | }, 217 | "Section.Predefined" : { 218 | "localizations" : { 219 | "en" : { 220 | "stringUnit" : { 221 | "state" : "translated", 222 | "value" : "Predefined message types" 223 | } 224 | } 225 | } 226 | }, 227 | "Section.Toasts" : { 228 | "localizations" : { 229 | "en" : { 230 | "stringUnit" : { 231 | "state" : "translated", 232 | "value" : "Toasts" 233 | } 234 | } 235 | } 236 | }, 237 | "SystemNotification" : { 238 | 239 | }, 240 | "Tab %lld" : { 241 | 242 | }, 243 | "Toggle.SilentMode" : { 244 | "localizations" : { 245 | "en" : { 246 | "stringUnit" : { 247 | "state" : "translated", 248 | "value" : "Silent Mode" 249 | } 250 | } 251 | } 252 | } 253 | }, 254 | "version" : "1.0" 255 | } -------------------------------------------------------------------------------- /Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/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 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-visionOS-Back.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Back.solidimagestacklayer/Content.imageset/Icon-visionOS-Back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Back.solidimagestacklayer/Content.imageset/Icon-visionOS-Back.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Back.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.solidimagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.solidimagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.solidimagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-visionOS-Front.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Front.solidimagestacklayer/Content.imageset/Icon-visionOS-Front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Front.solidimagestacklayer/Content.imageset/Icon-visionOS-Front.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Front.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-visionOS-Middle.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Icon-visionOS-Middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Icon-visionOS-Middle.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon-Vision.solidimagestack/Middle.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-iOS-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "filename" : "Icon-macOS-16.png", 11 | "idiom" : "mac", 12 | "scale" : "1x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-macOS-32.png", 17 | "idiom" : "mac", 18 | "scale" : "2x", 19 | "size" : "16x16" 20 | }, 21 | { 22 | "filename" : "Icon-macOS-32.png", 23 | "idiom" : "mac", 24 | "scale" : "1x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-macOS-64.png", 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "32x32" 32 | }, 33 | { 34 | "filename" : "Icon-macOS-128.png", 35 | "idiom" : "mac", 36 | "scale" : "1x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-macOS-256.png", 41 | "idiom" : "mac", 42 | "scale" : "2x", 43 | "size" : "128x128" 44 | }, 45 | { 46 | "filename" : "Icon-macOS-256.png", 47 | "idiom" : "mac", 48 | "scale" : "1x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Icon-macOS-512.png", 53 | "idiom" : "mac", 54 | "scale" : "2x", 55 | "size" : "256x256" 56 | }, 57 | { 58 | "filename" : "Icon-macOS-512.png", 59 | "idiom" : "mac", 60 | "scale" : "1x", 61 | "size" : "512x512" 62 | }, 63 | { 64 | "filename" : "Icon-macOS-1024.png", 65 | "idiom" : "mac", 66 | "scale" : "2x", 67 | "size" : "512x512" 68 | } 69 | ], 70 | "info" : { 71 | "author" : "xcode", 72 | "version" : 1 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-iOS-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-iOS-1024.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-1024.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-128.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-16.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-256.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-32.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-512.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Demo/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-macOS-64.png -------------------------------------------------------------------------------- /Demo/Demo/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Resources/Image+Demo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image+Demo.swift 3 | // Demo 4 | // 5 | // Created by Daniel Saidi on 2021-06-08. 6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Image { 12 | 13 | static let cover = Image(systemName: "rectangle.inset.fill") 14 | static let dismiss = Image(systemName: "xmark.circle") 15 | static let error = Image(systemName: "xmark.octagon") 16 | static let flag = Image(systemName: "flag") 17 | static let globe = Image(systemName: "globe") 18 | static let sheet = Image(systemName: "rectangle.bottomthird.inset.fill") 19 | static let silentModeOff = Image(systemName: "bell.fill") 20 | static let silentModeOn = Image(systemName: "bell.slash.fill") 21 | static let `static` = Image(systemName: "viewfinder") 22 | static let success = Image(systemName: "checkmark") 23 | static let warning = Image(systemName: "exclamationmark.triangle") 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Daniel Saidi 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 PackageDescription 4 | 5 | let package = Package( 6 | name: "SystemNotification", 7 | platforms: [ 8 | .iOS(.v15), 9 | .macOS(.v12), 10 | .tvOS(.v15), 11 | .watchOS(.v10), 12 | .visionOS(.v1) 13 | ], 14 | products: [ 15 | .library( 16 | name: "SystemNotification", 17 | targets: ["SystemNotification"] 18 | ) 19 | ], 20 | targets: [ 21 | .target( 22 | name: "SystemNotification" 23 | ), 24 | .testTarget( 25 | name: "SystemNotificationTests", 26 | dependencies: ["SystemNotification"] 27 | ) 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Project Icon 3 |

4 | 5 |

6 | Version 7 | Swift 6.0 8 | Swift UI 9 | Documentation 10 | MIT License 11 | Sponsor my work 12 |

13 | 14 | 15 | 16 | # SystemNotification 17 | 18 | SystemNotification is a SwiftUI library that lets you mimic the native iOS system notification that is presented when you toggle silent mode, connect your AirPods, etc. 19 | 20 |

21 | 22 |

23 | 24 | System notifications can be styled and customized. You can use a native-looking `SystemNotificationMessage` view as the content view, or any custom view. 25 | 26 | 27 | 28 | ## Installation 29 | 30 | SystemNotification can be installed with the Swift Package Manager: 31 | 32 | ``` 33 | https://github.com/danielsaidi/SystemNotification.git 34 | ``` 35 | 36 | 37 | ## Support My Work 38 | 39 | You can [become a sponsor][Sponsors] to help me dedicate more time on my various [open-source tools][OpenSource]. Every contribution, no matter the size, makes a real difference in keeping these tools free and actively developed. 40 | 41 | 42 | 43 | ## Getting started 44 | 45 | With SystemNotification, you can add a system notification to any view just as you add a `sheet`, `alert` and `fullScreenModal`, by applying a `systemNotification` view modifier (preferably to the application root view). 46 | 47 | State-based notifications take a boolean state binding and a view builder: 48 | 49 | ```swift 50 | import SystemNotification 51 | 52 | struct MyView: View { 53 | 54 | @State 55 | var isActive = false 56 | 57 | var body: some View { 58 | VStack { 59 | Button("Show notification") { 60 | isActive = true 61 | } 62 | } 63 | .systemNotification(isActive: $isActive) { 64 | Text("You can use any custom content view") 65 | .padding() 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | Context-based notifications just take a `SystemNotificationContext` instance and can then show many different notifications with a single modifier: 72 | 73 | ```swift 74 | import SystemNotification 75 | 76 | struct MyView: View { 77 | 78 | @StateObject 79 | var notification = SystemNotificationContext() 80 | 81 | var body: some View { 82 | VStack { 83 | Button("Show text") { 84 | notification.present { 85 | Text("Context-based notifications are more flexible.") 86 | .padding() 87 | .multilineTextAlignment(.center) 88 | } 89 | } 90 | Button("Show message") { 91 | notification.present { 92 | SystemNotificationMessage( 93 | icon: Text("👍"), 94 | title: "Great job!", 95 | text: "You presented a native-looking message!" 96 | ) 97 | } 98 | } 99 | } 100 | .systemNotification(notification) 101 | } 102 | } 103 | ``` 104 | 105 | The `SystemNotificationMessage` view lets you easily mimic a native notification view, with an icon, title and text, but you can use any custom view as the notification body. 106 | 107 | See the online [getting started guide][Getting-Started] for more information. 108 | 109 | 110 | 111 | ## Documentation 112 | 113 | The online [documentation][Documentation] has more information, articles, code examples, etc. 114 | 115 | 116 | 117 | ## Demo Application 118 | 119 | The `Demo` folder has an app that lets you explore the library. 120 | 121 | 122 | 123 | ## Contact 124 | 125 | Feel free to reach out if you have questions, or want to contribute in any way: 126 | 127 | * Website: [danielsaidi.com][Website] 128 | * E-mail: [daniel.saidi@gmail.com][Email] 129 | * Bluesky: [@danielsaidi@bsky.social][Bluesky] 130 | * Mastodon: [@danielsaidi@mastodon.social][Mastodon] 131 | 132 | 133 | 134 | ## License 135 | 136 | SystemNotification is available under the MIT license. See the [LICENSE][License] file for more info. 137 | 138 | 139 | 140 | [Email]: mailto:daniel.saidi@gmail.com 141 | [Website]: https://danielsaidi.com 142 | [GitHub]: https://github.com/danielsaidi 143 | [OpenSource]: https://danielsaidi.com/opensource 144 | [Sponsors]: https://github.com/sponsors/danielsaidi 145 | 146 | [Bluesky]: https://bsky.app/profile/danielsaidi.bsky.social 147 | [Mastodon]: https://mastodon.social/@danielsaidi 148 | [Twitter]: https://twitter.com/danielsaidi 149 | 150 | [Documentation]: https://danielsaidi.github.io/SystemNotification 151 | [Getting-Started]: https://danielsaidi.github.io/SystemNotification/documentation/systemnotification/getting-started 152 | [License]: https://github.com/danielsaidi/SystemNotification/blob/master/LICENSE 153 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | SystemNotification will use semver after 1.0. 4 | 5 | Until then, breaking changes can happen in any version, and deprecated features may be removed in any minor version bump. 6 | 7 | 8 | 9 | ## 1.3 10 | 11 | This version changes the default animation to `.bouncy`. 12 | 13 | 14 | 15 | ## 1.2.1 16 | 17 | This patch lets you provide a custom bundle to `SystemNotificatonMessage`. 18 | 19 | 20 | 21 | ## 1.2 22 | 23 | This version renames the `master` branch to `main` and updates to Swift 6. 24 | 25 | 26 | 27 | ## 1.1.2 28 | 29 | Thanks to @martindufort there's now an AppKit-specific overlay. 30 | 31 | ### ✨ New features 32 | 33 | `SystemNotificationAppKitOverlay` is a new AppKit-specific overlay view. This version renames the `master` branch to `main` and updates to Swift 6. 34 | 35 | 36 | 37 | ## 1.1.1 38 | 39 | This version adds support for strict concurrency. 40 | 41 | 42 | 43 | ## 1.1 44 | 45 | This version adds predefined system notification messages and styles and makes it easier to present a message. 46 | 47 | ### ✨ New features 48 | 49 | * `SystemNotificationContext` has a new `presentMessage` function. 50 | * `SystemNotificationMessage` has new, predefined `error`, `success`, `warning` and `silentMode` messages. 51 | * `SystemNotificationMessageStyle` has new, predefined `prominent`, `error`, `success` and `warning` styles. 52 | 53 | 54 | 55 | ## 1.0 56 | 57 | This version bumps the deployment targets and moves styling and configuration to view modifiers. 58 | 59 | ### 🚨 Important Information 60 | 61 | * All previous style- and config-based initializers have been removed. 62 | 63 | ### 📱 New Deployment Targets 64 | 65 | * .iOS(.v15) 66 | * .macOS(.v12) 67 | * .tvOS(.v15) 68 | * .watchOS(.v8) 69 | * .visionOS(.v1) 70 | 71 | ### ✨ New features 72 | 73 | * `SystemNotification` is more self-managed than before. 74 | * `SystemNotificationConfiguration` can now be used as an environment value. 75 | * `SystemNotificationStyle` now supports background materials. 76 | * `SystemNotificationStyle` can now be used as an environment value. 77 | * `SystemNotificationMessageStyle` can now be used as an environment value. 78 | * `SystemNotificationMessageStyle` now supports specifying a foreground color. 79 | * `SystemNotificationMessageStyle` now supports specifying a background color. 80 | * `View` has new system notification-related style- and config view modifiers. 81 | 82 | ### 🐛 Bug fixes 83 | 84 | * `SystemNotification` now correctly applies the configuration animation. 85 | 86 | 87 | 88 | ## 0.8 89 | 90 | ### ✨ New features 91 | 92 | * SystemNotification now supports visionOS. 93 | 94 | ### 💥 Breaking changes 95 | 96 | * SystemNotification now requires Swift 5.9. 97 | 98 | 99 | 100 | ## 0.7.3 101 | 102 | ### ✨ New features 103 | 104 | * The `SystemNotificationPresenter` feature was a bad addition and has been deprecated. 105 | 106 | 107 | 108 | ## 0.7.2 109 | 110 | ### ✨ New features 111 | 112 | * `SystemNotificationPresenter` is a new convenience protocol. 113 | 114 | 115 | 116 | ## 0.7.1 117 | 118 | This version rolls back the UIKit support deprecation. 119 | 120 | ### 🗑 Deprecations 121 | 122 | * `SystemNotificationUIKitOverlay` is no longer deprecated. 123 | 124 | 125 | 126 | ## 0.7 127 | 128 | This version splits up `SystemNotificationConfiguration` in a configuration and style type. 129 | 130 | Due to changes in the `SystemNotificationMessage` capabilities, the `LocalizedStringKey` support has been deprecated. 131 | 132 | Also, since SystemNotification aims to be a pure SwiftUI project, the `SystemNotificationUIKitOverlay` has been deprecated. Please let me know if you really need it, and I'll re-add it to the library. 133 | 134 | ### ✨ New features 135 | 136 | * `SystemNotificationMessage` now supports a custom icon view. 137 | * `SystemNotificationStyle` is a new type that's extracted from `SystemNotificationConfiguration`. 138 | 139 | ### 💡 Behavior changes 140 | 141 | * `SystemNotification` no longer uses async size bindings to apply the corner radius. 142 | 143 | ### 🗑 Deprecated 144 | 145 | * `SystemNotificationConfiguration` moves all styles to `SystemNotificationStyle`. 146 | * `SystemNotificationMessage` has deprecated its `LocalizedString` initializer. 147 | * `SystemNotificationMessageConfiguration` is renamed to `SystemNotificationMessageStyle`. 148 | 149 | 150 | 151 | ## 0.6 152 | 153 | ### ✨ New features 154 | 155 | * `SystemNotificationConfiguration` has a new `padding` parameter. 156 | * `SystemNotificationConfiguration` has a new `standardBackgroundColor` function. 157 | * `SystemNotificationConfiguration` has a new `standardPadding` property. 158 | 159 | ### 💡 Behavior changes 160 | 161 | * `SystemNotificationContext` handles custom presentation configurations better. 162 | 163 | ### 💥 Breaking changes 164 | 165 | * All deprecated code has been removed. 166 | 167 | 168 | 169 | ## 0.5.3 170 | 171 | ### 💡 Behavior changes 172 | 173 | * `SystemNotificationContext` `present` now has an optional configuration. 174 | * `SystemNotificationContext` now uses its own configuration if none is provided. 175 | 176 | 177 | 178 | ## 0.5.2 179 | 180 | This release fixes compile errors on tvOS and watchOS. 181 | 182 | 183 | 184 | ## 0.5.1 185 | 186 | This release makes configuration properties mutable. 187 | 188 | 189 | 190 | ## 0.5 191 | 192 | This release greatly improves how notifications are presented and dismissed and simplifies usage. 193 | 194 | The demo app now uses a local package, which makes it a lot easier to develop the library. 195 | 196 | ### 📖 Documentation 197 | 198 | SystemNotification has a brand new DocC documentation. 199 | 200 | Due to the new documentation, the package now requires Swift 5.5. 201 | 202 | ### ✨ New features 203 | 204 | * `SystemNotificationContext` has a new completion-based dismiss function. 205 | * `SystemNotificationMessageConfiguration` has new `iconTextSpacing` and `titleTextSpacing` properties. 206 | * `SystemNotificationUIKitOverlay` is a new view that simplifies adding a system notification to a UIKit view. 207 | * `View+SystemNotification` has a new parameter-based extension that replaces the old notification-based one. 208 | 209 | ### 💡 Behavior changes 210 | 211 | * `SystemNotificationMessageConfiguration` is adjusted to make a message look more like an iPhone system notification. 212 | * Presenting a new notification first dismisses the current notification, if any. 213 | * The auto-dismiss logic is moved from the system notification to the notification context. 214 | 215 | ### 🐛 Bug fixes 216 | 217 | * This version fixes a bug, where the message configuration padding was incorrectly applied. 218 | 219 | ### 🗑 Deprecated 220 | 221 | * The notification-based `systemNotification(:)` function is deprecated. 222 | 223 | ### 💥 Breaking changes 224 | 225 | * `SystemNotification+Message` has been deprecated. 226 | * `SystemNotificationConfiguration` `minWidth` is no longer used and has been removed. 227 | * `View+SystemNotification` has deprecated the `SystemNotification`-based extension. 228 | 229 | 230 | 231 | ## 0.4.3 232 | 233 | ### ✨ New features 234 | 235 | * `SystemNotificationConfiguration` has a new `isSwipeToDismissEnabled` parameter. 236 | * `SystemNotification` can now be swiped to be dismissed, if `isSwipeToDismissEnabled` is `true`. 237 | 238 | 239 | 240 | ## 0.4.2 241 | 242 | This relase makes it possible to provide a `nil` title to `SystemNotificationMessage`. 243 | 244 | 245 | 246 | ## 0.4.1 247 | 248 | This relase makes it possible to use plain `String` values when creating `SystemNotification` and `SystemNotificationMessage`. 249 | 250 | 251 | 252 | ## 0.4 253 | 254 | ### ✨ New features 255 | 256 | * The context-based view modifier no longer requires a `context` parameter name. 257 | 258 | ### 🗑 Deprecated 259 | 260 | * `systemNotification(context:)` is replaced with `systemNotification(_ context:)`. 261 | 262 | ### 🐛 Bug fixes 263 | 264 | * This version fixes a bug, where the configuration duration wasn't applied. 265 | * This version fixes a bug, where the default dark mode background was transparent. 266 | 267 | 268 | 269 | ## 0.3.2 270 | 271 | ### 🐛 Bug fixes 272 | 273 | * This version fixes a preview bug that caused the library not to build for macOS. 274 | 275 | 276 | 277 | ## 0.3.1 278 | 279 | ### ✨ New features 280 | 281 | * Thanks to Christian Mitteldorf, system notifications now use localized string keys, which makes it super simple to create localized notifications. 282 | 283 | 284 | 285 | ## 0.3.0 286 | 287 | This release has some breaking name changes and makes it easier to present multiple notifications with a single modifier. 288 | 289 | ### ✨ New features 290 | 291 | * `SystemNotificationContext` makes it easy to present multiple notifications with a single modifier. 292 | 293 | ### 💥 Breaking changes 294 | 295 | * `SystemNotification.Configuration` has been renamed to `SystemNotificationConfiguration` 296 | * `SystemNotificationMessage.Configuration` has been renamed to `SystemNotificationMessageConfiguration` 297 | 298 | 299 | 300 | ## 0.2.0 301 | 302 | This release improves platform supports, adds convenience utils and tweaks design. 303 | 304 | ### ✨ New features 305 | 306 | * The library now supports macOS, tvOS and watchOS as well. 307 | * `SystemNotification.Configuration` has new shadow properties. 308 | 309 | ### 💡 Behavior changes 310 | 311 | * The configuration types are no longed nested, to avoid generic limitations. 312 | 313 | ### 🎨 Design changes 314 | 315 | * `SystemNotification.Configuration` has removed the background opacity modifier. 316 | * `SystemNotification.Configuration` has now applies a more subtle standard shadow. 317 | * `SystemNotificationMessage.Configuration` now uses `title3` as standard icon font. 318 | 319 | ### 🐛 Bug fixes 320 | 321 | * The corner radius now works even when no image is provided. 322 | 323 | 324 | 325 | ## 0.1.0 326 | 327 | This is the first public release of SystemNotification. 328 | 329 | Check out the readme and the demo app for information about how to use it. 330 | -------------------------------------------------------------------------------- /Resources/Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Resources/Demo.gif -------------------------------------------------------------------------------- /Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Resources/Icon.png -------------------------------------------------------------------------------- /Sources/SystemNotification/Overlays/SystemNotificationAppKitOverlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemNotification.swift 3 | // SystemNotification 4 | // 5 | // Created by Daniel Saidi on 2022-01-20. 6 | // Copyright © 2022-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import SwiftUI 11 | 12 | /// This view can be used to create an overlay that can then 13 | /// be added to any AppKit view, using `addAsOverlay(to:)`. 14 | public struct SystemNotificationAppKitOverlay: View { 15 | 16 | public init(context: SystemNotificationContext) { 17 | self._context = ObservedObject(wrappedValue: context) 18 | } 19 | 20 | @ObservedObject 21 | var context: SystemNotificationContext 22 | 23 | public var body: some View { 24 | Color.clear 25 | .disabled(true) 26 | .edgesIgnoringSafeArea(.all) 27 | .systemNotification(context) 28 | } 29 | } 30 | 31 | public extension SystemNotificationAppKitOverlay { 32 | 33 | /// Add the overlay view to a certain AppKit view. 34 | func addAsOverlay(to view: NSView) { 35 | let overlay = NSHostingController(rootView: self) 36 | overlay.view.wantsLayer = true 37 | overlay.view.layer!.backgroundColor = .clear 38 | view.addSubview(overlay.view) 39 | 40 | overlay.view.translatesAutoresizingMaskIntoConstraints = false 41 | overlay.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true 42 | overlay.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true 43 | overlay.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 44 | overlay.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true 45 | } 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/SystemNotification/Overlays/SystemNotificationUIKitOverlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemNotification.swift 3 | // SystemNotification 4 | // 5 | // Created by Daniel Saidi on 2022-01-20. 6 | // Copyright © 2022-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | #if os(iOS) 10 | import SwiftUI 11 | 12 | /// This view can be used to create an overlay that can then 13 | /// be added to any UIKit view, using `addAsOverlay(to:)`. 14 | public struct SystemNotificationUIKitOverlay: View { 15 | 16 | public init(context: SystemNotificationContext) { 17 | self._context = ObservedObject(wrappedValue: context) 18 | } 19 | 20 | @ObservedObject 21 | var context: SystemNotificationContext 22 | 23 | public var body: some View { 24 | Color.clear 25 | .disabled(true) 26 | .edgesIgnoringSafeArea(.all) 27 | .systemNotification(context) 28 | } 29 | } 30 | 31 | public extension SystemNotificationUIKitOverlay { 32 | 33 | /// Add the overlay view to a certain UIKit view. 34 | func addAsOverlay(to view: UIView) { 35 | let overlay = UIHostingController(rootView: self) 36 | view.addSubview(overlay.view) 37 | 38 | // Prevent the UIHostingController from grabbing all touch events going to the UIKit view 39 | overlay.view.isUserInteractionEnabled = false 40 | overlay.view.backgroundColor = .clear 41 | overlay.view.translatesAutoresizingMaskIntoConstraints = false 42 | overlay.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true 43 | overlay.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true 44 | overlay.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 45 | overlay.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true 46 | } 47 | } 48 | #endif 49 | -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotification.docc/Articles/Demo-Article.md: -------------------------------------------------------------------------------- 1 | # Demo App 2 | 3 | This article describes the SystemNotification demo app. 4 | 5 | @Metadata { 6 | 7 | @CallToAction(purpose: link, url: https://github.com/danielsaidi/SystemNotification) 8 | 9 | @PageKind(sampleCode) 10 | 11 | @PageImage( 12 | purpose: card, 13 | source: "Page", 14 | alt: "Page icon" 15 | ) 16 | } 17 | 18 | SystemNotification has a multi-platform demo app that show how to use the library on multiple platforms. 19 | 20 | The demo app can be explored from the [GitHub repository][GitHub]. It's a SwiftUI app that runs on macOS, iOS & iPadOS. 21 | 22 | 23 | 24 | ## How to run the demo on device 25 | 26 | The demo app has disabled code signing, to simplify its setup. 27 | 28 | You can run the demo app on any iOS and iPadOS simulator, as well as on macOS. To run it on a physical iOS or iPadOS device, you must set up code signing just like you would with any other app. 29 | 30 | 31 | 32 | [GitHub]: https://github.com/danielsaidi/SystemNotification 33 | -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotification.docc/Articles/Getting Started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This article describes how to get started with SystemNotification. 4 | 5 | @Metadata { 6 | 7 | @PageImage( 8 | purpose: card, 9 | source: "Page", 10 | alt: "Page icon" 11 | ) 12 | 13 | @PageColor(blue) 14 | } 15 | 16 | 17 | 18 | ## Overview 19 | 20 | After adding SystemNotification to your project, you can add a system notification to any view just as you add a `sheet`, `alert` and `fullScreenModal`, with a simple view modifier. 21 | 22 | To add a system notification to a view, just use the ``SwiftUI/View/systemNotification(_:)`` view modifier with a state binding or a ``SystemNotificationContext`` instance. 23 | 24 | > Important: Since system notifications should be as global as possible, make sure to apply the view modifier to the application root view, e.g. the main `NavigationStack` or ``TabView``. Any new sheets or modals must also have the modifier applied. 25 | 26 | State-based notifications take a boolean state binding and a view builder: 27 | 28 | ```swift 29 | import SystemNotification 30 | 31 | struct MyView: View { 32 | 33 | @State 34 | var isActive = false 35 | 36 | var body: some View { 37 | VStack { 38 | Button("Show notification") { 39 | isActive = true 40 | } 41 | } 42 | .systemNotification(isActive: $isActive) { 43 | Text("You can use any custom content view") 44 | .padding() 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | Context-based notifications take a ``SystemNotificationContext`` and can then show different notifications with a single modifier: 51 | 52 | ```swift 53 | import SystemNotification 54 | 55 | struct MyView: View { 56 | 57 | @StateObject 58 | var notification = SystemNotificationContext() 59 | 60 | var body: some View { 61 | VStack { 62 | Button("Show text") { 63 | notification.present { 64 | Text("Context-based notifications are more flexible.") 65 | .padding() 66 | .multilineTextAlignment(.center) 67 | } 68 | } 69 | Button("Show message") { 70 | notification.present { 71 | SystemNotificationMessage( 72 | icon: Text("👍"), 73 | title: "Great job!", 74 | text: "You presented a native-looking message!" 75 | ) 76 | } 77 | } 78 | } 79 | .systemNotification(notification) 80 | } 81 | } 82 | ``` 83 | 84 | The ``SystemNotificationMessage`` view lets you easily mimic a native notification view, with an icon, an optional title and a text, but you can use any custom view as the notification content view. 85 | 86 | You can use the ``SwiftUI/View/systemNotificationConfiguration(_:)`` and ``SwiftUI/View/systemNotificationStyle(_:)`` view modifiers to apply custom configurations and styles. 87 | 88 | 89 | 90 | ## How to create custom notification messages 91 | 92 | The ``SystemNotificationMessage`` view lets you easily mimic a native notification message, with an icon, an optional title and a text, as well as an explicit style that overrides any environment style. 93 | 94 | You can easily extend ``SystemNotificationMessage`` with your own custom messages, which can then be easily presented with the context's ``SystemNotificationContext/presentMessage(_:afterDelay:)`` function: 95 | 96 | ```swift 97 | extension SystemNotificationMessage where IconView == Image { 98 | 99 | static var itemCreated: Self { 100 | .init( 101 | icon: Image(systemName: "checkmark"), 102 | title: "Item created!", 103 | text: "A new item was created", 104 | style: .... 105 | ) 106 | } 107 | } 108 | ``` 109 | 110 | You can also use the ``SwiftUI/View/systemNotificationMessageStyle(_:)`` view modifier to provide a standard style for all other messages. 111 | -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotification.docc/Resources/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Sources/SystemNotification/SystemNotification.docc/Resources/Logo.png -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotification.docc/Resources/Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielsaidi/SystemNotification/6da5f6ca3193ad32e4b06407f1f42b633e3881a6/Sources/SystemNotification/SystemNotification.docc/Resources/Page.png -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotification.docc/SystemNotification.md: -------------------------------------------------------------------------------- 1 | # ``SystemNotification`` 2 | 3 | SystemNotification is a SwiftUI library that lets you mimic the native iOS system notification. 4 | 5 | 6 | 7 | ## Overview 8 | 9 | ![SystemNotification logo](Logo.png) 10 | 11 | SystemNotification is a SwiftUI library that lets you mimic the native iOS system notification that is presented when you toggle silent mode, connect your AirPods, etc. 12 | 13 | System notifications can be styled and customized. You can use a native-looking ``SystemNotificationMessage`` view as the content view, or any custom view. 14 | 15 | 16 | 17 | ## Installation 18 | 19 | SystemNotification can be installed with the Swift Package Manager: 20 | 21 | ``` 22 | https://github.com/danielsaidi/SystemNotification.git 23 | ``` 24 | 25 | 26 | ## Support My Work 27 | 28 | You can [become a sponsor][Sponsors] to help me dedicate more time on my various [open-source tools][OpenSource]. Every contribution, no matter the size, makes a real difference in keeping these tools free and actively developed. 29 | 30 | 31 | 32 | ## Getting started 33 | 34 | @Links(visualStyle: detailedGrid) { 35 | 36 | - 37 | - 38 | } 39 | 40 | 41 | 42 | ## Repository 43 | 44 | For more information, source code, etc., visit the [project repository](https://github.com/danielsaidi/SystemNotification). 45 | 46 | 47 | 48 | ## License 49 | 50 | SystemNotification is available under the MIT license. 51 | 52 | 53 | 54 | ## Topics 55 | 56 | ### Articles 57 | 58 | - 59 | - 60 | - 61 | 62 | ### Essentials 63 | 64 | - ``SystemNotification/SystemNotification`` 65 | - ``SystemNotificationConfiguration`` 66 | - ``SystemNotificationContext`` 67 | - ``SystemNotificationEdge`` 68 | - ``SystemNotificationMessage`` 69 | - ``SystemNotificationMessageStyle`` 70 | - ``SystemNotificationStyle`` 71 | - ``SystemNotificationUIKitOverlay`` 72 | 73 | 74 | 75 | [Email]: mailto:daniel.saidi@gmail.com 76 | [Website]: https://danielsaidi.com 77 | [GitHub]: https://github.com/danielsaidi 78 | [OpenSource]: https://danielsaidi.com/opensource 79 | [Sponsors]: https://github.com/sponsors/danielsaidi 80 | -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemNotification.swift 3 | // SystemNotification 4 | // 5 | // Created by Daniel Saidi on 2021-06-01. 6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This view mimics the native iOS system notification that 12 | /// for instance is shown when toggling silent mode. 13 | /// 14 | /// This view renders a notification shape that contains the 15 | /// provided content view. You can use a custom view, or use 16 | /// a ``SystemNotificationMessage`` for convenience. 17 | /// 18 | /// You can use a ``SwiftUI/View/systemNotificationStyle(_:)`` 19 | /// and a ``SwiftUI/View/systemNotificationConfiguration(_:)`` 20 | /// to style and configure a system notification. 21 | public struct SystemNotification: View { 22 | 23 | /// Create a system notification view. 24 | /// 25 | /// - Parameters: 26 | /// - isActive: A binding that controls the active state of the notification. 27 | /// - content: The view to present within the notification badge. 28 | public init( 29 | isActive: Binding, 30 | @ViewBuilder content: @escaping ContentBuilder 31 | ) { 32 | _isActive = isActive 33 | self.initStyle = nil 34 | self.initConfig = nil 35 | self.content = content 36 | } 37 | 38 | public typealias ContentBuilder = (_ isActive: Bool) -> Content 39 | 40 | private let initConfig: SystemNotificationConfiguration? 41 | private let initStyle: SystemNotificationStyle? 42 | private let content: ContentBuilder 43 | 44 | @Binding 45 | private var isActive: Bool 46 | 47 | @Environment(\.colorScheme) 48 | private var colorScheme 49 | 50 | @Environment(\.systemNotificationConfiguration) 51 | private var envConfig 52 | 53 | @Environment(\.systemNotificationStyle) 54 | private var envStyle 55 | 56 | @State 57 | private var currentId = UUID() 58 | 59 | public var body: some View { 60 | ZStack(alignment: edge.alignment) { 61 | Color.clear 62 | content(isActive) 63 | .background(style.backgroundColor) 64 | .background(style.backgroundMaterial) 65 | .compositingGroup() 66 | .cornerRadius(style.cornerRadius ?? 1_000) 67 | .shadow( 68 | color: style.shadowColor, 69 | radius: style.shadowRadius, 70 | y: style.shadowOffset) 71 | .animation(config.animation, value: isActive) 72 | .offset(x: 0, y: verticalOffset) 73 | #if os(iOS) || os(macOS) || os(watchOS) || os(visionOS) 74 | .gesture(swipeGesture, if: config.isSwipeToDismissEnabled) 75 | #endif 76 | .padding(style.padding) 77 | .onChange(of: isActive, perform: handlePresentation) 78 | } 79 | } 80 | } 81 | 82 | private extension SystemNotification { 83 | 84 | var config: SystemNotificationConfiguration { 85 | initConfig ?? envConfig 86 | } 87 | 88 | var edge: SystemNotificationEdge { 89 | config.edge 90 | } 91 | 92 | var verticalOffset: CGFloat { 93 | if isActive { return 0 } 94 | switch edge { 95 | case .top: return -250 96 | case .bottom: return 250 97 | } 98 | } 99 | 100 | func dismiss() { 101 | isActive = false 102 | } 103 | 104 | var style: SystemNotificationStyle { 105 | initStyle ?? envStyle 106 | } 107 | } 108 | 109 | @MainActor 110 | private extension SystemNotification { 111 | 112 | func handlePresentation(_ isPresented: Bool) { 113 | guard isPresented else { return } 114 | currentId = UUID() 115 | let id = currentId 116 | DispatchQueue.main.asyncAfter(deadline: .now() + config.duration) { 117 | guard id == currentId else { return } 118 | isActive = false 119 | } 120 | } 121 | } 122 | 123 | 124 | // MARK: - Private View Logic 125 | 126 | private extension View { 127 | 128 | @ViewBuilder 129 | func gesture( 130 | _ gesture: GestureType, 131 | if condition: Bool 132 | ) -> some View { 133 | if condition { 134 | self.gesture(gesture) 135 | } else { 136 | self 137 | } 138 | } 139 | } 140 | 141 | private extension SystemNotification { 142 | 143 | @ViewBuilder 144 | var background: some View { 145 | if let color = style.backgroundColor { 146 | color 147 | } else { 148 | SystemNotificationStyle.standardBackgroundColor(for: colorScheme) 149 | } 150 | } 151 | 152 | #if os(iOS) || os(macOS) || os(watchOS) || os(visionOS) 153 | var swipeGesture: some Gesture { 154 | DragGesture(minimumDistance: 20, coordinateSpace: .global) 155 | .onEnded { value in 156 | let horizontalTranslation = value.translation.width as CGFloat 157 | let verticalTranslation = value.translation.height as CGFloat 158 | let isVertical = abs(verticalTranslation) > abs(horizontalTranslation) 159 | let isUp = verticalTranslation < 0 160 | // let isLeft = horizontalTranslation < 0 161 | guard isVertical else { return } // We only use vertical edges 162 | if isUp && edge == .top { dismiss() } 163 | if !isUp && edge == .bottom { dismiss() } 164 | } 165 | } 166 | #endif 167 | } 168 | 169 | #Preview { 170 | 171 | struct Preview: View { 172 | 173 | @State var isPresented = false 174 | 175 | var body: some View { 176 | ZStack { 177 | AsyncImage(url: .init(string: "https://picsum.photos/500/500")) { 178 | $0.image? 179 | .resizable() 180 | .aspectRatio(contentMode: .fill) 181 | } 182 | .clipped() 183 | .ignoresSafeArea() 184 | 185 | SystemNotification( 186 | isActive: $isPresented 187 | ) { _ in 188 | SystemNotificationMessage( 189 | icon: Image(systemName: "bell.fill"), 190 | title: "Silent mode", 191 | text: "Silent mode is off" 192 | ) 193 | } 194 | .systemNotificationStyle(.standard) 195 | .systemNotificationConfiguration( 196 | .init(animation: .bouncy) 197 | ) 198 | 199 | SystemNotification( 200 | isActive: $isPresented 201 | ) { _ in 202 | Text("HELLO") 203 | .padding() 204 | } 205 | .systemNotificationStyle( 206 | .init(backgroundColor: .blue) 207 | ) 208 | .systemNotificationConfiguration( 209 | .init(animation: .smooth, edge: .bottom) 210 | ) 211 | } 212 | #if os(iOS) 213 | .onTapGesture { 214 | isPresented.toggle() 215 | } 216 | #endif 217 | } 218 | } 219 | 220 | return Preview() 221 | } 222 | 223 | #Preview("README #1") { 224 | 225 | struct MyView: View { 226 | 227 | @State 228 | var isActive = false 229 | 230 | var body: some View { 231 | VStack { 232 | Button("Show notification") { 233 | isActive = true 234 | } 235 | } 236 | .systemNotification(isActive: $isActive) { 237 | Text("You can use any custom content view") 238 | .padding() 239 | } 240 | } 241 | } 242 | 243 | return MyView() 244 | } 245 | 246 | 247 | #Preview("README #2") { 248 | 249 | struct MyView: View { 250 | 251 | @StateObject 252 | var notification = SystemNotificationContext() 253 | 254 | var body: some View { 255 | VStack { 256 | Button("Show text") { 257 | notification.present { 258 | Text("Context-based notifications are more flexible.") 259 | .padding() 260 | .multilineTextAlignment(.center) 261 | } 262 | } 263 | Button("Show message") { 264 | notification.present { 265 | SystemNotificationMessage( 266 | icon: Text("👍"), 267 | title: "Great job!", 268 | text: "You presented a native-looking message!" 269 | ) 270 | } 271 | } 272 | } 273 | .systemNotification(notification) 274 | .systemNotificationConfiguration(.init(animation: .bouncy)) 275 | } 276 | } 277 | 278 | return MyView() 279 | } 280 | 281 | 282 | #Preview("README #3") { 283 | 284 | struct MyView: View { 285 | 286 | @State 287 | var isSilentModeEnabled = false 288 | 289 | @StateObject 290 | var notification = SystemNotificationContext() 291 | 292 | var body: some View { 293 | List { 294 | Toggle("Silent Mode", isOn: $isSilentModeEnabled) 295 | } 296 | .systemNotification(notification) 297 | .onChange(of: isSilentModeEnabled) { value in 298 | notification.presentMessage(.silentMode(isOn: value)) 299 | } 300 | } 301 | } 302 | 303 | return MyView() 304 | } 305 | -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotificationConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemNotificationConfiguration.swift 3 | // SystemNotification 4 | // 5 | // Created by Daniel Saidi on 2021-06-01. 6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This type can configure a ``SystemNotification``. 12 | /// 13 | /// See for more information on how to 14 | /// style and configure system notifications. 15 | /// 16 | /// You can apply a custom value with the corresponding view 17 | /// modifier. The ``standard`` value is used by default when 18 | /// you don't apply a custom value. 19 | /// 20 | /// You can use the ``standardToast`` configuration when you 21 | /// want to present the notification as a bottom toast. 22 | public struct SystemNotificationConfiguration { 23 | 24 | /// Create a custom system notification configuration. 25 | /// 26 | /// - Parameters: 27 | /// - animation: The animation to apply when sliding in the notification, by default `.bouncy`. 28 | /// - duration: The number of seconds the notification should be presented, by default `3`. 29 | /// - edge: The edge from which to present the notification, by default `.top`. 30 | /// - isSwipeToDismissEnabled: Whether or not a user can swipe to dismiss a notification, by default `true`. 31 | public init( 32 | animation: Animation = .bouncy, 33 | duration: TimeInterval = 3, 34 | edge: SystemNotificationEdge = .top, 35 | isSwipeToDismissEnabled: Bool = true 36 | ) { 37 | self.animation = animation 38 | self.duration = duration 39 | self.edge = edge 40 | self.isSwipeToDismissEnabled = isSwipeToDismissEnabled 41 | } 42 | 43 | /// The animation to use when presenting a notification. 44 | public var animation: Animation 45 | 46 | /// The number of seconds a notification should be shown. 47 | public var duration: TimeInterval 48 | 49 | /// The edge to present from. 50 | public var edge: SystemNotificationEdge = .top 51 | 52 | /// Whether or not swiping can to dismiss a notification. 53 | public var isSwipeToDismissEnabled: Bool 54 | } 55 | 56 | public extension SystemNotificationConfiguration { 57 | 58 | /// The standard system notification configuration. 59 | static var standard: Self { .init() } 60 | 61 | /// A standard toast configuration. 62 | static var standardToast: Self { 63 | .init( 64 | animation: .bouncy, 65 | edge: .bottom 66 | ) 67 | } 68 | } 69 | 70 | public extension View { 71 | 72 | /// Apply a ``SystemNotificationConfiguration`` to the view. 73 | func systemNotificationConfiguration( 74 | _ configuration: SystemNotificationConfiguration 75 | ) -> some View { 76 | self.environment(\.systemNotificationConfiguration, configuration) 77 | } 78 | } 79 | 80 | extension View { 81 | 82 | @ViewBuilder 83 | func systemNotificationConfiguration( 84 | _ configuration: SystemNotificationConfiguration? 85 | ) -> some View { 86 | if let configuration { 87 | self.environment(\.systemNotificationConfiguration, configuration) 88 | } else { 89 | self 90 | } 91 | } 92 | } 93 | 94 | private extension SystemNotificationConfiguration { 95 | 96 | struct Key: EnvironmentKey { 97 | 98 | static var defaultValue: SystemNotificationConfiguration { .init() } 99 | } 100 | } 101 | 102 | public extension EnvironmentValues { 103 | 104 | var systemNotificationConfiguration: SystemNotificationConfiguration { 105 | get { self [SystemNotificationConfiguration.Key.self] } 106 | set { self [SystemNotificationConfiguration.Key.self] = newValue } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotificationContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemNotificationContext.swift 3 | // SystemNotification 4 | // 5 | // Created by Daniel Saidi on 2021-06-02. 6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This context can be used to present system notifications 12 | /// in a more flexible way. 13 | public class SystemNotificationContext: ObservableObject { 14 | 15 | public init() {} 16 | 17 | public typealias Action = () -> Void 18 | 19 | @Published 20 | public var content = AnyView(EmptyView()) 21 | 22 | @Published 23 | public var isActive = false 24 | } 25 | 26 | @MainActor 27 | public extension SystemNotificationContext { 28 | 29 | var isActiveBinding: Binding { 30 | .init(get: { self.isActive }, 31 | set: { self.isActive = $0 } 32 | ) 33 | } 34 | 35 | /// Dismiss the current notification, if any. 36 | func dismiss() { 37 | dismiss {} 38 | } 39 | 40 | /// Dismiss the current notification, if any. 41 | func dismiss( 42 | completion: @escaping Action 43 | ) { 44 | guard isActive else { return completion() } 45 | isActive = false 46 | perform(after: 0.3, action: completion) 47 | } 48 | 49 | /// Present a system notification. 50 | func present( 51 | _ content: Content, 52 | afterDelay delay: TimeInterval = 0 53 | ) { 54 | dismiss { 55 | self.perform(after: delay) { 56 | self.presentAfterDismiss(content) 57 | } 58 | } 59 | } 60 | 61 | /// Present a system notification. 62 | func present( 63 | afterDelay delay: TimeInterval = 0, 64 | @ViewBuilder content: @escaping () -> Content 65 | ) { 66 | present(content(), afterDelay: delay) 67 | } 68 | 69 | /// Present a system notification message. 70 | func presentMessage( 71 | _ message: SystemNotificationMessage, 72 | afterDelay delay: TimeInterval = 0 73 | ) { 74 | present(message, afterDelay: delay) 75 | } 76 | } 77 | 78 | @MainActor 79 | private extension SystemNotificationContext { 80 | 81 | func perform( 82 | _ action: @escaping Action, 83 | after seconds: TimeInterval 84 | ) { 85 | guard seconds > 0 else { return action() } 86 | DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { 87 | action() 88 | } 89 | } 90 | 91 | func perform(after seconds: TimeInterval, action: @escaping Action) { 92 | perform(action, after: seconds) 93 | } 94 | 95 | func presentAfterDismiss(_ content: Content) { 96 | self.content = AnyView(content) 97 | perform(setActive, after: 0.1) 98 | } 99 | 100 | func setActive() { 101 | isActive = true 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotificationEdge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemNotificationEdge.swift 3 | // SystemNotification 4 | // 5 | // Created by Daniel Saidi on 2021-06-01. 6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This enum defines edges from which a system notification 12 | /// can be presented. 13 | public enum SystemNotificationEdge { 14 | 15 | case top, bottom 16 | 17 | public var alignment: Alignment { 18 | switch self { 19 | case .top: .top 20 | case .bottom: .bottom 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotificationMessage+Predefined.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemNotificationMessage+Predefined.swift 3 | // SystemNotification 4 | // 5 | // Created by Daniel Saidi on 2024-04-24. 6 | // Copyright © 2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension SystemNotificationMessageStyle { 12 | 13 | static var error: Self { 14 | prominent(backgroundColor: .red) 15 | } 16 | 17 | static var success: Self { 18 | prominent(backgroundColor: .green) 19 | } 20 | 21 | static var warning: Self { 22 | prominent(backgroundColor: .orange) 23 | } 24 | 25 | static func prominent( 26 | backgroundColor: Color 27 | ) -> Self { 28 | .init( 29 | backgroundColor: backgroundColor, 30 | iconColor: .white, 31 | textColor: .white.opacity(0.8), 32 | titleColor: .white 33 | ) 34 | } 35 | } 36 | 37 | public extension SystemNotificationMessage where IconView == Image { 38 | 39 | static func error( 40 | icon: Image = .init(systemName: "exclamationmark.triangle"), 41 | title: LocalizedStringKey? = nil, 42 | text: LocalizedStringKey, 43 | bundle: Bundle? = nil 44 | ) -> Self { 45 | .init( 46 | icon: icon, 47 | title: title, 48 | text: text, 49 | style: .error, 50 | bundle: bundle 51 | ) 52 | } 53 | 54 | static func success( 55 | icon: Image = .init(systemName: "checkmark"), 56 | title: LocalizedStringKey? = nil, 57 | text: LocalizedStringKey, 58 | bundle: Bundle? = nil 59 | ) -> Self { 60 | .init( 61 | icon: icon, 62 | title: title, 63 | text: text, 64 | style: .success, 65 | bundle: bundle 66 | ) 67 | } 68 | 69 | static func warning( 70 | icon: Image = .init(systemName: "exclamationmark.triangle"), 71 | title: LocalizedStringKey? = nil, 72 | text: LocalizedStringKey, 73 | bundle: Bundle? = nil 74 | ) -> Self { 75 | .init( 76 | icon: icon, 77 | title: title, 78 | text: text, 79 | style: .warning, 80 | bundle: bundle 81 | ) 82 | } 83 | } 84 | 85 | public extension SystemNotificationMessage where IconView == AnyView { 86 | 87 | /// This message mimics a native iOS silent mode message. 88 | static func silentMode( 89 | isOn: Bool, 90 | title: LocalizedStringKey? = nil 91 | ) -> Self { 92 | .init( 93 | icon: AnyView(SilentModeBell(isSilentModeOn: isOn)), 94 | text: title ?? "Silent Mode \(isOn ? "On" : "Off")" 95 | ) 96 | } 97 | } 98 | 99 | private struct SilentModeBell: View { 100 | 101 | var isSilentModeOn = false 102 | 103 | @State 104 | private var isRotated: Bool = false 105 | 106 | @State 107 | private var isAnimated: Bool = false 108 | 109 | var body: some View { 110 | Image(systemName: iconName) 111 | .rotationEffect( 112 | .degrees(isRotated ? -45 : 0), 113 | anchor: .top 114 | ) 115 | .animation( 116 | .interpolatingSpring( 117 | mass: 0.5, 118 | stiffness: animationStiffness, 119 | damping: animationDamping, 120 | initialVelocity: 0 121 | ), 122 | value: isAnimated) 123 | .foregroundColor(iconColor) 124 | .onAppear(perform: animate) 125 | } 126 | } 127 | 128 | @MainActor 129 | private extension SilentModeBell { 130 | 131 | func animate() { 132 | withAnimation { isRotated = true } 133 | perform(after: 0.1) { 134 | isRotated = false 135 | isAnimated = true 136 | } 137 | } 138 | 139 | func perform(after: Double, action: @escaping () -> Void) { 140 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { 141 | action() 142 | } 143 | } 144 | 145 | var animationDamping: Double { 146 | isSilentModeOn ? 4 : 1.5 147 | } 148 | 149 | var animationStiffness: Double { 150 | isSilentModeOn ? 129 : 179 151 | } 152 | 153 | var iconName: String { 154 | isSilentModeOn ? "bell.slash.fill" : "bell.fill" 155 | } 156 | 157 | var iconColor: Color { 158 | isSilentModeOn ? .red : .gray 159 | } 160 | } 161 | 162 | #Preview { 163 | 164 | VStack { 165 | SystemNotificationMessage.silentMode(isOn: true) 166 | SystemNotificationMessage.silentMode(isOn: false) 167 | SystemNotificationMessage.error(title: "Error!", text: "Something failed!") 168 | SystemNotificationMessage.success(title: "Success!", text: "You did it!") 169 | SystemNotificationMessage.warning(title: "Warning!", text: "Danger ahead!") 170 | } 171 | .padding() 172 | .background(Color.black.opacity(0.1)) 173 | .clipShape(.rect(cornerRadius: 10)) 174 | } 175 | -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotificationMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemNotificationMessage.swift 3 | // SystemNotification 4 | // 5 | // Created by Daniel Saidi on 2021-06-01. 6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This view mimics the message view that is shown within a 12 | /// native iOS system notification. 13 | /// 14 | /// You can provide a custom icon view, title, and text, and 15 | /// e.g. animate the icon when it's presented. 16 | /// 17 | /// You can easily create custom messages, by extending this 18 | /// type with static message builders, for instance: 19 | /// 20 | /// ```swift 21 | /// extension SystemNotificationMessage where IconView == Image { 22 | /// 23 | /// static func silentMode(on: Bool) -> Self { 24 | /// ... 25 | /// } 26 | /// } 27 | /// ``` 28 | public struct SystemNotificationMessage: View { 29 | 30 | /// Create a system notification message view. 31 | /// 32 | /// - Parameters: 33 | /// - icon: The leading icon view. 34 | /// - title: The bold title text, by default `nil`. 35 | /// - text: The plain message text. 36 | /// - style: An optional, explicit style to apply.. 37 | /// - bundle: The bundle of the localized texts, by default `.main`. 38 | public init( 39 | icon: IconView, 40 | title: LocalizedStringKey? = nil, 41 | text: LocalizedStringKey, 42 | style: SystemNotificationMessageStyle? = nil, 43 | bundle: Bundle? = nil 44 | ) { 45 | self.icon = icon 46 | self.title = title 47 | self.text = text 48 | self.initStyle = style 49 | self.bundle = bundle 50 | } 51 | 52 | /// Create a system notification message view. 53 | /// 54 | /// - Parameters: 55 | /// - icon: The leading icon image. 56 | /// - title: The bold title text, by default `nil`. 57 | /// - text: The plain message text. 58 | /// - style: An optional, explicit style to apply. 59 | /// - bundle: The bundle of the localized texts, by default `.main`. 60 | public init( 61 | icon: Image, 62 | title: LocalizedStringKey? = nil, 63 | text: LocalizedStringKey, 64 | style: SystemNotificationMessageStyle? = nil, 65 | bundle: Bundle? = nil 66 | ) where IconView == Image { 67 | self.icon = icon 68 | self.title = title 69 | self.text = text 70 | self.initStyle = style 71 | self.bundle = bundle 72 | } 73 | 74 | /// Create a system notification message view. 75 | /// 76 | /// - Parameters: 77 | /// - title: The bold title text, by default `nil`. 78 | /// - text: The plain message text. 79 | /// - style: An optional, explicit style to apply. 80 | /// - bundle: The bundle of the localized texts, by default `.main`. 81 | public init( 82 | title: LocalizedStringKey? = nil, 83 | text: LocalizedStringKey, 84 | style: SystemNotificationMessageStyle? = nil, 85 | bundle: Bundle? = nil 86 | ) where IconView == EmptyView { 87 | self.icon = EmptyView() 88 | self.title = title 89 | self.text = text 90 | self.initStyle = style 91 | self.bundle = bundle 92 | } 93 | 94 | let icon: IconView 95 | let title: LocalizedStringKey? 96 | let text: LocalizedStringKey 97 | let initStyle: SystemNotificationMessageStyle? 98 | let bundle: Bundle? 99 | 100 | @Environment(\.systemNotificationMessageStyle) 101 | private var environmentStyle 102 | 103 | public var body: some View { 104 | HStack(spacing: style.iconTextSpacing) { 105 | iconView 106 | .id(UUID()) 107 | textContent 108 | iconView.opacity(0.001) 109 | } 110 | .padding(.vertical, style.padding.height) 111 | .padding(.horizontal, style.padding.width) 112 | .background(style.backgroundColor) 113 | } 114 | } 115 | 116 | private extension SystemNotificationMessage { 117 | 118 | var style: SystemNotificationMessageStyle { 119 | initStyle ?? environmentStyle 120 | } 121 | 122 | func foregroundColor( 123 | for color: Color 124 | ) -> Color { 125 | style.foregroundColor ?? color 126 | } 127 | } 128 | 129 | private extension SystemNotificationMessage { 130 | 131 | var textContent: some View { 132 | VStack(spacing: style.titleTextSpacing) { 133 | if let title = title { 134 | Text(title, bundle: bundle ?? nil) 135 | .font(style.titleFont) 136 | .foregroundStyle(foregroundColor(for: style.titleColor)) 137 | } 138 | Text(text, bundle: bundle ?? nil) 139 | .font(style.textFont) 140 | .foregroundStyle(foregroundColor(for: style.textColor)) 141 | } 142 | .multilineTextAlignment(.center) 143 | } 144 | 145 | @ViewBuilder 146 | var iconView: some View { 147 | icon.font(style.iconFont) 148 | .foregroundStyle(foregroundColor(for: style.iconColor)) 149 | } 150 | } 151 | 152 | #Preview { 153 | 154 | VStack { 155 | Group { 156 | SystemNotificationMessage( 157 | icon: Image(systemName: "bell.slash.fill"), 158 | title: "Silent mode", 159 | text: "On" 160 | ) 161 | .systemNotificationMessageStyle( 162 | .init( 163 | backgroundColor: .yellow, 164 | iconColor: .red 165 | ) 166 | ) 167 | 168 | SystemNotificationMessage( 169 | icon: Color.red.frame(width: 20, height: 20), 170 | text: "Custom icon view, no title" 171 | ) 172 | .systemNotificationMessageStyle( 173 | .init(iconColor: .red) 174 | ) 175 | 176 | SystemNotificationMessage( 177 | title: "No icon", 178 | text: "On" 179 | ) 180 | .systemNotificationMessageStyle( 181 | .init(iconColor: .red) 182 | ) 183 | 184 | SystemNotificationMessage( 185 | icon: Image(systemName: "exclamationmark.triangle"), 186 | title: "Warning", 187 | text: "This is a long warning message to demonstrate multiline messages." 188 | ) 189 | .systemNotificationMessageStyle(.warning) 190 | } 191 | .background(Color.white) 192 | .cornerRadius(5) 193 | .padding() 194 | } 195 | .background(Color.gray) 196 | } 197 | -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotificationMessageStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemNotificationMessageStyle.swift 3 | // SystemNotification 4 | // 5 | // Created by Daniel Saidi on 2021-06-01. 6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This style can style a ``SystemNotificationMessage``. 12 | /// 13 | /// You can either set an overall foreground color, which is 14 | /// then applied to all components, or use individual colors. 15 | /// 16 | /// See for more information on how to 17 | /// style and configure system notifications. 18 | /// 19 | /// You can apply a custom value with the corresponding view 20 | /// modifier. The ``standard`` value is used by default when 21 | /// you don't apply a custom value. 22 | public struct SystemNotificationMessageStyle { 23 | 24 | /// Create a custom system notification message style. 25 | /// 26 | /// - Parameters: 27 | /// - backgroundColor: The overall background color. 28 | /// - foregroundColor: The overall foreground color. 29 | /// - iconColor: The color to apply to the icon. 30 | /// - iconFont: The font to apply to the icon. 31 | /// - iconTextSpacing: The spacing to apply between the icon and the text. 32 | /// - padding: The padding to add to the content. 33 | /// - textColor: The color to apply to the text. 34 | /// - textFont: The font to apply to the text. 35 | /// - titleColor: The color to apply to the title. 36 | /// - titleFont: The font to apply to the title. 37 | /// - titleTextSpacing: The spacing to apply between the title and the text. 38 | public init( 39 | backgroundColor: Color? = nil, 40 | foregroundColor: Color? = nil, 41 | iconColor: Color = .primary.opacity(0.6), 42 | iconFont: Font = Font.title3, 43 | iconTextSpacing: CGFloat = 20, 44 | padding: CGSize = .init(width: 15, height: 7), 45 | textColor: Color = .secondary, 46 | textFont: Font = Font.footnote.bold(), 47 | titleColor: Color = .primary, 48 | titleFont: Font = Font.footnote.bold(), 49 | titleTextSpacing: CGFloat = 2 50 | ) { 51 | self.backgroundColor = backgroundColor 52 | self.foregroundColor = foregroundColor 53 | self.iconColor = iconColor 54 | self.iconFont = iconFont 55 | self.iconTextSpacing = iconTextSpacing 56 | self.padding = padding 57 | self.textColor = textColor 58 | self.textFont = textFont 59 | self.titleColor = titleColor 60 | self.titleFont = titleFont 61 | self.titleTextSpacing = titleTextSpacing 62 | } 63 | 64 | /// The overall background color. 65 | public var backgroundColor: Color? 66 | 67 | /// The overall foreground color. 68 | public var foregroundColor: Color? 69 | 70 | /// The color to apply to the icon. 71 | public var iconColor: Color 72 | 73 | /// The font to apply to the icon. 74 | public var iconFont: Font 75 | 76 | /// The spacing to apply between the icon and the text. 77 | public var iconTextSpacing: CGFloat 78 | 79 | /// The padding to add to the content. 80 | public var padding: CGSize 81 | 82 | /// The color to apply to the text. 83 | public var textColor: Color 84 | 85 | /// The font to apply to the text. 86 | public var textFont: Font 87 | 88 | /// The color to apply to the title. 89 | public var titleColor: Color 90 | 91 | /// The font to apply to the title. 92 | public var titleFont: Font 93 | 94 | /// The spacing to apply between the title and the text. 95 | public var titleTextSpacing: CGFloat 96 | } 97 | 98 | public extension SystemNotificationMessageStyle { 99 | 100 | /// The standard system notification message style. 101 | static var standard: Self { .init() } 102 | } 103 | 104 | public extension View { 105 | 106 | /// Apply a ``SystemNotificationMessageStyle`` to the view. 107 | func systemNotificationMessageStyle( 108 | _ style: SystemNotificationMessageStyle 109 | ) -> some View { 110 | self.environment(\.systemNotificationMessageStyle, style) 111 | } 112 | } 113 | 114 | private extension SystemNotificationMessageStyle { 115 | 116 | struct Key: EnvironmentKey { 117 | 118 | static var defaultValue: SystemNotificationMessageStyle { .standard } 119 | } 120 | } 121 | 122 | public extension EnvironmentValues { 123 | 124 | var systemNotificationMessageStyle: SystemNotificationMessageStyle { 125 | get { self [SystemNotificationMessageStyle.Key.self] } 126 | set { self [SystemNotificationMessageStyle.Key.self] = newValue } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Sources/SystemNotification/SystemNotificationStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemNotificationStyle.swift 3 | // SystemNotification 4 | // 5 | // Created by Daniel Saidi on 2021-06-01. 6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// This style can style a ``SystemNotification``. 12 | /// 13 | /// See for more information on how to 14 | /// style and configure system notifications. 15 | /// 16 | /// You can apply a custom value with the corresponding view 17 | /// modifier. The ``standard`` value is used by default when 18 | /// you don't apply a custom value. 19 | public struct SystemNotificationStyle { 20 | 21 | /// Create a custom system notification style. 22 | /// 23 | /// - Parameters: 24 | /// - backgroundColor: The background color to apply, by default `nil`. 25 | /// - backgroundMaterial: The background material to apply, by default `.thin`. 26 | /// - cornerRadius: The corner radius to apply, by default `nil`. 27 | /// - padding: The edge padding to apply, by default `nil`. 28 | /// - edge: The edge from which to present the notification, by default `.top`. 29 | /// - shadowColor: The shadow color to apply, by default `.black.opacity(0.1)`. 30 | /// - shadowOffset: The shadow offset to apply, by default `5`. 31 | /// - shadowRadius: The shadow radius to apply, by default `7.5`. 32 | public init( 33 | backgroundColor: Color? = nil, 34 | backgroundMaterial: Material = .thin, 35 | cornerRadius: CGFloat? = nil, 36 | padding: EdgeInsets? = nil, 37 | shadowColor: Color = .black.opacity(0.1), 38 | shadowOffset: CGFloat = 5, 39 | shadowRadius: CGFloat = 7.5 40 | ) { 41 | self.backgroundColor = backgroundColor 42 | self.backgroundMaterial = backgroundMaterial 43 | self.cornerRadius = cornerRadius 44 | self.padding = padding ?? Self.standardPadding 45 | self.shadowColor = shadowColor 46 | self.shadowOffset = shadowOffset 47 | self.shadowRadius = shadowRadius 48 | } 49 | 50 | /// The standard background color. 51 | @ViewBuilder 52 | public static func standardBackgroundColor(for colorScheme: ColorScheme) -> some View { 53 | if colorScheme == .light { 54 | Color.primary.colorInvert() 55 | } else { 56 | #if os(iOS) 57 | Color(UIColor.secondarySystemBackground) 58 | #elseif os(macOS) 59 | Color.primary.colorInvert() 60 | #elseif os(macOS) 61 | Color.secondary 62 | .colorInvert() 63 | .background(Color.white) 64 | #endif 65 | } 66 | } 67 | 68 | /// The standard content padding. 69 | public static var standardPadding: EdgeInsets { 70 | #if os(iOS) 71 | EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) 72 | #else 73 | EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) 74 | #endif 75 | } 76 | 77 | /// The background color to apply. 78 | public var backgroundColor: Color? 79 | 80 | /// The background material to apply. 81 | public var backgroundMaterial: Material 82 | 83 | /// The corner radius to apply. 84 | public var cornerRadius: CGFloat? 85 | 86 | /// The edge padding to apply. 87 | public var padding: EdgeInsets 88 | 89 | /// The shadow color to apply. 90 | public var shadowColor: Color 91 | 92 | /// The shadow offset to apply. 93 | public var shadowOffset: CGFloat 94 | 95 | /// The shadow radius to apply. 96 | public var shadowRadius: CGFloat 97 | } 98 | 99 | public extension SystemNotificationStyle { 100 | 101 | /// The standard system notification style. 102 | static var standard: Self { .init() } 103 | } 104 | 105 | public extension View { 106 | 107 | /// Apply a ``SystemNotificationStyle`` to the view. 108 | func systemNotificationStyle( 109 | _ style: SystemNotificationStyle 110 | ) -> some View { 111 | self.environment(\.systemNotificationStyle, style) 112 | } 113 | } 114 | 115 | extension View { 116 | 117 | @ViewBuilder 118 | func systemNotificationStyle( 119 | _ style: SystemNotificationStyle? 120 | ) -> some View { 121 | if let style { 122 | self.environment(\.systemNotificationStyle, style) 123 | } else { 124 | self 125 | } 126 | } 127 | } 128 | 129 | private extension SystemNotificationStyle { 130 | 131 | struct Key: EnvironmentKey { 132 | 133 | static var defaultValue: SystemNotificationStyle { .standard } 134 | } 135 | } 136 | 137 | public extension EnvironmentValues { 138 | 139 | var systemNotificationStyle: SystemNotificationStyle { 140 | get { self [SystemNotificationStyle.Key.self] } 141 | set { self [SystemNotificationStyle.Key.self] = newValue } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/SystemNotification/View+SystemNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+SystemNotification.swift 3 | // SystemNotification 4 | // 5 | // Created by Daniel Saidi on 2021-06-01. 6 | // Copyright © 2021-2024 Daniel Saidi. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension View { 12 | 13 | /// Attach a ``SystemNotification`` to the view. 14 | /// 15 | /// State-based system notifications make it easy to use 16 | /// a single binding to control a specific notification. 17 | /// 18 | /// After applying the modifier, you can use the binding 19 | /// to present the provided `content`. 20 | func systemNotification( 21 | isActive: Binding, 22 | content: @escaping () -> Content 23 | ) -> some View { 24 | ZStack { 25 | self 26 | SystemNotification( 27 | isActive: isActive, 28 | content: { _ in content() } 29 | ) 30 | } 31 | } 32 | 33 | /// Attach a system notification context to the view. 34 | /// 35 | /// Context-based system notifications make it very easy 36 | /// to show multiple notifications with a single context. 37 | /// 38 | /// After applying the modifier, you can use the context 39 | /// to present notifications. 40 | /// 41 | /// This modifier will also pass in the context into the 42 | /// environment, as an environment object. 43 | func systemNotification( 44 | _ context: SystemNotificationContext 45 | ) -> some View { 46 | self.systemNotification( 47 | isActive: context.isActiveBinding, 48 | content: { context.content } 49 | ) 50 | .environmentObject(context) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/SystemNotificationTests/SystemNotificationTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SystemNotification 3 | 4 | final class SystemNotificationTests: XCTestCase { 5 | 6 | func testExample() {} 7 | } 8 | -------------------------------------------------------------------------------- /package_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script creates a new project version for the current project. 5 | # You can customize this to fit your project when you copy these scripts. 6 | # You can pass in a custom branch if you don't want to use the default one. 7 | 8 | SCRIPT="scripts/package_version.sh" 9 | chmod +x $SCRIPT 10 | bash $SCRIPT 11 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script builds a for all provided . 5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default. 6 | # You can pass in a list of if you want to customize the build. 7 | 8 | # Usage: 9 | # build.sh [ default:iOS macOS tvOS watchOS xrOS] 10 | # e.g. `bash scripts/build.sh MyTarget iOS macOS` 11 | 12 | # Exit immediately if a command exits with a non-zero status 13 | set -e 14 | 15 | # Verify that all required arguments are provided 16 | if [ $# -eq 0 ]; then 17 | echo "Error: This script requires at least one argument" 18 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]" 19 | echo "For instance: $0 MyTarget iOS macOS" 20 | exit 1 21 | fi 22 | 23 | # Define argument variables 24 | TARGET=$1 25 | 26 | # Remove TARGET from arguments list 27 | shift 28 | 29 | # Define platforms variable 30 | if [ $# -eq 0 ]; then 31 | set -- iOS macOS tvOS watchOS xrOS 32 | fi 33 | PLATFORMS=$@ 34 | 35 | # A function that builds $TARGET for a specific platform 36 | build_platform() { 37 | 38 | # Define a local $PLATFORM variable 39 | local PLATFORM=$1 40 | 41 | # Build $TARGET for the $PLATFORM 42 | echo "Building $TARGET for $PLATFORM..." 43 | if ! xcodebuild -scheme $TARGET -derivedDataPath .build -destination generic/platform=$PLATFORM; then 44 | echo "Failed to build $TARGET for $PLATFORM" 45 | return 1 46 | fi 47 | 48 | # Complete successfully 49 | echo "Successfully built $TARGET for $PLATFORM" 50 | } 51 | 52 | # Start script 53 | echo "" 54 | echo "Building $TARGET for [$PLATFORMS]..." 55 | echo "" 56 | 57 | # Loop through all platforms and call the build function 58 | for PLATFORM in $PLATFORMS; do 59 | if ! build_platform "$PLATFORM"; then 60 | exit 1 61 | fi 62 | done 63 | 64 | # Complete successfully 65 | echo "" 66 | echo "Building $TARGET completed successfully!" 67 | echo "" 68 | -------------------------------------------------------------------------------- /scripts/chmod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script makes all scripts in this folder executable. 5 | 6 | # Usage: 7 | # scripts_chmod.sh 8 | # e.g. `bash scripts/chmod.sh` 9 | 10 | # Exit immediately if a command exits with a non-zero status 11 | set -e 12 | 13 | # Use the script folder to refer to other scripts. 14 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 15 | 16 | # Find all .sh files in the FOLDER except chmod.sh 17 | find "$FOLDER" -name "*.sh" ! -name "chmod.sh" -type f | while read -r script; do 18 | chmod +x "$script" 19 | done 20 | -------------------------------------------------------------------------------- /scripts/docc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script builds DocC for a and certain . 5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default. 6 | # You can pass in a list of if you want to customize the build. 7 | # The documentation ends up in to .build/docs-. 8 | 9 | # Usage: 10 | # docc.sh [ default:iOS macOS tvOS watchOS xrOS] 11 | # e.g. `bash scripts/docc.sh MyTarget iOS macOS` 12 | 13 | # Exit immediately if a command exits with a non-zero status 14 | set -e 15 | 16 | # Fail if any command in a pipeline fails 17 | set -o pipefail 18 | 19 | # Verify that all required arguments are provided 20 | if [ $# -eq 0 ]; then 21 | echo "Error: This script requires at least one argument" 22 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]" 23 | echo "For instance: $0 MyTarget iOS macOS" 24 | exit 1 25 | fi 26 | 27 | # Define argument variables 28 | TARGET=$1 29 | TARGET_LOWERCASED=$(echo "$1" | tr '[:upper:]' '[:lower:]') 30 | 31 | # Remove TARGET from arguments list 32 | shift 33 | 34 | # Define platforms variable 35 | if [ $# -eq 0 ]; then 36 | set -- iOS macOS tvOS watchOS xrOS 37 | fi 38 | PLATFORMS=$@ 39 | 40 | # Prepare the package for DocC 41 | swift package resolve; 42 | 43 | # A function that builds $TARGET for a specific platform 44 | build_platform() { 45 | 46 | # Define a local $PLATFORM variable and set an exit code 47 | local PLATFORM=$1 48 | local EXIT_CODE=0 49 | 50 | # Define the build folder name, based on the $PLATFORM 51 | case $PLATFORM in 52 | "iOS") 53 | DEBUG_PATH="Debug-iphoneos" 54 | ;; 55 | "macOS") 56 | DEBUG_PATH="Debug" 57 | ;; 58 | "tvOS") 59 | DEBUG_PATH="Debug-appletvos" 60 | ;; 61 | "watchOS") 62 | DEBUG_PATH="Debug-watchos" 63 | ;; 64 | "xrOS") 65 | DEBUG_PATH="Debug-xros" 66 | ;; 67 | *) 68 | echo "Error: Unsupported platform '$PLATFORM'" 69 | exit 1 70 | ;; 71 | esac 72 | 73 | # Build $TARGET docs for the $PLATFORM 74 | echo "Building $TARGET docs for $PLATFORM..." 75 | if ! xcodebuild docbuild -scheme $TARGET -derivedDataPath .build/docbuild -destination "generic/platform=$PLATFORM"; then 76 | echo "Error: Failed to build documentation for $PLATFORM" >&2 77 | return 1 78 | fi 79 | 80 | # Transform docs for static hosting 81 | if ! $(xcrun --find docc) process-archive \ 82 | transform-for-static-hosting .build/docbuild/Build/Products/$DEBUG_PATH/$TARGET.doccarchive \ 83 | --output-path .build/docs-$PLATFORM \ 84 | --hosting-base-path "$TARGET"; then 85 | echo "Error: Failed to transform documentation for $PLATFORM" >&2 86 | return 1 87 | fi 88 | 89 | # Inject a root redirect script on the root page 90 | echo "" > .build/docs-$PLATFORM/index.html; 91 | 92 | # Complete successfully 93 | echo "Successfully built $TARGET docs for $PLATFORM" 94 | return 0 95 | } 96 | 97 | # Start script 98 | echo "" 99 | echo "Building $TARGET docs for [$PLATFORMS]..." 100 | echo "" 101 | 102 | # Loop through all platforms and call the build function 103 | for PLATFORM in $PLATFORMS; do 104 | if ! build_platform "$PLATFORM"; then 105 | exit 1 106 | fi 107 | done 108 | 109 | # Complete successfully 110 | echo "" 111 | echo "Building $TARGET docs completed successfully!" 112 | echo "" 113 | -------------------------------------------------------------------------------- /scripts/framework.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script builds DocC for a and certain . 5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default. 6 | # You can pass in a list of if you want to customize the build. 7 | 8 | # Important: 9 | # This script doesn't work on packages, only on .xcproj projects that generate a framework. 10 | 11 | # Usage: 12 | # framework.sh [ default:iOS macOS tvOS watchOS xrOS] 13 | # e.g. `bash scripts/framework.sh MyTarget iOS macOS` 14 | 15 | # Exit immediately if a command exits with a non-zero status 16 | set -e 17 | 18 | # Verify that all required arguments are provided 19 | if [ $# -eq 0 ]; then 20 | echo "Error: This script requires exactly one argument" 21 | echo "Usage: $0 " 22 | exit 1 23 | fi 24 | 25 | # Define argument variables 26 | TARGET=$1 27 | 28 | # Remove TARGET from arguments list 29 | shift 30 | 31 | # Define platforms variable 32 | if [ $# -eq 0 ]; then 33 | set -- iOS macOS tvOS watchOS xrOS 34 | fi 35 | PLATFORMS=$@ 36 | 37 | # Define local variables 38 | BUILD_FOLDER=.build 39 | BUILD_FOLDER_ARCHIVES=.build/framework_archives 40 | BUILD_FILE=$BUILD_FOLDER/$TARGET.xcframework 41 | BUILD_ZIP=$BUILD_FOLDER/$TARGET.zip 42 | 43 | # Start script 44 | echo "" 45 | echo "Building $TARGET XCFramework for [$PLATFORMS]..." 46 | echo "" 47 | 48 | # Delete old builds 49 | echo "Cleaning old builds..." 50 | rm -rf $BUILD_ZIP 51 | rm -rf $BUILD_FILE 52 | rm -rf $BUILD_FOLDER_ARCHIVES 53 | 54 | 55 | # Generate XCArchive files for all platforms 56 | echo "Generating XCArchives..." 57 | 58 | # Initialize the xcframework command 59 | XCFRAMEWORK_CMD="xcodebuild -create-xcframework" 60 | 61 | # Build iOS archives and append to the xcframework command 62 | if [[ " ${PLATFORMS[@]} " =~ " iOS " ]]; then 63 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=iOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-iOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES 64 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=iOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-iOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES 65 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-iOS.xcarchive/Products/Library/Frameworks/$TARGET.framework" 66 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-iOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework" 67 | fi 68 | 69 | # Build iOS archive and append to the xcframework command 70 | if [[ " ${PLATFORMS[@]} " =~ " macOS " ]]; then 71 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=macOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-macOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES 72 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-macOS.xcarchive/Products/Library/Frameworks/$TARGET.framework" 73 | fi 74 | 75 | # Build tvOS archives and append to the xcframework command 76 | if [[ " ${PLATFORMS[@]} " =~ " tvOS " ]]; then 77 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=tvOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES 78 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=tvOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES 79 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS.xcarchive/Products/Library/Frameworks/$TARGET.framework" 80 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-tvOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework" 81 | fi 82 | 83 | # Build watchOS archives and append to the xcframework command 84 | if [[ " ${PLATFORMS[@]} " =~ " watchOS " ]]; then 85 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=watchOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES 86 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=watchOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES 87 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS.xcarchive/Products/Library/Frameworks/$TARGET.framework" 88 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-watchOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework" 89 | fi 90 | 91 | # Build xrOS archives and append to the xcframework command 92 | if [[ " ${PLATFORMS[@]} " =~ " xrOS " ]]; then 93 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=xrOS" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES 94 | xcodebuild archive -scheme $TARGET -configuration Release -destination "generic/platform=xrOS Simulator" -archivePath $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS-Sim SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES 95 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS.xcarchive/Products/Library/Frameworks/$TARGET.framework" 96 | XCFRAMEWORK_CMD+=" -framework $BUILD_FOLDER_ARCHIVES/$TARGET-xrOS-Sim.xcarchive/Products/Library/Frameworks/$TARGET.framework" 97 | fi 98 | 99 | # Genererate XCFramework 100 | echo "Generating XCFramework..." 101 | XCFRAMEWORK_CMD+=" -output $BUILD_FILE" 102 | eval "$XCFRAMEWORK_CMD" 103 | 104 | # Genererate iOS XCFramework zip 105 | echo "Generating XCFramework zip..." 106 | zip -r $BUILD_ZIP $BUILD_FILE 107 | echo "" 108 | echo "***** CHECKSUM *****" 109 | swift package compute-checksum $BUILD_ZIP 110 | echo "********************" 111 | echo "" 112 | 113 | # Complete successfully 114 | echo "" 115 | echo "$TARGET XCFramework created successfully!" 116 | echo "" 117 | -------------------------------------------------------------------------------- /scripts/git_default_branch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script echos the default git branch name. 5 | 6 | # Usage: 7 | # git_default_branch.sh 8 | # e.g. `bash scripts/git_default_branch.sh` 9 | 10 | BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@') 11 | echo $BRANCH 12 | -------------------------------------------------------------------------------- /scripts/package_docc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script builds DocC documentation for `Package.swift`. 5 | # This script targets iOS by default, but you can pass in custom . 6 | 7 | # Usage: 8 | # package_docc.sh [ default:iOS] 9 | # e.g. `bash scripts/package_docc.sh iOS macOS` 10 | 11 | # Exit immediately if a command exits with non-zero status 12 | set -e 13 | 14 | # Use the script folder to refer to other scripts. 15 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 16 | SCRIPT_PACKAGE_NAME="$FOLDER/package_name.sh" 17 | SCRIPT_DOCC="$FOLDER/docc.sh" 18 | 19 | # Define platforms variable 20 | if [ $# -eq 0 ]; then 21 | set -- iOS 22 | fi 23 | PLATFORMS=$@ 24 | 25 | # Get package name 26 | PACKAGE_NAME=$("$SCRIPT_PACKAGE_NAME") || { echo "Failed to get package name"; exit 1; } 27 | 28 | # Build package documentation 29 | bash $SCRIPT_DOCC $PACKAGE_NAME $PLATFORMS || { echo "DocC script failed"; exit 1; } 30 | -------------------------------------------------------------------------------- /scripts/package_framework.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script generates an XCFramework for `Package.swift`. 5 | # This script targets iOS by default, but you can pass in custom . 6 | 7 | # Usage: 8 | # package_framework.sh [ default:iOS] 9 | # e.g. `bash scripts/package_framework.sh iOS macOS` 10 | 11 | # Exit immediately if a command exits with non-zero status 12 | set -e 13 | 14 | # Use the script folder to refer to other scripts. 15 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 16 | SCRIPT_PACKAGE_NAME="$FOLDER/package_name.sh" 17 | SCRIPT_FRAMEWORK="$FOLDER/framework.sh" 18 | 19 | # Define platforms variable 20 | if [ $# -eq 0 ]; then 21 | set -- iOS 22 | fi 23 | PLATFORMS=$@ 24 | 25 | # Get package name 26 | PACKAGE_NAME=$("$SCRIPT_PACKAGE_NAME") || { echo "Failed to get package name"; exit 1; } 27 | 28 | # Build package framework 29 | bash $SCRIPT_FRAMEWORK $PACKAGE_NAME $PLATFORMS 30 | -------------------------------------------------------------------------------- /scripts/package_name.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script finds the main target name in `Package.swift`. 5 | 6 | # Usage: 7 | # package_name.sh 8 | # e.g. `bash scripts/package_name.sh` 9 | 10 | # Exit immediately if a command exits with non-zero status 11 | set -e 12 | 13 | # Check that a Package.swift file exists 14 | if [ ! -f "Package.swift" ]; then 15 | echo "Error: Package.swift not found in current directory" 16 | exit 1 17 | fi 18 | 19 | # Using grep and sed to extract the package name 20 | # 1. grep finds the line containing "name:" 21 | # 2. sed extracts the text between quotes 22 | package_name=$(grep -m 1 'name:.*"' Package.swift | sed -n 's/.*name:[[:space:]]*"\([^"]*\)".*/\1/p') 23 | 24 | if [ -z "$package_name" ]; then 25 | echo "Error: Could not find package name in Package.swift" 26 | exit 1 27 | else 28 | echo "$package_name" 29 | fi 30 | -------------------------------------------------------------------------------- /scripts/package_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script creates a new version for `Package.swift`. 5 | # You can pass in a to validate any non-main branch. 6 | 7 | # Usage: 8 | # package_version.sh 9 | # e.g. `bash scripts/package_version.sh master` 10 | 11 | # Exit immediately if a command exits with non-zero status 12 | set -e 13 | 14 | # Use the script folder to refer to other scripts. 15 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 16 | SCRIPT_BRANCH_NAME="$FOLDER/git_default_branch.sh" 17 | SCRIPT_PACKAGE_NAME="$FOLDER/package_name.sh" 18 | SCRIPT_VERSION="$FOLDER/version.sh" 19 | 20 | # Get branch name 21 | DEFAULT_BRANCH=$("$SCRIPT_BRANCH_NAME") || { echo "Failed to get branch name"; exit 1; } 22 | BRANCH_NAME=${1:-$DEFAULT_BRANCH} 23 | 24 | # Get package name 25 | PACKAGE_NAME=$("$SCRIPT_PACKAGE_NAME") || { echo "Failed to get package name"; exit 1; } 26 | 27 | # Build package version 28 | bash $SCRIPT_VERSION $PACKAGE_NAME $BRANCH_NAME 29 | -------------------------------------------------------------------------------- /scripts/sync_from.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script syncs Swift Package Scripts from a . 5 | # This script will overwrite the existing "scripts" folder. 6 | # Only pass in the full path to a Swift Package Scripts root. 7 | 8 | # Usage: 9 | # package_name.sh 10 | # e.g. `bash sync_from.sh ../SwiftPackageScripts` 11 | 12 | # Define argument variables 13 | SOURCE=$1 14 | 15 | # Define variables 16 | FOLDER="scripts/" 17 | SOURCE_FOLDER="$SOURCE/$FOLDER" 18 | 19 | # Start script 20 | echo "" 21 | echo "Syncing scripts from $SOURCE_FOLDER..." 22 | echo "" 23 | 24 | # Remove existing folder 25 | rm -rf $FOLDER 26 | 27 | # Copy folder 28 | cp -r "$SOURCE_FOLDER/" "$FOLDER/" 29 | 30 | # Complete successfully 31 | echo "" 32 | echo "Script syncing from $SOURCE_FOLDER completed successfully!" 33 | echo "" 34 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script tests a for all provided . 5 | 6 | # Usage: 7 | # test.sh [ default:iOS macOS tvOS watchOS xrOS] 8 | # e.g. `bash scripts/test.sh MyTarget iOS macOS` 9 | 10 | # Exit immediately if a command exits with a non-zero status 11 | set -e 12 | 13 | # Verify that all required arguments are provided 14 | if [ $# -eq 0 ]; then 15 | echo "Error: This script requires at least one argument" 16 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]" 17 | echo "For instance: $0 MyTarget iOS macOS" 18 | exit 1 19 | fi 20 | 21 | # Define argument variables 22 | TARGET=$1 23 | 24 | # Remove TARGET from arguments list 25 | shift 26 | 27 | # Define platforms variable 28 | if [ $# -eq 0 ]; then 29 | set -- iOS macOS tvOS watchOS xrOS 30 | fi 31 | PLATFORMS=$@ 32 | 33 | # Start script 34 | echo "" 35 | echo "Testing $TARGET for [$PLATFORMS]..." 36 | echo "" 37 | 38 | # A function that gets the latest simulator for a certain OS. 39 | get_latest_simulator() { 40 | local PLATFORM=$1 41 | local SIMULATOR_TYPE 42 | 43 | case $PLATFORM in 44 | "iOS") 45 | SIMULATOR_TYPE="iPhone" 46 | ;; 47 | "tvOS") 48 | SIMULATOR_TYPE="Apple TV" 49 | ;; 50 | "watchOS") 51 | SIMULATOR_TYPE="Apple Watch" 52 | ;; 53 | "xrOS") 54 | SIMULATOR_TYPE="Apple Vision" 55 | ;; 56 | *) 57 | echo "Error: Unsupported platform for simulator '$PLATFORM'" 58 | return 1 59 | ;; 60 | esac 61 | 62 | # Get the latest simulator for the platform 63 | xcrun simctl list devices available | grep "$SIMULATOR_TYPE" | tail -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/' 64 | } 65 | 66 | # A function that tests $TARGET for a specific platform 67 | test_platform() { 68 | 69 | # Define a local $PLATFORM variable 70 | local PLATFORM="${1//_/ }" 71 | 72 | # Define the destination, based on the $PLATFORM 73 | case $PLATFORM in 74 | "iOS"|"tvOS"|"watchOS"|"xrOS") 75 | local SIMULATOR_UDID=$(get_latest_simulator "$PLATFORM") 76 | if [ -z "$SIMULATOR_UDID" ]; then 77 | echo "Error: No simulator found for $PLATFORM" 78 | return 1 79 | fi 80 | DESTINATION="id=$SIMULATOR_UDID" 81 | ;; 82 | "macOS") 83 | DESTINATION="platform=macOS" 84 | ;; 85 | *) 86 | echo "Error: Unsupported platform '$PLATFORM'" 87 | return 1 88 | ;; 89 | esac 90 | 91 | # Test $TARGET for the $DESTINATION 92 | echo "Testing $TARGET for $PLATFORM..." 93 | xcodebuild test -scheme $TARGET -derivedDataPath .build -destination "$DESTINATION" -enableCodeCoverage YES 94 | local TEST_RESULT=$? 95 | 96 | if [[ $TEST_RESULT -ne 0 ]]; then 97 | return $TEST_RESULT 98 | fi 99 | 100 | # Complete successfully 101 | echo "Successfully tested $TARGET for $PLATFORM" 102 | return 0 103 | } 104 | 105 | # Loop through all platforms and call the test function 106 | for PLATFORM in $PLATFORMS; do 107 | if ! test_platform "$PLATFORM"; then 108 | exit 1 109 | fi 110 | done 111 | 112 | # Complete successfully 113 | echo "" 114 | echo "Testing $TARGET completed successfully!" 115 | echo "" 116 | -------------------------------------------------------------------------------- /scripts/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script creates a new version for the provided and . 5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default. 6 | # You can pass in a list of if you want to customize the build. 7 | 8 | # Usage: 9 | # version.sh [ default:iOS macOS tvOS watchOS xrOS]" 10 | # e.g. `scripts/version.sh MyTarget master iOS macOS` 11 | 12 | # This script will: 13 | # * Call version_validate_git.sh to validate the git repo. 14 | # * Call version_validate_target to run tests, swiftlint, etc. 15 | # * Call version_bump.sh if all validation steps above passed. 16 | 17 | # Exit immediately if a command exits with a non-zero status 18 | set -e 19 | 20 | # Verify that all required arguments are provided 21 | if [ $# -lt 2 ]; then 22 | echo "Error: This script requires at least two arguments" 23 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]" 24 | echo "For instance: $0 MyTarget master iOS macOS" 25 | exit 1 26 | fi 27 | 28 | # Define argument variables 29 | TARGET=$1 30 | BRANCH=${2:-main} 31 | 32 | # Remove TARGET and BRANCH from arguments list 33 | shift 34 | shift 35 | 36 | # Read platform arguments or use default value 37 | if [ $# -eq 0 ]; then 38 | set -- iOS macOS tvOS watchOS xrOS 39 | fi 40 | 41 | # Use the script folder to refer to other scripts. 42 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 43 | SCRIPT_VALIDATE_GIT="$FOLDER/version_validate_git.sh" 44 | SCRIPT_VALIDATE_TARGET="$FOLDER/version_validate_target.sh" 45 | SCRIPT_VERSION_BUMP="$FOLDER/version_bump.sh" 46 | 47 | # A function that run a certain script and checks for errors 48 | run_script() { 49 | local script="$1" 50 | shift # Remove the first argument (the script path) 51 | 52 | if [ ! -f "$script" ]; then 53 | echo "Error: Script not found: $script" 54 | exit 1 55 | fi 56 | 57 | chmod +x "$script" 58 | if ! "$script" "$@"; then 59 | echo "Error: Script $script failed" 60 | exit 1 61 | fi 62 | } 63 | 64 | # Start script 65 | echo "" 66 | echo "Creating a new version for $TARGET on the $BRANCH branch..." 67 | echo "" 68 | 69 | # Validate git and project 70 | echo "Validating..." 71 | run_script "$SCRIPT_VALIDATE_GIT" "$BRANCH" 72 | run_script "$SCRIPT_VALIDATE_TARGET" "$TARGET" 73 | 74 | # Bump version 75 | echo "Bumping version..." 76 | run_script "$SCRIPT_VERSION_BUMP" 77 | 78 | # Complete successfully 79 | echo "" 80 | echo "Version created successfully!" 81 | echo "" 82 | -------------------------------------------------------------------------------- /scripts/version_bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script bumps the project version number. 5 | # You can append --no-semver to disable semantic version validation. 6 | 7 | # Usage: 8 | # version_bump.sh [--no-semver] 9 | # e.g. `bash scripts/version_bump.sh` 10 | # e.g. `bash scripts/version_bump.sh --no-semver` 11 | 12 | # Exit immediately if a command exits with a non-zero status 13 | set -e 14 | 15 | # Use the script folder to refer to other scripts. 16 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 17 | SCRIPT_VERSION_NUMBER="$FOLDER/version_number.sh" 18 | 19 | 20 | # Parse --no-semver argument 21 | VALIDATE_SEMVER=true 22 | for arg in "$@"; do 23 | case $arg in 24 | --no-semver) 25 | VALIDATE_SEMVER=false 26 | shift # Remove --no-semver from processing 27 | ;; 28 | esac 29 | done 30 | 31 | # Start script 32 | echo "" 33 | echo "Bumping version number..." 34 | echo "" 35 | 36 | # Get the latest version 37 | VERSION=$($SCRIPT_VERSION_NUMBER) 38 | if [ $? -ne 0 ]; then 39 | echo "Failed to get the latest version" 40 | exit 1 41 | fi 42 | 43 | # Print the current version 44 | echo "The current version is: $VERSION" 45 | 46 | # Function to validate semver format, including optional -rc. suffix 47 | validate_semver() { 48 | if [ "$VALIDATE_SEMVER" = false ]; then 49 | return 0 50 | fi 51 | 52 | if [[ $1 =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then 53 | return 0 54 | else 55 | return 1 56 | fi 57 | } 58 | 59 | # Prompt user for new version 60 | while true; do 61 | read -p "Enter the new version number: " NEW_VERSION 62 | 63 | # Validate the version number to ensure that it's a semver version 64 | if validate_semver "$NEW_VERSION"; then 65 | break 66 | else 67 | echo "Invalid version format. Please use semver format (e.g., 1.2.3, v1.2.3, 1.2.3-rc.1, etc.)." 68 | exit 1 69 | fi 70 | done 71 | 72 | # Push the new tag 73 | git push -u origin HEAD 74 | git tag $NEW_VERSION 75 | git push --tags 76 | 77 | # Complete successfully 78 | echo "" 79 | echo "Version tag pushed successfully!" 80 | echo "" 81 | -------------------------------------------------------------------------------- /scripts/version_number.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script returns the latest project version. 5 | 6 | # Usage: 7 | # version_number.sh 8 | # e.g. `bash scripts/version_number.sh` 9 | 10 | # Exit immediately if a command exits with a non-zero status 11 | set -e 12 | 13 | # Check if the current directory is a Git repository 14 | if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then 15 | echo "Error: Not a Git repository" 16 | exit 1 17 | fi 18 | 19 | # Fetch all tags 20 | git fetch --tags > /dev/null 2>&1 21 | 22 | # Get the latest semver tag 23 | latest_version=$(git tag -l --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1) 24 | 25 | # Check if we found a version tag 26 | if [ -z "$latest_version" ]; then 27 | echo "Error: No semver tags found in this repository" >&2 28 | exit 1 29 | fi 30 | 31 | # Print the latest version 32 | echo "$latest_version" 33 | -------------------------------------------------------------------------------- /scripts/version_validate_git.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script validates the Git repository for release. 5 | # You can pass in a to validate any non-main branch. 6 | 7 | # Usage: 8 | # version_validate_git.sh " 9 | # e.g. `bash scripts/version_validate_git.sh master` 10 | 11 | # This script will: 12 | # * Validate that the script is run within a git repository. 13 | # * Validate that the git repository doesn't have any uncommitted changes. 14 | # * Validate that the current git branch matches the provided one. 15 | 16 | # Exit immediately if a command exits with a non-zero status 17 | set -e 18 | 19 | # Verify that all required arguments are provided 20 | if [ $# -eq 0 ]; then 21 | echo "Error: This script requires exactly one argument" 22 | echo "Usage: $0 " 23 | exit 1 24 | fi 25 | 26 | # Create local argument variables. 27 | BRANCH=$1 28 | 29 | # Start script 30 | echo "" 31 | echo "Validating git repository..." 32 | echo "" 33 | 34 | # Check if the current directory is a Git repository 35 | if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then 36 | echo "Error: Not a Git repository" 37 | exit 1 38 | fi 39 | 40 | # Check for uncommitted changes 41 | if [ -n "$(git status --porcelain)" ]; then 42 | echo "Error: Git repository is dirty. There are uncommitted changes." 43 | exit 1 44 | fi 45 | 46 | # Verify that we're on the correct branch 47 | current_branch=$(git rev-parse --abbrev-ref HEAD) 48 | if [ "$current_branch" != "$BRANCH" ]; then 49 | echo "Error: Not on the specified branch. Current branch is $current_branch, expected $1." 50 | exit 1 51 | fi 52 | 53 | # The Git repository validation succeeded. 54 | echo "" 55 | echo "Git repository validated successfully!" 56 | echo "" 57 | -------------------------------------------------------------------------------- /scripts/version_validate_target.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Documentation: 4 | # This script validates a for release. 5 | # This script targets iOS, macOS, tvOS, watchOS, and xrOS by default. 6 | # You can pass in a list of if you want to customize the build. 7 | 8 | # Usage: 9 | # version_validate_target.sh [ default:iOS macOS tvOS watchOS xrOS]" 10 | # e.g. `bash scripts/version_validate_target.sh iOS macOS` 11 | 12 | # This script will: 13 | # * Validate that swiftlint passes. 14 | # * Validate that all unit tests passes for all . 15 | 16 | # Exit immediately if a command exits with a non-zero status 17 | set -e 18 | 19 | # Verify that all requires at least one argument" 20 | if [ $# -eq 0 ]; then 21 | echo "Error: This script requires at least one argument" 22 | echo "Usage: $0 [ default:iOS macOS tvOS watchOS xrOS]" 23 | exit 1 24 | fi 25 | 26 | # Create local argument variables. 27 | TARGET=$1 28 | 29 | # Remove TARGET from arguments list 30 | shift 31 | 32 | # Define platforms variable 33 | if [ $# -eq 0 ]; then 34 | set -- iOS macOS tvOS watchOS xrOS 35 | fi 36 | PLATFORMS=$@ 37 | 38 | # Use the script folder to refer to other scripts. 39 | FOLDER="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 40 | SCRIPT_TEST="$FOLDER/test.sh" 41 | 42 | # A function that run a certain script and checks for errors 43 | run_script() { 44 | local script="$1" 45 | shift # Remove the first argument (script path) from the argument list 46 | 47 | if [ ! -f "$script" ]; then 48 | echo "Error: Script not found: $script" 49 | exit 1 50 | fi 51 | 52 | chmod +x "$script" 53 | if ! "$script" "$@"; then 54 | echo "Error: Script $script failed" 55 | exit 1 56 | fi 57 | } 58 | 59 | # Start script 60 | echo "" 61 | echo "Validating project..." 62 | echo "" 63 | 64 | # Run SwiftLint 65 | echo "Running SwiftLint" 66 | if ! swiftlint --strict; then 67 | echo "Error: SwiftLint failed" 68 | exit 1 69 | fi 70 | 71 | # Run unit tests 72 | echo "Testing..." 73 | run_script "$SCRIPT_TEST" "$TARGET" "$PLATFORMS" 74 | 75 | # Complete successfully 76 | echo "" 77 | echo "Project successfully validated!" 78 | echo "" 79 | --------------------------------------------------------------------------------