diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 09c9019..5ec44e6 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,5 +1,5 @@ # -# This source file is part of the TemplatePackage open source project +# This source file is part of the SpeziNotifications open source project # # SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) # @@ -14,79 +14,66 @@ on: jobs: reuse_action: name: REUSE Compliance Check - uses: StanfordBDHG/.github/.github/workflows/reuse.yml@v2 + uses: StanfordSpezi/.github/.github/workflows/reuse.yml@v2 swiftlint: name: SwiftLint - uses: StanfordBDHG/.github/.github/workflows/swiftlint.yml@v2 + uses: StanfordSpezi/.github/.github/workflows/swiftlint.yml@v2 packageios: name: Build and Test Swift Package iOS - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 with: - scheme: TemplatePackage - resultBundle: TemplatePackage-iOS.xcresult - artifactname: TemplatePackage-iOS.xcresult + runsonlabels: '["macOS", "self-hosted"]' + scheme: SpeziNotifications-Package + resultBundle: SpeziNotifications-iOS.xcresult + artifactname: SpeziNotifications-iOS.xcresult packagewatchos: name: Build and Test Swift Package watchOS - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 with: - scheme: TemplatePackage - destination: 'platform=watchOS Simulator,name=Apple Watch Series 9 (45mm)' - resultBundle: TemplatePackage-watchOS.xcresult - artifactname: TemplatePackage-watchOS.xcresult + runsonlabels: '["macOS", "self-hosted"]' + scheme: SpeziNotifications-Package + destination: 'platform=watchOS Simulator,name=Apple Watch Series 10 (46mm)' + resultBundle: SpeziNotifications-watchOS.xcresult + artifactname: SpeziNotifications-watchOS.xcresult packagevisionos: name: Build and Test Swift Package visionOS - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 with: - scheme: TemplatePackage + runsonlabels: '["macOS", "self-hosted"]' + scheme: SpeziNotifications-Package destination: 'platform=visionOS Simulator,name=Apple Vision Pro' - resultBundle: TemplatePackage-visionOS.xcresult - artifactname: TemplatePackage-visionOS.xcresult + resultBundle: SpeziNotifications-visionOS.xcresult + artifactname: SpeziNotifications-visionOS.xcresult packagetvos: name: Build and Test Swift Package tvOS - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 with: - scheme: TemplatePackage - resultBundle: TemplatePackage-tvOS.xcresult + runsonlabels: '["macOS", "self-hosted"]' + scheme: SpeziNotifications-Package + resultBundle: SpeziNotifications-tvOS.xcresult destination: 'platform=tvOS Simulator,name=Apple TV 4K (3rd generation)' - artifactname: TemplatePackage-tvOS.xcresult + artifactname: SpeziNotifications-tvOS.xcresult packagemacos: name: Build and Test Swift Package macOS - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 with: - scheme: TemplatePackage - resultBundle: TemplatePackage-macOS.xcresult + runsonlabels: '["macOS", "self-hosted"]' + scheme: SpeziNotifications-Package + resultBundle: SpeziNotifications-macOS.xcresult destination: 'platform=macOS,arch=arm64' - artifactname: TemplatePackage-macOS.xcresult + artifactname: SpeziNotifications-macOS.xcresult ios: name: Build and Test iOS - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 with: + runsonlabels: '["macOS", "self-hosted"]' path: 'Tests/UITests' scheme: TestApp resultBundle: TestApp-iOS.xcresult artifactname: TestApp-iOS.xcresult - ipados: - name: Build and Test iPadOS - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 - with: - path: 'Tests/UITests' - scheme: TestApp - destination: 'platform=iOS Simulator,name=iPad Air (5th generation)' - resultBundle: TestApp-iPadOS.xcresult - artifactname: TestApp-iPadOS.xcresult - watchos: - name: Build and Test watchOS - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 - with: - runsonlabels: '["macOS", "self-hosted"]' - path: 'Tests/UITests' - scheme: TestAppWatchApp - destination: 'platform=watchOS Simulator,name=Apple Watch Series 9 (45mm)' - resultBundle: TestApp-watchOS.xcresult - artifactname: TestApp-watchOS.xcresult visionos: name: Build and Test visionOS - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 with: runsonlabels: '["macOS", "self-hosted"]' path: 'Tests/UITests' @@ -94,30 +81,11 @@ jobs: destination: 'platform=visionOS Simulator,name=Apple Vision Pro' resultBundle: TestApp-visionOS.xcresult artifactname: TestApp-visionOS.xcresult - tvos: - name: Build and Test tvOS - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 - with: - path: 'Tests/UITests' - scheme: TestApp - destination: 'platform=tvOS Simulator,name=Apple TV 4K (3rd generation)' - resultBundle: TestApp-tvOS.xcresult - artifactname: TestApp-tvOS.xcresult - codeql: - name: CodeQL - uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 - with: - codeql: true - test: false - scheme: TemplatePackage - permissions: - security-events: write - actions: read uploadcoveragereport: name: Upload Coverage Report - needs: [packageios, packagewatchos, packagevisionos, packagetvos, packagemacos, ios, ipados, watchos, visionos, tvos] - uses: StanfordBDHG/.github/.github/workflows/create-and-upload-coverage-report.yml@v2 + needs: [packageios, packagewatchos, packagevisionos, packagetvos, packagemacos, ios, visionos] + uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2 with: - coveragereports: TemplatePackage-iOS.xcresult TemplatePackage-watchOS.xcresult TemplatePackage-visionOS.xcresult TemplatePackage-tvOS.xcresult TemplatePackage-macOS.xcresult TestApp-iOS.xcresult TestApp-iPadOS.xcresult TestApp-watchOS.xcresult TestApp-visionOS.xcresult TestApp-tvOS.xcresult + coveragereports: SpeziNotifications-iOS.xcresult SpeziNotifications-watchOS.xcresult SpeziNotifications-visionOS.xcresult SpeziNotifications-tvOS.xcresult SpeziNotifications-macOS.xcresult TestApp-iOS.xcresult TestApp-visionOS.xcresult secrets: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c64e9ff..71446a9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,5 @@ # -# This source file is part of the TemplatePackage open source project +# This source file is part of the SpeziNotifications open source project # # SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) # diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 272edeb..165ce1d 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,5 +1,5 @@ # -# This source file is part of the TemplatePackage open source project +# This source file is part of the SpeziNotifications open source project # # SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) # diff --git a/.gitignore b/.gitignore index f9a765f..6cb73f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # -# This source file is part of the TemplatePackage open source project +# This source file is part of the SpeziNotifications open source project # # SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) # diff --git a/.spi.yml b/.spi.yml index 504bb5a..0a55c66 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,5 +1,5 @@ # -# This source file is part of the TemplatePackage open source project +# This source file is part of the SpeziNotifications open source project # # SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) # @@ -11,4 +11,6 @@ builder: configs: - platform: ios documentation_targets: - - TemplatePackage + - SpeziNotifications + - XCTSpeziNotifications + - XCTSpeziNotificationsUI diff --git a/.swiftlint.yml b/.swiftlint.yml index 3c7bab5..a122698 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,5 +1,5 @@ # -# This source file is part of the TemplatePackage open source project +# This source file is part of the SpeziNotifications open source project # # SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) # @@ -367,13 +367,6 @@ only_rules: # The variable should be placed on the left, the constant on the right of a comparison operator. - yoda_condition -attributes: - attributes_with_arguments_always_on_line_above: false - -deployment_target: # Availability checks or attributes shouldn’t be using older versions that are satisfied by the deployment target. - iOSApplicationExtension_deployment_target: 16.0 - iOS_deployment_target: 16.0 - excluded: # paths to ignore during linting. Takes precedence over `included`. - .build - .swiftpm diff --git a/CITATION.cff b/CITATION.cff index ee53639..f5689b6 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,7 +1,7 @@ # -# This source file is part of the TemplatePackage open source project +# This source file is part of the SpeziNotifications open source project # -# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) # # SPDX-License-Identifier: MIT # @@ -12,9 +12,9 @@ authors: - family-names: "Schmiedmayer" given-names: "Paul" orcid: "https://orcid.org/0000-0002-8607-9148" -- family-names: "Ravi" - given-names: "Vishnu" - orcid: "https://orcid.org/0000-0003-0359-1275" -title: "TemplatePackage" +- family-names: "Bauer" + given-names: "Andreas" + orcid: "https://orcid.org/0000-0002-1680-237X" +title: "SpeziNotifications" doi: 10.5281/zenodo.7538165 -url: "https://github.com/StanfordBDHG/SwiftPackageTemplate" +url: "https://github.com/StanfordSpezi/SpeziNotifications" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7574c84..a53f7fe 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,17 +1,17 @@ -TemplatePackage contributors +SpeziNotifications contributors ==================== * [Paul Schmiedmayer](https://github.com/PSchmiedmayer) -* [Vishnu Ravi](https://github.com/vishnuravi) +* [Andreas Bauer](https://github.com/Supereg) diff --git a/LICENSE.md b/LICENSE.md index 6998b5f..f127054 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,9 +1,9 @@ MIT License -Copyright (c) 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +Copyright (c) 2024 Stanford University and the project authors (see CONTRIBUTORS.md) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt index 6998b5f..f127054 100644 --- a/LICENSES/MIT.txt +++ b/LICENSES/MIT.txt @@ -1,9 +1,9 @@ MIT License -Copyright (c) 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +Copyright (c) 2024 Stanford University and the project authors (see CONTRIBUTORS.md) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Package.swift b/Package.swift index d567050..6740cd9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,18 +1,20 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 // -// This source file is part of the TemplatePackage open source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// +// This source file is part of the SpeziNotifications open source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// // SPDX-License-Identifier: MIT // +import class Foundation.ProcessInfo import PackageDescription let package = Package( - name: "TemplatePackage", + name: "SpeziNotifications", + defaultLocalization: "en", platforms: [ .iOS(.v17), .watchOS(.v10), @@ -21,17 +23,63 @@ let package = Package( .macOS(.v14) ], products: [ - .library(name: "TemplatePackage", targets: ["TemplatePackage"]) + .library(name: "SpeziNotifications", targets: ["SpeziNotifications"]), + .library(name: "XCTSpeziNotifications", targets: ["XCTSpeziNotifications"]), + .library(name: "XCTSpeziNotificationsUI", targets: ["XCTSpeziNotificationsUI"]) ], + dependencies: [ + .package(url: "https://github.com/StanfordSpezi/Spezi.git", branch: "feature/application-for-swiftui"), + .package(url: "https://github.com/StanfordSpezi/SpeziViews.git", branch: "feature/additional-infrastructure") + ] + swiftLintPackage(), targets: [ .target( - name: "TemplatePackage" + name: "SpeziNotifications", + dependencies: [ + .product(name: "Spezi", package: "Spezi") + ], + plugins: [] + swiftLintPlugin() + ), + .target( + name: "XCTSpeziNotifications", + dependencies: [ + .target(name: "SpeziNotifications") + ], + plugins: [] + swiftLintPlugin() + ), + .target( + name: "XCTSpeziNotificationsUI", + dependencies: [ + .target(name: "SpeziNotifications"), + .product(name: "SpeziViews", package: "SpeziViews") + ], + plugins: [] + swiftLintPlugin() ), .testTarget( - name: "TemplatePackageTests", + name: "SpeziNotificationsTests", dependencies: [ - .target(name: "TemplatePackage") - ] + .target(name: "SpeziNotifications"), + .product(name: "Spezi", package: "Spezi"), + .product(name: "XCTSpezi", package: "Spezi") + ], + plugins: [] + swiftLintPlugin() ) ] ) + + +func swiftLintPlugin() -> [Target.PluginUsage] { + // Fully quit Xcode and open again with `open --env SPEZI_DEVELOPMENT_SWIFTLINT /Applications/Xcode.app` + if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil { + [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")] + } else { + [] + } +} + +func swiftLintPackage() -> [PackageDescription.Package.Dependency] { + if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil { + [.package(url: "https://github.com/realm/SwiftLint.git", from: "0.55.1")] + } else { + [] + } +} diff --git a/README.md b/README.md index d2e1676..87d6d0c 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,126 @@ -# TemplatePackage +# SpeziNotifications -[![Build and Test](https://github.com/StanfordBDHG/SwiftPackageTemplate/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/StanfordBDHG/SwiftPackageTemplate/actions/workflows/build-and-test.yml) -[![codecov](https://codecov.io/gh/StanfordBDHG/SwiftPackageTemplate/branch/main/graph/badge.svg?token=X7BQYSUKOH)](https://codecov.io/gh/StanfordBDHG/SwiftPackageTemplate) -[![DOI](https://zenodo.org/badge/573230182.svg)](https://zenodo.org/badge/latestdoi/573230182) -[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordBDHG%2FSwiftPackageTemplate%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/StanfordBDHG/SwiftPackageTemplate) -[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordBDHG%2FSwiftPackageTemplate%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/StanfordBDHG/SwiftPackageTemplate) +[![Build and Test](https://github.com/StanfordSpezi/SpeziNotifications/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/StanfordSpezi/SpeziNotifications/actions/workflows/build-and-test.yml) +[![codecov](https://codecov.io/gh/StanfordSpezi/SpeziNotifications/graph/badge.svg?token=dWaDzUBFoV)](https://codecov.io/gh/StanfordSpezi/SpeziNotifications) +Simplify User Notifications in Spezi-based applications. -## How To Use This Template +## Overview + +SpeziNotifications simplifies interaction with user notifications by adding additional actions to the Environment of SwiftUI Views and +Spezi Modules. -The template repository contains a template Swift Package, including a continuous integration setup. +### Schedule Notifications -Follow these steps to customize it to your needs: -1. Rename the Swift Package. Be sure that you update the name in the `build-and-test.yml` GitHub Action accordingly. If you have multiple targets in your Swift Package, you need to pass the name of the Swift Package followed by an `-Package` as the scheme to the GitHub Action, e.g., `StanfordProject-Package` if your Swift Package is named `StanfordProject`. -2. If your Swift Package does not provide any user interface or does not require an iOS application environment to function, you can remove the `UITests` application from the `Tests` folder. You need to update the `build-and-test.yml` GitHub Action accordingly by removing the GitHub Action that builds and tests the application, removing the dependency from the code coverage upload step, and removing the UI test `.xresult` input from the code coverage test. -3. If your Swift Package uses UI test, you need to ... - - ... add it to the scheme editor (*Scheme > Edit Scheme*) and your targets to the "Build" configuration and ensure that it is built before the test app target when building for the "Test" configuration. It is not required to enable building for other configurations like "Analyze", "Run", "Profile", or "Archive". - - ... add it as a linked framework in the main target configuration (In your Xcode project settings, select your *test app target > General > Frameworks, Libraries, and Embedded Comments*). - - ... add ensure that the targets are all added in the code coverage settings of your .xctestplan file in the Xcode Project (*Shared Settings > Code Coverage > Code Coverage*). -4. You will either need to add the [CodeCov GitHub App](https://github.com/apps/codecov) or add a codecov.io token to your [GitHub Actions Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-an-environment) following the instructions of the [Codecov GitHub Action](https://github.com/marketplace/actions/codecov#usage). The StanfordBDHG organization already has the [CodeCov GitHub App](https://github.com/apps/codecov) installed. If you do not want to cover test coverage data, you can remove the code coverage job in the `build-and-test.yml` GitHub Action. -5. Adjust this README.md to describe your project and adjust the badges at the top to point to the correct GitHub Action of your repository and Codecov badge. -6. The Swift Package template includes a Swift Package Index configuration file to automatically build the package and [host the documentation on the Swift Package Index website](https://blog.swiftpackageindex.com/posts/auto-generating-auto-hosting-and-auto-updating-docc-documentation/). Adjust the `.spi.yml` file to include all targets that you want to build documentation for. You can follow the [instructions of the Swift Package Index](https://swiftpackageindex.com/add-a-package) to include your Swift Package in the Swift Package Index. You can link to the [API documentation](https://swiftpackageindex.com/StanfordBDHG/SwiftPackageTemplate/documentation) from your README file. -7. Adjust the CITATION.cff file to amend information about the new Swift Package ([learn more about CITATION files on GitHub](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-citation-files)) and [register the Swift Package on Zenodo](https://docs.github.com/en/repositories/archiving-a-github-repository/referencing-and-citing-content). +You can use the [`Notifications`]((https://swiftpackageindex.com/stanfordspezi/spezinotifications/documentation/spezinotifications/notifications)) +module to interact with user notifications within your application. You can either define it as a dependency +of your Spezi [`Module`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module) +or retrieve it from the environment using the [`@Environment`](https://developer.apple.com/documentation/swiftui/environment) +property wrapper in your SwiftUI View. +The code example below schedules a notification request, accessing the `Notifications` module from within the custom `MyNotifications` module. -## Installation +```swift +import Spezi +import UserNotifications -The project can be added to your Xcode project or Swift Package using the [Swift Package Manager](https://github.com/apple/swift-package-manager). -**Xcode:** For an Xcode project, follow the instructions on [adding package dependencies to your app](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app). +final class MyNotifications: Module { + @Dependency(Notifications.self) + private var notifications -**Swift Package:** You can follow the [Swift Package Manager documentation about defining dependencies](https://github.com/apple/swift-package-manager/blob/main/Documentation/Usage.md#defining-dependencies) to add this project as a dependency to your Swift Package. + @Application(\.notificationSettings) + private var settings + func scheduleAppointmentReminder() async throws { + let status = await settings().authorizationStatus + guard status == .authorized || status == .provisional else { + return // no authorization to schedule notification + } + + let content = UNMutableNotificationContent() + content.title = "Your Appointment" + content.body = "Your appointment is in 3 hours" + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3 * 60, repeats: false) + + let request = UNNotificationRequest(identifier: "3-hour-reminder", content: content, trigger: trigger) + + try await notifications.add(request: request) + } +} +``` + +### Requesting Authorization in SwiftUI + +The Notification module and notification-related actions are also available in the SwiftUI Environment. The code example below creates a simple +notification authorization onboarding view that (1) determines the current authorization status and (2) request notification authorization +when the user taps the button. + + +```swift +import SpeziNotifications +import SpeziViews + +struct NotificationOnboarding: View { + @Environment(\.notificationSettings) + private var notificationSettings + @Environment(\.requestNotificationAuthorization) + private var requestNotificationAuthorization + + @State private var viewState: ViewState = .idle + @State private var notificationsAuthorized = false + + var body: some View { + VStack { + // ... + if notificationsAuthorized { + Button("Continue") { + // show next view ... + } + } else { + AsyncButton("Allow Notifications", state: $viewState) { + try await requestNotificationAuthorization(options: [.alert, .badge, .sound]) + } + .environment(\.defaultErrorDescription, "Failed to request notification authorization.") + } + } + .viewStateAlert(state: $viewState) + .task { + notificationsAuthorized = await notificationSettings().authorizationStatus == .authorized + } + } +} +``` + +> [!IMPORTANT] +> The example above uses the [`AsyncButton`](https://swiftpackageindex.com/stanfordspezi/speziviews/documentation/speziviews/asyncbutton) +> and the [`ViewState`](https://swiftpackageindex.com/stanfordspezi/speziviews/documentation/speziviews/viewstate) model from SpeziViews to more +> easily manage the state of asynchronous actions and handle erroneous conditions. + +## Setup + +You need to add the SpeziNotifications Swift package to +[your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or +[Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package). ## License -This project is licensed under the MIT License. See [Licenses](https://github.com/StanfordBDHG/TemplatePackage/tree/main/LICENSES) for more information. +This project is licensed under the MIT License. See [Licenses](https://github.com/StanfordSpezi/SpeziNotifications/tree/main/LICENSES) for more information. ## Contributors -This project is developed as part of the Stanford Byers Center for Biodesign at Stanford University. -See [CONTRIBUTORS.md](https://github.com/StanfordBDHG/TemplatePackage/tree/main/CONTRIBUTORS.md) for a full list of all TemplatePackage contributors. +This project is developed as part of the Stanford Mussallem Center for Biodesign at Stanford University. +See [CONTRIBUTORS.md](https://github.com/StanfordBDHG/StanfordSpezi/tree/main/CONTRIBUTORS.md) for a full list of all SpeziNotifications contributors. -![Stanford Byers Center for Biodesign Logo](https://raw.githubusercontent.com/StanfordBDHG/.github/main/assets/biodesign-footer-light.png#gh-light-mode-only) -![Stanford Byers Center for Biodesign Logo](https://raw.githubusercontent.com/StanfordBDHG/.github/main/assets/biodesign-footer-dark.png#gh-dark-mode-only) +![Stanford Mussallem Center for Biodesign Logo](https://raw.githubusercontent.com/StanfordBDHG/.github/main/assets/biodesign-footer-light.png#gh-light-mode-only) +![Stanford Mussallem Center for Biodesign Logo](https://raw.githubusercontent.com/StanfordBDHG/.github/main/assets/biodesign-footer-dark.png#gh-dark-mode-only) diff --git a/Sources/SpeziNotifications/Actions/NotificationSettings.swift b/Sources/SpeziNotifications/Actions/NotificationSettings.swift new file mode 100644 index 0000000..2d86842 --- /dev/null +++ b/Sources/SpeziNotifications/Actions/NotificationSettings.swift @@ -0,0 +1,88 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Spezi +import SwiftUI +import UserNotifications + + +extension Spezi { + /// An action to request the current user notifications settings. + /// + /// Refer to ``Spezi/notificationSettings`` for documentation. + public struct NotificationSettingsAction { + fileprivate init() {} + + /// Request the current user notification settings. + /// - Returns: Returns the current user notification settings. + public func callAsFunction() async -> sending UNNotificationSettings { + await UNUserNotificationCenter.current().notificationSettings() + } + } + + /// Retrieve the current notification settings of the application. + /// + /// ```swift + /// struct MyModule: Module { + /// @Application(\.notificationSettings) + /// private var notificationSettings + /// + /// func deliverNotification(request: UNNotificationRequest) async throws { + /// let settings = await notificationSettings() + /// guard settings.authorizationStatus == .authorized + /// || settings.authorizationStatus == .provisional else { + /// return // notifications not permitted + /// } + /// + /// // continue to add the notification request to the center ... + /// } + /// } + /// ``` + /// + /// ## Topics + /// ### Action + /// - ``NotificationSettingsAction`` + public var notificationSettings: NotificationSettingsAction { + NotificationSettingsAction() + } +} + + +extension EnvironmentValues { + /// Retrieve the current notification settings of the application. + /// + /// ```swift + /// struct MyView: View { + /// @Environment(\.notificationSettings) + /// private var notificationSettings + /// + /// @State private var requestAuthorization = false + /// + /// var body: some View { + /// List { + /// if requestAuthorization { + /// Button("Allow Notifications") { + /// Task { + /// try await requestNotificationAuthorization(options: [.alert, .badge, .sound]) + /// } + /// } + /// } + /// } + /// .task { + /// requestAuthorization = try? await notificationSettings() != .authorized + /// } + /// } + /// } + /// ``` + public var notificationSettings: Spezi.NotificationSettingsAction { + Spezi.NotificationSettingsAction() + } +} + + +extension Spezi.NotificationSettingsAction: Sendable {} diff --git a/Sources/SpeziNotifications/Actions/RegisterForRemoteNotificationsAction.swift b/Sources/SpeziNotifications/Actions/RegisterForRemoteNotificationsAction.swift new file mode 100644 index 0000000..39f7c16 --- /dev/null +++ b/Sources/SpeziNotifications/Actions/RegisterForRemoteNotificationsAction.swift @@ -0,0 +1,158 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +@_spi(APISupport) +import Spezi +import SpeziFoundation +import SwiftUI + + +extension Spezi { + /// Registers to receive remote notifications through Apple Push Notification service. + /// + /// Refer to the documentation of ``Spezi/registerRemoteNotifications``. + public struct RegisterForRemoteNotificationsAction { + private weak var spezi: Spezi? + + fileprivate init(_ spezi: Spezi) { + self.spezi = spezi + } + + /// Registers to receive remote notifications through Apple Push Notification service. + /// + /// - Returns: A globally unique token that identifies this device to APNs. + /// Send this token to the server that you use to generate remote notifications. + /// Your server must pass this token unmodified back to APNs when sending those remote notifications. + /// For more information refer to the documentation of + /// [`application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622958-application). + /// - Throws: Registration might fail if the user's device isn't connected to the network or + /// if your app is not properly configured for remote notifications. It might also throw a `TimeoutError` when running on a simulator device running on a host + /// that is not connected to an Apple ID. + @discardableResult + @MainActor + public func callAsFunction() async throws -> Data { + guard let spezi else { + preconditionFailure("RegisterRemoteNotificationsAction was used in a scope where Spezi was not available anymore!") + } + + return try await spezi.remoteNotificationRegistrationSupport() + } + } + + /// Registers to receive remote notifications through Apple Push Notification service. + /// + /// For more information refer to the [`registerForRemoteNotifications()`](https://developer.apple.com/documentation/uikit/uiapplication/1623078-registerforremotenotifications) + /// documentation for `UIApplication` or for the respective equivalent for your current platform. + /// + /// - Note: For more information on the general topic on how to register your app with APNs, + /// refer to the [Registering your app with APNs](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns) + /// article. + /// + /// Below is a short code example on how to use this action within your [`Module`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). + /// + /// - Warning: Registering for Remote Notifications on Simulator devices might not be possible if your are not signed into an Apple ID on the host machine. + /// The method might throw a [`TimeoutError`](https://swiftpackageindex.com/stanfordspezi/spezifoundation/documentation/spezifoundation/timeouterror) + /// in such a case. + /// + /// ```swift + /// import SpeziFoundation + /// + /// class ExampleModule: Module { + /// @Application(\.registerRemoteNotifications) + /// var registerRemoteNotifications + /// + /// func handleNotificationsPermissions() async throws { + /// // Make sure to request notifications permissions before registering for remote notifications ... + /// + /// + /// do { + /// let deviceToken = try await registerRemoteNotifications() + /// } catch let error as TimeoutError { + /// #if targetEnvironment(simulator) + /// return // override logic when running within a simulator + /// #else + /// throw error + /// #endif + /// } + /// + /// // .. send the device token to your remote server that generates push notifications + /// } + /// } + /// ``` + /// + /// > Tip: Make sure to request authorization by calling ``requestNotificationAuthorization`` + /// to have your remote notifications be able to display alerts, badges or use sound. Otherwise, all remote notifications will be delivered silently. + /// + /// ## Topics + /// ### Action + /// - ``RegisterForRemoteNotificationsAction`` + public var registerRemoteNotifications: RegisterForRemoteNotificationsAction { + RegisterForRemoteNotificationsAction(self) + } +} + + +extension EnvironmentValues { + /// Registers to receive remote notifications through Apple Push Notification service. + /// + /// For more information refer to the [`registerForRemoteNotifications()`](https://developer.apple.com/documentation/uikit/uiapplication/1623078-registerforremotenotifications) + /// documentation for `UIApplication` or for the respective equivalent for your current platform. + /// + /// - Note: For more information on the general topic on how to register your app with APNs, + /// refer to the [Registering your app with APNs](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns) + /// article. + /// + /// Below is a short code example on how to use this action within your [`Module`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). + /// + /// - Warning: Registering for Remote Notifications on Simulator devices might not be possible if your are not signed into an Apple ID on the host machine. + /// The method might throw a [`TimeoutError`](https://swiftpackageindex.com/stanfordspezi/spezifoundation/documentation/spezifoundation/timeouterror) + /// in such a case. + /// + /// ```swift + /// import SpeziFoundation + /// + /// struct ExampleView: View { + /// @Environment(\.registerRemoteNotifications) + /// private var registerRemoteNotifications + /// + /// var body: some View { + /// // ... + /// } + /// + /// private func handleNotificationsPermissions() async throws { + /// // Make sure to request notifications permissions before registering for remote notifications + /// try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) + /// + /// + /// do { + /// let deviceToken = try await registerRemoteNotifications() + /// } catch let error as TimeoutError { + /// #if targetEnvironment(simulator) + /// return // override logic when running within a simulator + /// #else + /// throw error + /// #endif + /// } + /// + /// // .. send the device token to your remote server that generates push notifications + /// } + /// } + /// ``` + /// + /// > Tip: Make sure to request authorization by calling ``requestNotificationAuthorization`` + /// to have your remote notifications be able to display alerts, badges or use sound. Otherwise, all remote notifications will be delivered silently. + @MainActor public var registerRemoteNotifications: Spezi.RegisterForRemoteNotificationsAction { + guard let spezi = SpeziAppDelegate.spezi else { + preconditionFailure("@Environment(\\.registerRemoteNotifications) can only be accessed within a Spezi application.") + } + return Spezi.RegisterForRemoteNotificationsAction(spezi) + } +} + + +extension Spezi.RegisterForRemoteNotificationsAction: Sendable {} diff --git a/Sources/SpeziNotifications/Actions/RequestNotificationAuthorization.swift b/Sources/SpeziNotifications/Actions/RequestNotificationAuthorization.swift new file mode 100644 index 0000000..0d0dd44 --- /dev/null +++ b/Sources/SpeziNotifications/Actions/RequestNotificationAuthorization.swift @@ -0,0 +1,73 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Spezi +import SwiftUI +import UserNotifications + + +extension Spezi { + /// An action to request notification authorization. + /// + /// Refer to ``Spezi/requestNotificationAuthorization`` for documentation. + public struct RequestNotificationAuthorizationAction { + fileprivate init() {} + + /// Request notification authorization. + /// - Parameter options: The authorization options your app is requesting. + public func callAsFunction(options: UNAuthorizationOptions) async throws { + try await UNUserNotificationCenter.current().requestAuthorization(options: options) + } + } + + /// Request notification authorization. + /// + /// ```swift + /// struct MyModule: Module { + /// @Application(\.requestNotificationAuthorization) + /// private var requestNotificationAuthorization + /// + /// func notificationPermissionWhileOnboarding() async throws -> Bool { + /// try await requestNotificationAuthorization(options: [.alert, .badge, .sound]) + /// } + /// } + /// ``` + /// + /// ## Topics + /// ### Action + /// - ``RequestNotificationAuthorizationAction`` + public var requestNotificationAuthorization: RequestNotificationAuthorizationAction { + RequestNotificationAuthorizationAction() + } +} + + +extension EnvironmentValues { + /// Request notification authorization. + /// + /// ```swift + /// struct MyView: View { + /// @Environment(\.requestNotificationAuthorization) + /// private var requestNotificationAuthorization + /// + /// var body: some View { + /// Button("Allow Notifications") { + /// Task { + /// try await requestNotificationAuthorization(options: [.alert, .badge, .sound]) + /// } + /// } + /// } + /// } + /// ``` + public var requestNotificationAuthorization: Spezi.RequestNotificationAuthorizationAction { + Spezi.RequestNotificationAuthorizationAction() + } +} + + +extension Spezi.RequestNotificationAuthorizationAction: Sendable {} diff --git a/Sources/SpeziNotifications/Actions/UnregisterForRemoteNotificationsAction.swift b/Sources/SpeziNotifications/Actions/UnregisterForRemoteNotificationsAction.swift new file mode 100644 index 0000000..ffb6d63 --- /dev/null +++ b/Sources/SpeziNotifications/Actions/UnregisterForRemoteNotificationsAction.swift @@ -0,0 +1,84 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Spezi +import SwiftUI + + +extension Spezi { + /// Unregisters for all remote notifications received through Apple Push Notification service. + /// + /// Refer to the documentation of ``Spezi/unregisterRemoteNotifications``. + public struct UnregisterForRemoteNotificationsAction { + fileprivate init() {} + + + /// Unregisters for all remote notifications received through Apple Push Notification service. + @MainActor + public func callAsFunction() { + _Application.shared.unregisterForRemoteNotifications() + } + } + + /// Unregisters for all remote notifications received through Apple Push Notification service. + /// + /// For more information refer to the [`unregisterForRemoteNotifications()`](https://developer.apple.com/documentation/uikit/uiapplication/1623093-unregisterforremotenotifications) + /// documentation for `UIApplication` or for the respective equivalent for your current platform. + /// + /// Below is a short code example on how to use this action within your [`Module`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module). + /// + /// ```swift + /// class ExampleModule: Module { + /// @Application(\.unregisterRemoteNotifications) + /// var unregisterRemoteNotifications + /// + /// func onAccountLogout() { + /// // handling your cleanup ... + /// unregisterRemoteNotifications() + /// } + /// } + /// ``` + /// + /// ## Topics + /// ### Action + /// - ``UnregisterForRemoteNotificationsAction`` + public var unregisterRemoteNotifications: UnregisterForRemoteNotificationsAction { + UnregisterForRemoteNotificationsAction() + } +} + + +extension EnvironmentValues { + /// Unregisters for all remote notifications received through Apple Push Notification service. + /// + /// For more information refer to the [`unregisterForRemoteNotifications()`](https://developer.apple.com/documentation/uikit/uiapplication/1623093-unregisterforremotenotifications) + /// documentation for `UIApplication` or for the respective equivalent for your current platform. + /// + /// Below is a short code example on how to use this action within your `View`. + /// + /// ```swift + /// struct ExampleView: View { + /// @Environment(\.unregisterRemoteNotifications) + /// private var unregisterRemoteNotifications + /// + /// var body: some View { + /// Button("Disable Notifications") { + /// Task { + /// try await unregisterRemoteNotifications() + /// } + /// } + /// } + /// } + /// ``` + public var unregisterRemoteNotifications: Spezi.UnregisterForRemoteNotificationsAction { + Spezi.UnregisterForRemoteNotificationsAction() + } +} + + +extension Spezi.UnregisterForRemoteNotificationsAction: Sendable {} diff --git a/Sources/SpeziNotifications/Extensions/UNAuthorizationStatus+Description.swift b/Sources/SpeziNotifications/Extensions/UNAuthorizationStatus+Description.swift new file mode 100644 index 0000000..0060b1c --- /dev/null +++ b/Sources/SpeziNotifications/Extensions/UNAuthorizationStatus+Description.swift @@ -0,0 +1,34 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import UserNotifications + + +extension UNAuthorizationStatus: @retroactive CustomStringConvertible, @retroactive CustomDebugStringConvertible { + public var description: String { + switch self { + case .notDetermined: + "notDetermined" + case .denied: + "denied" + case .authorized: + "authorized" + case .provisional: + "provisional" + case .ephemeral: + "ephemeral" + @unknown default: + "unknown(\(rawValue))" + } + } + + public var debugDescription: String { + description + } +} diff --git a/Sources/SpeziNotifications/Extensions/UNNotificationInterruptionLevel+Description.swift b/Sources/SpeziNotifications/Extensions/UNNotificationInterruptionLevel+Description.swift new file mode 100644 index 0000000..43e4ac1 --- /dev/null +++ b/Sources/SpeziNotifications/Extensions/UNNotificationInterruptionLevel+Description.swift @@ -0,0 +1,32 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import UserNotifications + + +extension UNNotificationInterruptionLevel: @retroactive CustomStringConvertible, @retroactive CustomDebugStringConvertible { + public var description: String { + switch self { + case .passive: + "passive" + case .active: + "active" + case .timeSensitive: + "timeSensitive" + case .critical: + "critical" + @unknown default: + "unknown(\(rawValue))" + } + } + + public var debugDescription: String { + description + } +} diff --git a/Sources/SpeziNotifications/Handler/NotificationHandler.swift b/Sources/SpeziNotifications/Handler/NotificationHandler.swift new file mode 100644 index 0000000..17427a5 --- /dev/null +++ b/Sources/SpeziNotifications/Handler/NotificationHandler.swift @@ -0,0 +1,10 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +// NotificationHandler protocol is currently defined in Spezi. Once Spezi removes it and makes a breaking change, we can move it to this package. +@_exported import protocol Spezi.NotificationHandler diff --git a/Sources/SpeziNotifications/Handler/NotificationTokenHandler.swift b/Sources/SpeziNotifications/Handler/NotificationTokenHandler.swift new file mode 100644 index 0000000..356f69c --- /dev/null +++ b/Sources/SpeziNotifications/Handler/NotificationTokenHandler.swift @@ -0,0 +1,10 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +// NotificationTokenHandler protocol is currently defined in Spezi. Once Spezi removes it and makes a breaking change, we can move it to this package. +@_exported import protocol Spezi.NotificationTokenHandler diff --git a/Sources/SpeziNotifications/Misc/BackgroundFetchResult.swift b/Sources/SpeziNotifications/Misc/BackgroundFetchResult.swift new file mode 100644 index 0000000..6c638fe --- /dev/null +++ b/Sources/SpeziNotifications/Misc/BackgroundFetchResult.swift @@ -0,0 +1,12 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +#if os(iOS) || os(visionOS) || os(tvOS) || os(watchOS) +// BackgroundFetchResult type-alias is currently defined in Spezi. Once Spezi removes it and makes a breaking change, we can move it to this package. +@_exported import typealias Spezi.BackgroundFetchResult +#endif diff --git a/Sources/SpeziNotifications/Notifications.swift b/Sources/SpeziNotifications/Notifications.swift new file mode 100644 index 0000000..ed5cae7 --- /dev/null +++ b/Sources/SpeziNotifications/Notifications.swift @@ -0,0 +1,124 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Spezi +@preconcurrency import UserNotifications + + +/// Interact with local notifications. +/// +/// This module provides some easy to use API to schedule and manage local notifications. +/// +/// ## Topics +/// +/// ### Configuration +/// - ``init()`` +/// +/// ### Badge Count +/// - ``setBadgeCount(isolation:_:)`` +/// +/// ### Add a Notification Request +/// - ``add(isolation:request:)`` +/// +/// ### Notification Limits +/// - ``pendingNotificationsLimit`` +/// - ``remainingNotificationLimit(isolation:)`` +/// +/// ### Fetching Notifications +/// - ``pendingNotificationRequests(isolation:)`` +/// - ``deliveredNotifications(isolation:)`` +/// +/// ### Categories +/// - ``add(isolation:categories:)`` +public final class Notifications: Module, DefaultInitializable, EnvironmentAccessible { + /// The total limit of simultaneously scheduled notifications. + /// + /// The limit is `64`. + public static let pendingNotificationsLimit = 64 + + @Application(\.notificationSettings) + public var notificationSettings + + @Application(\.requestNotificationAuthorization) + public var requestNotificationAuthorization + + /// Configure the local notifications module. + public init() {} + + /// Updates the badge count for your app’s icon. + /// - Parameters: + /// - isolation: Inherits the current isolation. + /// - badgeCount: The new badge count to display. + @available(watchOS, unavailable) + public func setBadgeCount( // swiftlint:disable:this function_default_parameter_at_end + isolation: isolated (any Actor)? = #isolation, + _ badgeCount: Int + ) async throws { + try await UNUserNotificationCenter.current().setBadgeCount(badgeCount) + } + + /// Schedule a new notification request. + /// - Parameters: + /// - isolation: Inherits the current isolation. + /// - request: The notification request. + public func add( // swiftlint:disable:this function_default_parameter_at_end + isolation: isolated (any Actor)? = #isolation, + request: UNNotificationRequest + ) async throws { + try await UNUserNotificationCenter.current().add(request) + } + + /// Retrieve the amount of notifications that can be scheduled for the app. + /// + /// An application has a total limit of ``pendingNotificationsLimit`` that can be scheduled (pending). This method retrieve the reaming notifications that can be scheduled. + /// + /// - Note: Already delivered notifications do not count towards this limit. + /// - Parameter isolation: Inherits the current isolation. + /// - Returns: Returns the remaining amount of notifications that can be scheduled for the application. + public func remainingNotificationLimit(isolation: isolated (any Actor)? = #isolation) async -> Int { + let pendingRequests = await UNUserNotificationCenter.current().pendingNotificationRequests() + return max(0, Self.pendingNotificationsLimit - pendingRequests.count) + } + + /// Fetch all notification requests that are pending delivery. + /// - Parameter isolation: Inherits the current isolation. + /// - Returns: The array of pending notifications requests. + public func pendingNotificationRequests(isolation: isolated (any Actor)? = #isolation) async -> sending [UNNotificationRequest] { + await UNUserNotificationCenter.current().pendingNotificationRequests() + } + + /// Fetch all delivered notifications that are still shown in the notification center. + /// - Parameter isolation: Inherits the current isolation. + /// - Returns: The array of local and remote notifications that have been delivered and are still show in the notification center. + @available(tvOS, unavailable) + public func deliveredNotifications(isolation: isolated (any Actor)? = #isolation) async -> sending [UNNotification] { + await UNUserNotificationCenter.current().deliveredNotifications() + } + + /// Add additional notification categories. + /// + /// This method adds additional notification categories. Call this method within your configure method of your Module to ensure that categories are configured + /// as early as possible. + /// + /// To receive the action that are performed for your category, implement the ``NotificationHandler/handleNotificationAction(_:)`` method of the + /// ``NotificationHandler`` protocol. + /// + /// - Note: Aim to only call this method once at startup. + /// + /// - Parameters: + /// - isolation: Inherits the current isolation. + /// - categories: The notification categories you support. + @available(tvOS, unavailable) + public func add( // swiftlint:disable:this function_default_parameter_at_end + isolation: isolated (any Actor)? = #isolation, + categories: Set + ) async { + let previousCategories = await UNUserNotificationCenter.current().notificationCategories() + UNUserNotificationCenter.current().setNotificationCategories(categories.union(previousCategories)) + } +} diff --git a/Sources/SpeziNotifications/SpeziNotifications.docc/SpeziNotifications.md b/Sources/SpeziNotifications/SpeziNotifications.docc/SpeziNotifications.md new file mode 100644 index 0000000..0954fe4 --- /dev/null +++ b/Sources/SpeziNotifications/SpeziNotifications.docc/SpeziNotifications.md @@ -0,0 +1,124 @@ +# ``SpeziNotifications`` + + + +Simplify User Notifications in Spezi-based applications. + +## Overview + +SpeziNotifications simplifies interaction with user notifications by adding additional actions to the Environment of SwiftUI Views and +Spezi Modules. + +### Schedule Notifications + +You can use the ``Notifications`` module to interact with user notifications within your application. You can either define it as a dependency +of your Spezi [`Module`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module) +or retrieve it from the environment using the [`@Environment`](https://developer.apple.com/documentation/swiftui/environment) +property wrapper in your SwiftUI View. + +The code example below schedules a notification request, accessing the `Notifications` module from within the custom `MyNotifications` module. + +```swift +import Spezi +import UserNotifications + + +final class MyNotifications: Module { + @Dependency(Notifications.self) + private var notifications + + @Application(\.notificationSettings) + private var settings + + func scheduleAppointmentReminder() async throws { + let status = await settings().authorizationStatus + guard status == .authorized || status == .provisional else { + return // no authorization to schedule notification + } + + let content = UNMutableNotificationContent() + content.title = "Your Appointment" + content.body = "Your appointment is in 3 hours" + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3 * 60, repeats: false) + + let request = UNNotificationRequest(identifier: "3-hour-reminder", content: content, trigger: trigger) + + try await notifications.add(request: request) + } +} +``` + +### Requesting Authorization in SwiftUI + +The Notification module and notification-related actions are also available in the SwiftUI Environment. The code example below creates a simple +notification authorization onboarding view that (1) determines the current authorization status and (2) request notification authorization +when the user taps the button. + + +```swift +import SpeziNotifications +import SpeziViews + +struct NotificationOnboarding: View { + @Environment(\.notificationSettings) + private var notificationSettings + @Environment(\.requestNotificationAuthorization) + private var requestNotificationAuthorization + + @State private var viewState: ViewState = .idle + @State private var notificationsAuthorized = false + + var body: some View { + VStack { + // ... + if notificationsAuthorized { + Button("Continue") { + // show next view ... + } + } else { + AsyncButton("Allow Notifications", state: $viewState) { + try await requestNotificationAuthorization(options: [.alert, .badge, .sound]) + } + .environment(\.defaultErrorDescription, "Failed to request notification authorization.") + } + } + .viewStateAlert(state: $viewState) + .task { + notificationsAuthorized = await notificationSettings().authorizationStatus == .authorized + } + } +} +``` + +- Important: The example above uses the [`AsyncButton`](https://swiftpackageindex.com/stanfordspezi/speziviews/documentation/speziviews/asyncbutton) + and the [`ViewState`](https://swiftpackageindex.com/stanfordspezi/speziviews/documentation/speziviews/viewstate) model from SpeziViews to more + easily manage the state of asynchronous actions and handle erroneous conditions. + +## Topics + +### Notifications +- ``Notifications`` +- ``NotificationHandler`` +- ``BackgroundFetchResult`` + +### Notification Authorization +- ``Spezi/Spezi/notificationSettings`` +- ``SwiftUICore/EnvironmentValues/notificationSettings`` +- ``Spezi/Spezi/requestNotificationAuthorization`` +- ``SwiftUICore/EnvironmentValues/requestNotificationAuthorization`` + +### Remote Notifications +- ``NotificationTokenHandler`` +- ``Spezi/Spezi/registerRemoteNotifications`` +- ``SwiftUICore/EnvironmentValues/registerRemoteNotifications`` +- ``Spezi/Spezi/unregisterRemoteNotifications`` +- ``SwiftUICore/EnvironmentValues/unregisterRemoteNotifications`` diff --git a/Sources/TemplatePackage/TemplatePackage.docc/TemplatePackage.md b/Sources/TemplatePackage/TemplatePackage.docc/TemplatePackage.md deleted file mode 100644 index 2417f3d..0000000 --- a/Sources/TemplatePackage/TemplatePackage.docc/TemplatePackage.md +++ /dev/null @@ -1,23 +0,0 @@ -# ``TemplatePackage`` - - - -The template repository contains a template Swift Package, including a continuous integration setup. - -## Overview - -Please follow the steps in the README.md file to customize the code to your needs. - -## Types - -### Template Package - -- ``TemplatePackage`` diff --git a/Sources/TemplatePackage/TemplatePackage.swift b/Sources/TemplatePackage/TemplatePackage.swift deleted file mode 100644 index fb06b53..0000000 --- a/Sources/TemplatePackage/TemplatePackage.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// This source file is part of the TemplatePackage open source project -// -// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -/// The main type of the Swift Package template. -public struct TemplatePackage { - /// The Swift Package template package is provided by Stanford University. - public var stanford: String { - "Stanford University" - } - - - /// The main type of the Swift Package template. - public init() {} -} diff --git a/Sources/XCTSpeziNotifications/XCTSpeziNotifications.docc/XCTSpeziNotifications.md b/Sources/XCTSpeziNotifications/XCTSpeziNotifications.docc/XCTSpeziNotifications.md new file mode 100644 index 0000000..61bf36f --- /dev/null +++ b/Sources/XCTSpeziNotifications/XCTSpeziNotifications.docc/XCTSpeziNotifications.md @@ -0,0 +1,23 @@ +# ``XCTSpeziNotifications`` + +XCTest extensions for testing notification-related actions. + + + +## Topics + +### Notification Authorization + +- ``XCTest/XCUIApplication/NotificationAuthorizationAction`` +- ``XCTest/XCUIApplication/confirmNotificationAuthorization(action:)`` + +### Notification Requests +- ``XCTest/XCUIApplication/assertNotificationDetails(identifier:title:subtitle:body:category:thread:sound:interruption:type:nextTrigger:nextTriggerExistenceTimeout:)`` diff --git a/Sources/XCTSpeziNotifications/XCUIApplication+AuthorizationAlert.swift b/Sources/XCTSpeziNotifications/XCUIApplication+AuthorizationAlert.swift new file mode 100644 index 0000000..44089c9 --- /dev/null +++ b/Sources/XCTSpeziNotifications/XCUIApplication+AuthorizationAlert.swift @@ -0,0 +1,44 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziNotifications +import UserNotifications +import XCTest + + +extension XCUIApplication { + /// Action of the notification authorization alert. + public enum NotificationAuthorizationAction: String { + case allow = "Allow" + case doNotAllow = "Don’t Allow" + } + + /// Confirm the notification authorization dialog. + /// - Parameter action: The action to confirm the alert with. + @available(tvOS, unavailable) + @available(macOS, unavailable) + @available(watchOS, unavailable) + public func confirmNotificationAuthorization(action: NotificationAuthorizationAction = .allow) { + let predicate = NSPredicate(format: "label CONTAINS 'Would Like to Send You Notifications'") + +#if os(iOS) + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + let alert = springboard.alerts.element(matching: predicate) + XCTAssert(alert.waitForExistence(timeout: 5.0)) + XCTAssert(alert.buttons[action.rawValue].exists) + alert.buttons[action.rawValue].tap() +#elseif os(visionOS) + let notifications = XCUIApplication(bundleIdentifier: "com.apple.RealityNotifications") + XCTAssert(notifications.scrollViews.staticTexts.element(matching: predicate).waitForExistence(timeout: 5.0)) + XCTAssert(notifications.buttons[action.rawValue].exists) + notifications.buttons[action.rawValue].tap() +#else + preconditionFailure("Unsupported platform") +#endif + } +} diff --git a/Sources/XCTSpeziNotifications/XCUIApplication+NotificationDetails.swift b/Sources/XCTSpeziNotifications/XCUIApplication+NotificationDetails.swift new file mode 100644 index 0000000..9567324 --- /dev/null +++ b/Sources/XCTSpeziNotifications/XCUIApplication+NotificationDetails.swift @@ -0,0 +1,73 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziNotifications +import UserNotifications +import XCTest + + +extension XCUIApplication { + /// Assert the contents of a pending notification visualized with the `NotificationRequestView`. + /// - Parameters: + /// - identifier: The identifier to assert. + /// - title: The title to assert. + /// - subtitle: The optional subtitle to assert. + /// - body: The body to assert. + /// - category: The optional category identifier to assert. + /// - thread: The optional thread identifier to assert. + /// - sound: Assert if there is sound played for the notification. + /// - interruption: The interruption level to assert. + /// - type: The trigger type to assert. + /// - nextTrigger: The next trigger label to assert. + /// - nextTriggerExistenceTimeout: The time to await for the trigger label to appear. + public func assertNotificationDetails( // swiftlint:disable:this function_default_parameter_at_end + identifier: String? = nil, + title: String, + subtitle: String? = nil, + body: String, + category: String? = nil, + thread: String? = nil, + sound: Bool = false, + interruption: UNNotificationInterruptionLevel = .active, + type: String? = nil, + nextTrigger: String? = nil, + nextTriggerExistenceTimeout: TimeInterval = 60 + ) { + XCTAssert(navigationBars.staticTexts[title].waitForExistence(timeout: 2.0)) + if let identifier { + XCTAssert(staticTexts["Identifier, \(identifier)"].exists) + } + XCTAssert(staticTexts["Title, \(title)"].exists) + if let subtitle { + XCTAssert(staticTexts["Subtitle, \(subtitle)"].exists) + } + XCTAssert(staticTexts["Body, \(body)"].exists) + if let category { + XCTAssert(staticTexts["Category, \(category)"].exists) + } + if let thread { + XCTAssert(staticTexts["Thread, \(thread)"].exists) + } + + XCTAssert(staticTexts["Sound, \(sound ? "Yes" : "No")"].exists) + XCTAssert(staticTexts["Interruption, \(interruption.description)"].exists) + +#if os(visionOS) + staticTexts["Interruption, \(interruption.description)"].swipeUp(velocity: .fast) +#endif + + if let type { + XCTAssert(staticTexts["Type, \(type)"].exists) + } + + + if let nextTrigger { + XCTAssert(staticTexts["Next Trigger, \(nextTrigger)"].waitForExistence(timeout: nextTriggerExistenceTimeout)) + } + } +} diff --git a/Sources/XCTSpeziNotificationsUI/NotificationRequestLabel.swift b/Sources/XCTSpeziNotificationsUI/NotificationRequestLabel.swift new file mode 100644 index 0000000..62458c8 --- /dev/null +++ b/Sources/XCTSpeziNotificationsUI/NotificationRequestLabel.swift @@ -0,0 +1,46 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziViews +import SwiftUI +import UserNotifications + + +@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +struct NotificationRequestLabel: View { + private let request: UNNotificationRequest + + @ManagedViewUpdate private var viewUpdate + + var body: some View { + NavigationLink { + NotificationRequestView(request) + } label: { + VStack(alignment: .leading) { +#if os(tvOS) + Text("Notification", bundle: .module) +#else + Text(request.content.title) + .bold() +#endif + if let trigger = request.trigger, + let nextDate = trigger.nextDate() { + NotificationTriggerLabel(nextDate) + .foregroundStyle(.secondary) + .onAppear { + viewUpdate.schedule(at: nextDate) + } + } + } + } + } + + init(_ request: UNNotificationRequest) { + self.request = request + } +} diff --git a/Sources/XCTSpeziNotificationsUI/NotificationRequestView.swift b/Sources/XCTSpeziNotificationsUI/NotificationRequestView.swift new file mode 100644 index 0000000..638ab3b --- /dev/null +++ b/Sources/XCTSpeziNotificationsUI/NotificationRequestView.swift @@ -0,0 +1,148 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziViews +import SwiftUI +import UserNotifications + + +/// Present the details of a notification request. +@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +public struct NotificationRequestView: View { + private let request: UNNotificationRequest + + @ManagedViewUpdate private var viewUpdate + + public var body: some View { + List { + Section { + LabeledContent { + Text(request.identifier) + } label: { + Text("Identifier", bundle: .module) + } + .accessibilityElement(children: .combine) + } + +#if !os(tvOS) + content +#endif + + delivery + + trigger + } +#if !os(tvOS) + .navigationTitle(request.content.title) +#if !os(macOS) + .navigationBarTitleDisplayMode(.inline) +#endif +#endif + } + + @available(tvOS, unavailable) + @ViewBuilder private var content: some View { + Section { // swiftlint:disable:this closure_body_length + LabeledContent { + Text(request.content.title) + } label: { + Text("Title", bundle: .module) + } + .accessibilityElement(children: .combine) + + if !request.content.subtitle.isEmpty { + LabeledContent { + Text(request.content.subtitle) + } label: { + Text("Subtitle", bundle: .module) + } + .accessibilityElement(children: .combine) + } + + LabeledContent { + Text(request.content.body) + } label: { + Text("Body", bundle: .module) + } + .accessibilityElement(children: .combine) + + if !request.content.categoryIdentifier.isEmpty { + LabeledContent { + Text(request.content.categoryIdentifier) + } label: { + Text("Category", bundle: .module) + } + .accessibilityElement(children: .combine) + } + + if !request.content.threadIdentifier.isEmpty { + LabeledContent { + Text(request.content.threadIdentifier) + } label: { + Text("Thread", bundle: .module) + } + .accessibilityElement(children: .combine) + } + } header: { + Text("Content", bundle: .module) + } + } + + @ViewBuilder private var delivery: some View { + Section { +#if !os(tvOS) + LabeledContent { + Text(request.content.sound != nil ? "Yes" : "No", bundle: .module) + } label: { + Text("Sound", bundle: .module) + } + .accessibilityElement(children: .combine) +#endif + + LabeledContent { + Text(request.content.interruptionLevel.description) + } label: { + Text("Interruption", bundle: .module) + } + .accessibilityElement(children: .combine) + } header: { + Text("Delivery", bundle: .module) + } + } + + @ViewBuilder private var trigger: some View { + if let trigger = request.trigger { + Section { + LabeledContent { + Text(trigger.type) + } label: { + Text("Type", bundle: .module) + } + .accessibilityElement(children: .combine) + + if let nextDate = trigger.nextDate() { + LabeledContent("Next Trigger") { + NotificationTriggerLabel(nextDate) + } + .accessibilityElement(children: .combine) + .onAppear { + viewUpdate.schedule(at: nextDate) + } + } + } header: { + Text("Trigger", bundle: .module) + } + } + } + + /// Create a new notification request details view. + /// - Parameter request: The notification request. + public init(_ request: UNNotificationRequest) { + self.request = request + } +} diff --git a/Sources/XCTSpeziNotificationsUI/NotificationTriggerLabel.swift b/Sources/XCTSpeziNotificationsUI/NotificationTriggerLabel.swift new file mode 100644 index 0000000..a53510e --- /dev/null +++ b/Sources/XCTSpeziNotificationsUI/NotificationTriggerLabel.swift @@ -0,0 +1,36 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Spezi +import SpeziViews +import SwiftUI + + +@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +struct NotificationTriggerLabel: View { + private let nextTriggerDate: Date + + @ManagedViewUpdate private var viewUpdate + + var body: some View { + Group { + if nextTriggerDate > .now { + Text("in \(Text(.currentDate, format: SystemFormatStyle.DateOffset(to: nextTriggerDate, sign: .never)))", bundle: .module) + } else { + Text("\(Text(.currentDate, format: SystemFormatStyle.DateOffset(to: nextTriggerDate, sign: .never))) ago", bundle: .module) + } + } + .onAppear { + viewUpdate.schedule(at: nextTriggerDate) + } + } + + init(_ nextTriggerDate: Date) { + self.nextTriggerDate = nextTriggerDate + } +} diff --git a/Sources/XCTSpeziNotificationsUI/NotificationsView.swift b/Sources/XCTSpeziNotificationsUI/NotificationsView.swift new file mode 100644 index 0000000..687db10 --- /dev/null +++ b/Sources/XCTSpeziNotificationsUI/NotificationsView.swift @@ -0,0 +1,68 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import OSLog +import SpeziNotifications +import SpeziViews +import SwiftUI + + +/// Fully integrated notifications view that shows the list of pending notifications. +@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +public struct NotificationsView: View { + private let logger = Logger(subsystem: "edu.stanford.spezi.SpeziNotifications", category: "NotificationsView") + private let authorizationAction: () -> Void + + @Environment(\.notificationSettings) + private var notificationSettings + @Environment(\.requestNotificationAuthorization) + private var requestNotificationAuthorization + + @State private var requestAuthorization = false + @State private var viewState: ViewState = .idle + + public var body: some View { + PendingNotificationsList() + .toolbar { + if requestAuthorization { + AsyncButton(state: $viewState) { + try await requestNotificationAuthorization(options: [.alert, .sound, .badge]) + await queryAuthorization() + authorizationAction() + } label: { + Label { + Text("Request Notification Authorization", bundle: .module) + } icon: { + Image(systemName: "alarm.waves.left.and.right.fill") + .accessibilityHidden(true) + } + } + } + } + .task { + await queryAuthorization() + } + } + + /// Create a new notifications view. + public init() { + self.init {} + } + + /// Create a new notification view a action that is called after requesting authorization. + /// - Parameter authorizationAction: The action that is executed once the user confirms the notification authorization. + public init(authorizationAction: @escaping () -> Void) { + self.authorizationAction = authorizationAction + } + + private func queryAuthorization() async { + let status = await notificationSettings().authorizationStatus + requestAuthorization = status != .authorized && status != .denied + logger.debug("Notification authorization is now \(status.description)") + } +} diff --git a/Sources/XCTSpeziNotificationsUI/PendingNotificationsList.swift b/Sources/XCTSpeziNotificationsUI/PendingNotificationsList.swift new file mode 100644 index 0000000..b04c947 --- /dev/null +++ b/Sources/XCTSpeziNotificationsUI/PendingNotificationsList.swift @@ -0,0 +1,83 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziNotifications +import SpeziViews +import SwiftUI +import UserNotifications + + +/// Present a list of pending notifications. +@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +public struct PendingNotificationsList: View { + @Environment(Notifications.self) + private var localNotifications + + @State private var viewState: ViewState = .idle + @State private var pendingNotifications: [UNNotificationRequest] = [] + + public var body: some View { + Group { + if viewState == .processing { + ProgressView() + } else if pendingNotifications.isEmpty { + ContentUnavailableView { + Label { + Text("No Notifications", bundle: .module) + } icon: { + Image(systemName: "mail") // swiftlint:disable:this accessibility_label_for_image + } + } description: { + Text("No pending notification requests.", bundle: .module) + } actions: { + refreshButton + .labelStyle(.titleOnly) + } + } else { + List { + ForEach(pendingNotifications, id: \.identifier) { request in + NotificationRequestLabel(request) + } + } + .toolbar { + refreshButton + } + } + } + .navigationTitle(Text("Pending Notifications", bundle: .module)) + .task { + await refreshList() + } + } + + @ViewBuilder private var refreshButton: some View { + AsyncButton(state: $viewState) { + await refreshList() + } label: { + Label { + Text("Refresh", bundle: .module) + } icon: { + Image(systemName: "arrow.clockwise") // swiftlint:disable:this accessibility_label_for_image + } + } + } + + /// Create a new list of pending notifications + public init() {} + + + private func refreshList() async { + viewState = .processing + defer { + viewState = .idle + } + + pendingNotifications.removeAll() + pendingNotifications = await localNotifications.pendingNotificationRequests() + } +} diff --git a/Sources/XCTSpeziNotificationsUI/Resources/Localizable.xcstrings b/Sources/XCTSpeziNotificationsUI/Resources/Localizable.xcstrings new file mode 100644 index 0000000..6905b26 --- /dev/null +++ b/Sources/XCTSpeziNotificationsUI/Resources/Localizable.xcstrings @@ -0,0 +1,286 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "%@ ago" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ ago" + } + } + } + }, + "Body" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Body" + } + } + } + }, + "Calendar" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calendar" + } + } + } + }, + "Category" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Category" + } + } + } + }, + "Content" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Content" + } + } + } + }, + "Delivery" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delivery" + } + } + } + }, + "Identifier" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Identifier" + } + } + } + }, + "in %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "in %@" + } + } + } + }, + "Interruption" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Interruption" + } + } + } + }, + "Interval" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Interval" + } + } + } + }, + "Location" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Location" + } + } + } + }, + "Next Trigger" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Next Trigger" + } + } + } + }, + "No" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No" + } + } + } + }, + "No Notifications" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Notifications" + } + } + } + }, + "No pending notification requests." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No pending notification requests." + } + } + } + }, + "Notification" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Notification" + } + } + } + }, + "Pending Notifications" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pending Notifications" + } + } + } + }, + "Push" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Push" + } + } + } + }, + "Refresh" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refresh" + } + } + } + }, + "Request Notification Authorization" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Request Notification Authorization" + } + } + } + }, + "Sound" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sound" + } + } + } + }, + "Subtitle" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Subtitle" + } + } + } + }, + "Thread" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thread" + } + } + } + }, + "Title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Title" + } + } + } + }, + "Trigger" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trigger" + } + } + } + }, + "Type" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Type" + } + } + } + }, + "Unknown" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unknown" + } + } + } + }, + "Yes" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yes" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Sources/XCTSpeziNotificationsUI/Resources/Localizable.xcstrings.license b/Sources/XCTSpeziNotificationsUI/Resources/Localizable.xcstrings.license new file mode 100644 index 0000000..a648e99 --- /dev/null +++ b/Sources/XCTSpeziNotificationsUI/Resources/Localizable.xcstrings.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/Sources/XCTSpeziNotificationsUI/UNNotificationTrigger+Extensions.swift b/Sources/XCTSpeziNotificationsUI/UNNotificationTrigger+Extensions.swift new file mode 100644 index 0000000..1af858e --- /dev/null +++ b/Sources/XCTSpeziNotificationsUI/UNNotificationTrigger+Extensions.swift @@ -0,0 +1,43 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI +import UserNotifications + + +extension UNNotificationTrigger { + var type: LocalizedStringResource { + if self is UNCalendarNotificationTrigger { + LocalizedStringResource("Calendar", bundle: .atURL(from: .module)) + } else if self is UNTimeIntervalNotificationTrigger { + LocalizedStringResource("Interval", bundle: .atURL(from: .module)) + } else if self is UNPushNotificationTrigger { + LocalizedStringResource("Push", bundle: .atURL(from: .module)) + } else { +#if !os(visionOS) && !os(macOS) && !os(tvOS) + if self is UNLocationNotificationTrigger { + LocalizedStringResource("Location", bundle: .atURL(from: .module)) + } else { + LocalizedStringResource("Unknown", bundle: .atURL(from: .module)) + } +#else + LocalizedStringResource("Unknown", bundle: .atURL(from: .module)) +#endif + } + } + + func nextDate() -> Date? { + if let calendarTrigger = self as? UNCalendarNotificationTrigger { + calendarTrigger.nextTriggerDate() + } else if let intervalTrigger = self as? UNTimeIntervalNotificationTrigger { + intervalTrigger.nextTriggerDate() + } else { + nil + } + } +} diff --git a/Sources/XCTSpeziNotificationsUI/XCTSpeziNotificationsUI.docc/XCTSpeziNotificationsUI.md b/Sources/XCTSpeziNotificationsUI/XCTSpeziNotificationsUI.docc/XCTSpeziNotificationsUI.md new file mode 100644 index 0000000..510e2a4 --- /dev/null +++ b/Sources/XCTSpeziNotificationsUI/XCTSpeziNotificationsUI.docc/XCTSpeziNotificationsUI.md @@ -0,0 +1,21 @@ +# ``XCTSpeziNotificationsUI`` + +Utilities and view components useful when testing notifications + + + +## Topics + +### Visualize Pending Notifications + +- ``NotificationsView`` +- ``PendingNotificationsList`` +- ``NotificationRequestView`` diff --git a/Tests/SpeziNotificationsTests/SpeziNotificationTests.swift b/Tests/SpeziNotificationsTests/SpeziNotificationTests.swift new file mode 100644 index 0000000..536864a --- /dev/null +++ b/Tests/SpeziNotificationsTests/SpeziNotificationTests.swift @@ -0,0 +1,262 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import protocol Spezi.Module +import class Spezi.SpeziAppDelegate +@testable import struct Spezi.Configuration +@testable import SpeziNotifications +import SwiftUI +import UserNotifications +import XCTest + + +private final class TestNotificationHandler: Module, NotificationHandler, NotificationTokenHandler { + @Application(\.registerRemoteNotifications) + var registerRemoteNotifications + @Application(\.unregisterRemoteNotifications) + var unregisterRemoteNotifications + + private let actionExpectation: XCTestExpectation + private let incomingNotificationExpectation: XCTestExpectation + private let remoteNotificationExpectation: XCTestExpectation +#if !os(macOS) + private var backgroundFetchResult: BackgroundFetchResult = .noData +#endif + + var lastDeviceToken: Data? + + init( + actionExpectation: XCTestExpectation = .init(), + incomingNotificationExpectation: XCTestExpectation = .init(), + remoteNotificationExpectation: XCTestExpectation = .init() + ) { + self.actionExpectation = actionExpectation + self.incomingNotificationExpectation = incomingNotificationExpectation + self.remoteNotificationExpectation = remoteNotificationExpectation + } + +#if !os(macOS) + func setFetchResult(_ fetchResult: BackgroundFetchResult) { + self.backgroundFetchResult = fetchResult + } +#endif + + + func receiveUpdatedDeviceToken(_ deviceToken: Data) { + lastDeviceToken = deviceToken + } + +#if !os(tvOS) + func handleNotificationAction(_ response: UNNotificationResponse) async { + actionExpectation.fulfill() + } +#endif + + func receiveIncomingNotification(_ notification: UNNotification) async -> UNNotificationPresentationOptions? { + incomingNotificationExpectation.fulfill() + return [.badge, .banner] + } + +#if !os(macOS) + func receiveRemoteNotification(_ remoteNotification: [AnyHashable: Any]) async -> BackgroundFetchResult { + remoteNotificationExpectation.fulfill() + return backgroundFetchResult + } +#else + func receiveRemoteNotification(_ remoteNotification: [AnyHashable: Any]) { + remoteNotificationExpectation.fulfill() + } +#endif +} + + +private final class EmptyNotificationHandler: Module, NotificationHandler {} + + +private class TestNotificationApplicationDelegate: SpeziAppDelegate { + private let injectedModule: TestNotificationHandler + + override var configuration: Configuration { + Configuration { + injectedModule + EmptyNotificationHandler() // ensure default implementations don't interfere with the tests + } + } + + init(_ injectedModule: TestNotificationHandler) { + self.injectedModule = injectedModule + } +} + + +final class NotificationsTests: XCTestCase { + @MainActor + func testRegisterNotificationsSuccessful() async throws { + let module = TestNotificationHandler() + let delegate = TestNotificationApplicationDelegate(module) + _ = delegate.spezi // init spezi + + let action = module.registerRemoteNotifications + + let expectation = XCTestExpectation(description: "RegisterRemoteNotifications") + var caught: Error? + + Task { // this task also runs on main actor + do { + try await action() + } catch { + caught = error + } + expectation.fulfill() + } + + try await Task.sleep(for: .milliseconds(500)) // allow dispatch of Task above + + let data = try XCTUnwrap("Hello World".data(using: .utf8)) + +#if os(iOS) || os(visionOS) || os(tvOS) + delegate.application(UIApplication.shared, didRegisterForRemoteNotificationsWithDeviceToken: data) +#elseif os(watchOS) + delegate.application(WKApplication.shared(), didRegisterForRemoteNotificationsWithDeviceToken: data) +#elseif os(macOS) + delegate.application(NSApplication.shared, didRegisterForRemoteNotificationsWithDeviceToken: data) +#endif + + try await Task.sleep(for: .milliseconds(500)) // allow dispatch of Task above + + await fulfillment(of: [expectation]) + XCTAssertNil(caught) + XCTAssertEqual(module.lastDeviceToken, data) + } + + @MainActor + func testRegisterNotificationsErroneous() async throws { + enum TestError: Error, Equatable { + case testError + } + + let module = TestNotificationHandler() + let delegate = TestNotificationApplicationDelegate(module) + _ = delegate.spezi // init spezi + + let action = module.registerRemoteNotifications + + let expectation = XCTestExpectation(description: "RegisterRemoteNotifications") + var caught: Error? + + Task { // this task also runs on main actor + do { + try await action() + } catch { + caught = error + } + expectation.fulfill() + } + + try await Task.sleep(for: .milliseconds(500)) // allow dispatch of Task above + +#if os(iOS) || os(visionOS) || os(tvOS) + delegate.application(UIApplication.shared, didFailToRegisterForRemoteNotificationsWithError: TestError.testError) +#elseif os(watchOS) + delegate.application(WKApplication.shared(), didFailToRegisterForRemoteNotificationsWithError: TestError.testError) +#elseif os(macOS) + delegate.application(NSApplication.shared, didFailToRegisterForRemoteNotificationsWithError: TestError.testError) +#endif + + try await Task.sleep(for: .milliseconds(500)) // allow dispatch of Task above + + await fulfillment(of: [expectation]) + XCTAssertNil(module.lastDeviceToken) + let receivedError = try XCTUnwrap(caught as? TestError) + XCTAssertEqual(receivedError, TestError.testError) + } + + @MainActor + func testUnregisterNotifications() async throws { + let module = TestNotificationHandler() + let delegate = TestNotificationApplicationDelegate(module) + _ = delegate.spezi // init spezi + + let action = module.unregisterRemoteNotifications + action() + } + + @MainActor + func testRemoteNotificationDeliveryNoData() async throws { + let expectation = XCTestExpectation(description: "RemoteNotification") + + let module = TestNotificationHandler(remoteNotificationExpectation: expectation) + let delegate = TestNotificationApplicationDelegate(module) + _ = delegate.spezi + +#if os(iOS) || os(visionOS) || os(tvOS) + let result = await delegate.application(UIApplication.shared, didReceiveRemoteNotification: [:]) +#elseif os(watchOS) + let result = await delegate.didReceiveRemoteNotification([:]) +#elseif os(macOS) + delegate.application(NSApplication.shared, didReceiveRemoteNotification: [:]) +#endif + + await fulfillment(of: [expectation]) +#if !os(macOS) + XCTAssertEqual(result, .noData) +#endif + } + + @MainActor + func testRemoteNotificationDeliveryNewData() async throws { + let expectation = XCTestExpectation(description: "RemoteNotification") + + let module = TestNotificationHandler(remoteNotificationExpectation: expectation) +#if !os(macOS) + module.setFetchResult(.newData) +#endif + + let delegate = TestNotificationApplicationDelegate(module) + _ = delegate.spezi + +#if os(iOS) || os(visionOS) || os(tvOS) + let result = await delegate.application(UIApplication.shared, didReceiveRemoteNotification: [:]) +#elseif os(watchOS) + let result = await delegate.didReceiveRemoteNotification([:]) +#elseif os(macOS) + delegate.application(NSApplication.shared, didReceiveRemoteNotification: [:]) +#endif + + await fulfillment(of: [expectation]) +#if !os(macOS) + XCTAssertEqual(result, .newData) +#endif + } + + @MainActor + func testRemoteNotificationDeliveryFailed() async throws { + let expectation = XCTestExpectation(description: "RemoteNotification") + + let module = TestNotificationHandler(remoteNotificationExpectation: expectation) +#if !os(macOS) + module.setFetchResult(.failed) +#endif + + let delegate = TestNotificationApplicationDelegate(module) + _ = delegate.spezi + +#if os(iOS) || os(visionOS) || os(tvOS) + let result = await delegate.application(UIApplication.shared, didReceiveRemoteNotification: [:]) +#elseif os(watchOS) + let result = await delegate.didReceiveRemoteNotification([:]) +#elseif os(macOS) + delegate.application(NSApplication.shared, didReceiveRemoteNotification: [:]) +#endif + + await fulfillment(of: [expectation]) +#if !os(macOS) + XCTAssertEqual(result, .failed) +#endif + } +} diff --git a/Tests/TemplatePackageTests/TemplatePackageTests.swift b/Tests/TemplatePackageTests/TemplatePackageTests.swift deleted file mode 100644 index bc52b5f..0000000 --- a/Tests/TemplatePackageTests/TemplatePackageTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// This source file is part of the TemplatePackage open source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -@testable import TemplatePackage -import XCTest - - -final class TemplatePackageTests: XCTestCase { - func testTemplatePackage() throws { - let templatePackage = TemplatePackage() - XCTAssertEqual(templatePackage.stanford, "Stanford University") - } -} diff --git a/Tests/UITests/TestApp.xctestplan b/Tests/UITests/TestApp.xctestplan index 54e441a..6dbb867 100644 --- a/Tests/UITests/TestApp.xctestplan +++ b/Tests/UITests/TestApp.xctestplan @@ -13,8 +13,8 @@ "targets" : [ { "containerPath" : "container:..\/..", - "identifier" : "TemplatePackage", - "name" : "TemplatePackage" + "identifier" : "SpeziNotifications", + "name" : "SpeziNotifications" } ] }, diff --git a/Tests/UITests/TestApp.xctestplan.license b/Tests/UITests/TestApp.xctestplan.license index d77e33d..1f1a4cd 100644 --- a/Tests/UITests/TestApp.xctestplan.license +++ b/Tests/UITests/TestApp.xctestplan.license @@ -1,5 +1,5 @@ -This source file is part of the TemplatePackage open-source project +This source file is part of the SpeziNotifications open-source project -SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) -SPDX-License-Identifier: MIT \ No newline at end of file +SPDX-License-Identifier: MIT diff --git a/Tests/UITests/TestApp/Assets.xcassets/AccentColor.colorset/Contents.json.license b/Tests/UITests/TestApp/Assets.xcassets/AccentColor.colorset/Contents.json.license index d77e33d..f2f23af 100644 --- a/Tests/UITests/TestApp/Assets.xcassets/AccentColor.colorset/Contents.json.license +++ b/Tests/UITests/TestApp/Assets.xcassets/AccentColor.colorset/Contents.json.license @@ -1,4 +1,4 @@ -This source file is part of the TemplatePackage open-source project +This source file is part of the SpeziNotifications open-source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/TestApp/Assets.xcassets/AppIcon.appiconset/Contents.json.license b/Tests/UITests/TestApp/Assets.xcassets/AppIcon.appiconset/Contents.json.license index d77e33d..f2f23af 100644 --- a/Tests/UITests/TestApp/Assets.xcassets/AppIcon.appiconset/Contents.json.license +++ b/Tests/UITests/TestApp/Assets.xcassets/AppIcon.appiconset/Contents.json.license @@ -1,4 +1,4 @@ -This source file is part of the TemplatePackage open-source project +This source file is part of the SpeziNotifications open-source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/TestApp/Assets.xcassets/Contents.json.license b/Tests/UITests/TestApp/Assets.xcassets/Contents.json.license index d77e33d..f2f23af 100644 --- a/Tests/UITests/TestApp/Assets.xcassets/Contents.json.license +++ b/Tests/UITests/TestApp/Assets.xcassets/Contents.json.license @@ -1,4 +1,4 @@ -This source file is part of the TemplatePackage open-source project +This source file is part of the SpeziNotifications open-source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/TestApp/ControlsView.swift b/Tests/UITests/TestApp/ControlsView.swift new file mode 100644 index 0000000..4e161e7 --- /dev/null +++ b/Tests/UITests/TestApp/ControlsView.swift @@ -0,0 +1,166 @@ +// +// This source file is part of the SpeziNotifications open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziNotifications +import SpeziViews +import SwiftUI +import UserNotifications + + +struct ControlsView: View { + @Environment(\.notificationSettings) + private var notificationSettings + @Environment(\.requestNotificationAuthorization) + private var requestNotificationAuthorization + + @Environment(\.registerRemoteNotifications) + private var registerRemoteNotifications + @Environment(\.unregisterRemoteNotifications) + private var unregisterRemoteNotifications + + @Environment(Notifications.self) + private var notifications + + @State private var token: Data? + @State private var tokenError: Error? + @State private var authorizationStatus: UNAuthorizationStatus? + @State private var viewState: ViewState = .idle + + @State private var task: Task? { + willSet { + task?.cancel() + } + } + + + var body: some View { + List { // swiftlint:disable:this closure_body_length + Section { + if let authorizationStatus { + LabeledContent("Authorization", value: authorizationStatus.description) + } + LabeledContent("Token") { + if let token { + Text(token.description) + .foregroundStyle(.green) + } else if let error = tokenError as? LocalizedError, + let description = error.errorDescription ?? error.failureReason { + Text(verbatim: description) + .foregroundStyle(.red) + } else if tokenError != nil { + Text(verbatim: "failed") + .foregroundStyle(.red) + } else { + Text(verbatim: "none") + .foregroundStyle(.secondary) + } + } + .accessibilityElement(children: .combine) + .accessibilityIdentifier("token-field") + } + + Section("Actions") { + Button("Register") { + task = Task { + do { + token = try await registerRemoteNotifications() + } catch { + print("Registration failed with \(error)") + self.tokenError = error + } + } + } + Button("Unregister") { + unregisterRemoteNotifications() + token = nil + tokenError = nil + } + if authorizationStatus != .authorized { + AsyncButton("Request Authorization", state: $viewState) { + do { + try await requestNotificationAuthorization(options: [.alert, .badge, .sound]) + authorizationStatus = await notificationSettings().authorizationStatus + } catch { + authorizationStatus = await notificationSettings().authorizationStatus + throw error + } + } + } + if authorizationStatus != .denied { + AsyncButton("Schedule Notifications", state: $viewState) { + try await scheduleNotifications() + } + } + } + } + .navigationTitle("Notifications") + .viewStateAlert(state: $viewState) + .task { + authorizationStatus = await notificationSettings().authorizationStatus + } + .onDisappear { + task?.cancel() + } + } + + private func scheduleNotifications() async throws { + let settings = await notificationSettings() + if settings.authorizationStatus == .notDetermined { + try await requestNotificationAuthorization(options: [.alert, .badge, .sound, .provisional]) + authorizationStatus = await notificationSettings().authorizationStatus + } + + try await notifications.add(request: .calendarRequest) + try await notifications.add(request: .intervalRequest) + } +} + + +extension UNNotificationContent { + static func content(type: String, interruption: UNNotificationInterruptionLevel = .active) -> UNNotificationContent { + let content = UNMutableNotificationContent() +#if !os(tvOS) + content.title = "\(type) Notification" + content.subtitle = "Test Notification" + content.body = "This is a \(type.lowercased()) notification" + + content.categoryIdentifier = "\(type.lowercased())-test-notification" + content.threadIdentifier = "SpeziNotifications" + content.interruptionLevel = interruption + content.sound = .default +#endif + + return content + } +} + + +extension UNNotificationTrigger { + static var calendarTrigger: UNNotificationTrigger { + UNCalendarNotificationTrigger(dateMatching: DateComponents(hour: 8, minute: 0, second: 0), repeats: true) + } + + static var intervalTrigger: UNNotificationTrigger { + UNTimeIntervalNotificationTrigger(timeInterval: 3600, repeats: false) + } +} + + +extension UNNotificationRequest { + static var calendarRequest: UNNotificationRequest { + UNNotificationRequest( + identifier: "calendar-request", + content: .content(type: "Calendar", interruption: .timeSensitive), + trigger: .calendarTrigger + ) + } + + static var intervalRequest: UNNotificationRequest { + UNNotificationRequest(identifier: "interval-request", content: .content(type: "Interval", interruption: .critical), trigger: .intervalTrigger) + } +} diff --git a/Tests/UITests/TestApp/OperatingSystem.swift b/Tests/UITests/TestApp/OperatingSystem.swift deleted file mode 100644 index 201ed6e..0000000 --- a/Tests/UITests/TestApp/OperatingSystem.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// This source file is part of the TemplatePackage open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -#if os(macOS) -let operatingSystem = "macOS" -#elseif os(iOS) -let operatingSystem = "iOS" -#elseif os(watchOS) -let operatingSystem = "watchOS" -#elseif os(visionOS) -let operatingSystem = "visionOS" -#elseif os(tvOS) -let operatingSystem = "tvOS" -#endif diff --git a/Tests/UITests/TestApp/TestApp.entitlements b/Tests/UITests/TestApp/TestApp.entitlements new file mode 100644 index 0000000..a28da91 --- /dev/null +++ b/Tests/UITests/TestApp/TestApp.entitlements @@ -0,0 +1,10 @@ + + + + + aps-environment + development + com.apple.developer.aps-environment + development + + diff --git a/Tests/UITests/TestApp/TestApp.entitlements.license b/Tests/UITests/TestApp/TestApp.entitlements.license new file mode 100644 index 0000000..1f1a4cd --- /dev/null +++ b/Tests/UITests/TestApp/TestApp.entitlements.license @@ -0,0 +1,5 @@ +This source file is part of the SpeziNotifications open-source project + +SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index 4ddb27e..0eb07fd 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -1,21 +1,38 @@ // -// This source file is part of the TemplatePackage open-source project +// This source file is part of the SpeziNotifications open-source project // -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) // // SPDX-License-Identifier: MIT // +import Spezi +import SpeziNotifications import SwiftUI -import TemplatePackage +import XCTSpeziNotificationsUI @main struct UITestsApp: App { + @UIApplicationDelegateAdaptor(TestAppDelegate.self) + private var appDelegate + var body: some Scene { WindowGroup { - Text(TemplatePackage().stanford) - Text(operatingSystem) + TabView { + Tab("Controls", systemImage: "switch.2") { + NavigationStack { + ControlsView() + } + } + + Tab("Notifications", systemImage: "mail") { + NavigationStack { + NotificationsView() + } + } + } + .spezi(appDelegate) } } } diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift new file mode 100644 index 0000000..7278743 --- /dev/null +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -0,0 +1,19 @@ +// +// This source file is part of the SpeziNotifications open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Spezi +import SpeziNotifications + + +final class TestAppDelegate: SpeziAppDelegate { + override var configuration: Configuration { + Configuration { + Notifications() + } + } +} diff --git a/Tests/UITests/TestAppUITests/NotificationAuthorizationTests.swift b/Tests/UITests/TestAppUITests/NotificationAuthorizationTests.swift new file mode 100644 index 0000000..6460182 --- /dev/null +++ b/Tests/UITests/TestAppUITests/NotificationAuthorizationTests.swift @@ -0,0 +1,53 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import XCTest +import XCTestExtensions +import XCTSpeziNotifications + + +final class NotificationAuthorizationTests: XCTestCase { + @MainActor + override func setUp() async throws { + continueAfterFailure = false + } + + @MainActor + func testNotificationAuthorizationAllow() { + let app = XCUIApplication() + app.deleteAndLaunch(withSpringboardAppName: "TestApp") + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + XCTAssert(app.navigationBars.staticTexts["Notifications"].waitForExistence(timeout: 2.0)) + + XCTAssert(app.staticTexts["Authorization, notDetermined"].exists) + XCTAssert(app.buttons["Request Authorization"].exists) + app.buttons["Request Authorization"].tap() + + app.confirmNotificationAuthorization() + + XCTAssert(app.staticTexts["Authorization, authorized"].waitForExistence(timeout: 0.5)) + } + + @MainActor + func testNotificationAuthorizationNotAllow() { + let app = XCUIApplication() + app.deleteAndLaunch(withSpringboardAppName: "TestApp") + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + XCTAssert(app.navigationBars.staticTexts["Notifications"].waitForExistence(timeout: 2.0)) + + XCTAssert(app.staticTexts["Authorization, notDetermined"].exists) + XCTAssert(app.buttons["Request Authorization"].exists) + app.buttons["Request Authorization"].tap() + + app.confirmNotificationAuthorization(action: .doNotAllow) + + XCTAssert(app.staticTexts["Authorization, denied"].waitForExistence(timeout: 0.5)) + } +} diff --git a/Tests/UITests/TestAppUITests/PendingNotificationsTests.swift b/Tests/UITests/TestAppUITests/PendingNotificationsTests.swift new file mode 100644 index 0000000..4205cfd --- /dev/null +++ b/Tests/UITests/TestAppUITests/PendingNotificationsTests.swift @@ -0,0 +1,78 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import XCTest +import XCTestExtensions +import XCTSpeziNotifications + + +final class PendingNotificationsTests: XCTestCase { + @MainActor + override func setUp() async throws { + continueAfterFailure = false + } + + @MainActor + func testPendingNotifications() { + let app = XCUIApplication() + app.deleteAndLaunch(withSpringboardAppName: "TestApp") + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + XCTAssert(app.navigationBars.staticTexts["Notifications"].waitForExistence(timeout: 2.0)) + + XCTAssert(app.staticTexts["Authorization, notDetermined"].exists) + XCTAssert(app.buttons["Schedule Notifications"].exists) + app.buttons["Schedule Notifications"].tap() + + XCTAssert(app.staticTexts["Authorization, provisional"].waitForExistence(timeout: 0.5)) + + #if os(visionOS) + XCTAssert(app.buttons["Notifications"].exists) + app.buttons["Notifications"].firstMatch.tap() + #else + XCTAssert(app.tabBars.buttons["Notifications"].exists) + app.tabBars.buttons["Notifications"].tap() + #endif + + XCTAssert(app.navigationBars.staticTexts["Pending Notifications"].waitForExistence(timeout: 2.0)) + + XCTAssert(app.staticTexts["Calendar Notification"].exists) + XCTAssert(app.staticTexts["Interval Notification"].exists) + + app.staticTexts["Calendar Notification"].tap() + XCTAssert(app.navigationBars.staticTexts["Calendar Notification"].waitForExistence(timeout: 2.0)) + app.assertNotificationDetails( + identifier: "calendar-request", + title: "Calendar Notification", + subtitle: "Test Notification", + body: "This is a calendar notification", + category: "calendar-test-notification", + thread: "SpeziNotifications", + sound: true, + interruption: .timeSensitive, + type: "Calendar" + ) + XCTAssert(app.navigationBars.buttons["Pending Notifications"].exists) + app.navigationBars.buttons["Pending Notifications"].tap() + + XCTAssert(app.staticTexts["Interval Notification"].waitForExistence(timeout: 2.0)) + app.staticTexts["Interval Notification"].tap() + XCTAssert(app.navigationBars.staticTexts["Interval Notification"].waitForExistence(timeout: 2.0)) + app.assertNotificationDetails( + identifier: "interval-request", + title: "Interval Notification", + subtitle: "Test Notification", + body: "This is a interval notification", + category: "interval-test-notification", + thread: "SpeziNotifications", + sound: true, + interruption: .critical, + type: "Interval" + ) + } +} diff --git a/Tests/UITests/TestAppUITests/RemoteNotificationsTests.swift b/Tests/UITests/TestAppUITests/RemoteNotificationsTests.swift new file mode 100644 index 0000000..7fa7c98 --- /dev/null +++ b/Tests/UITests/TestAppUITests/RemoteNotificationsTests.swift @@ -0,0 +1,44 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import XCTest + + +final class RemoteNotificationsTests: XCTestCase { + @MainActor + override func setUp() async throws { + continueAfterFailure = false + } + + @MainActor + func testRegistrationOnSimulator() throws { + let app = XCUIApplication() + app.launch() + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + + XCTAssertTrue(app.navigationBars.staticTexts["Notifications"].waitForExistence(timeout: 2.0)) + XCTAssertTrue(app.staticTexts["Token, none"].exists) + XCTAssertTrue(app.buttons["Register"].exists) + XCTAssertTrue(app.buttons["Unregister"].exists) + + app.buttons["Register"].tap() + + if !(app.staticTexts["Token, 80 bytes"].waitForExistence(timeout: 1.0) + || app.staticTexts["Token, 60 bytes"].exists) { + XCTAssertFalse(app.staticTexts["Token, failed"].exists) + XCTAssertTrue(app.staticTexts["Token, Timeout"].waitForExistence(timeout: 10)) + } + + // the unit test accepts both success and failure states. Therefore, print the content of the field to have it visible in the logs + print("Read token field as: \(app.staticTexts.matching(identifier: "token-field").firstMatch.debugDescription)") + + app.buttons["Unregister"].tap() + XCTAssertTrue(app.staticTexts["Token, none"].waitForExistence(timeout: 1.0)) + } +} diff --git a/Tests/UITests/TestAppUITests/TestAppUITests.swift b/Tests/UITests/TestAppUITests/TestAppUITests.swift deleted file mode 100644 index 82a97bb..0000000 --- a/Tests/UITests/TestAppUITests/TestAppUITests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// This source file is part of the TemplatePackage open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import OSLog -import XCTest - - -class TestAppUITests: XCTestCase { - override func setUpWithError() throws { - try super.setUpWithError() - - continueAfterFailure = false - } - - - func testTemplatePackage() throws { - let app = XCUIApplication() - app.launch() - - XCTAssert(app.staticTexts["Stanford University"].waitForExistence(timeout: 0.1)) - XCTAssert(app.staticTexts[operatingSystem].exists) - } -} diff --git a/Tests/UITests/TestAppWatchApp.xctestplan b/Tests/UITests/TestAppWatchApp.xctestplan deleted file mode 100644 index 6f6f2dd..0000000 --- a/Tests/UITests/TestAppWatchApp.xctestplan +++ /dev/null @@ -1,37 +0,0 @@ -{ - "configurations" : [ - { - "id" : "B8537494-39D3-45EC-98D4-B3C417844ADD", - "name" : "Default", - "options" : { - - } - } - ], - "defaultOptions" : { - "codeCoverage" : { - "targets" : [ - { - "containerPath" : "container:..\/..", - "identifier" : "TemplatePackage", - "name" : "TemplatePackage" - } - ] - }, - "targetForVariableExpansion" : { - "containerPath" : "container:UITests.xcodeproj", - "identifier" : "2F9CBEA52A76C40E009818FF", - "name" : "TestAppWatchApp" - } - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:UITests.xcodeproj", - "identifier" : "2F9CBEBE2A76C412009818FF", - "name" : "TestAppWatchAppUITests" - } - } - ], - "version" : 1 -} diff --git a/Tests/UITests/TestAppWatchApp.xctestplan.license b/Tests/UITests/TestAppWatchApp.xctestplan.license deleted file mode 100644 index d77e33d..0000000 --- a/Tests/UITests/TestAppWatchApp.xctestplan.license +++ /dev/null @@ -1,5 +0,0 @@ -This source file is part of the TemplatePackage open-source project - -SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) - -SPDX-License-Identifier: MIT \ No newline at end of file diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index e880b01..29eb495 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -3,23 +3,21 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ - 2F68C3C8292EA52000B3E12C /* TemplatePackage in Frameworks */ = {isa = PBXBuildFile; productRef = 2F68C3C7292EA52000B3E12C /* TemplatePackage */; }; + 2F68C3C8292EA52000B3E12C /* SpeziNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = 2F68C3C7292EA52000B3E12C /* SpeziNotifications */; }; 2F6D139A28F5F386007C25D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F6D139928F5F386007C25D6 /* Assets.xcassets */; }; - 2F8A431329130A8C005D2B8F /* TestAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8A431229130A8C005D2B8F /* TestAppUITests.swift */; }; - 2F9CBEC92A76C412009818FF /* TestApp Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 2F9CBEA62A76C40E009818FF /* TestApp Watch App.app */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 2F9CBED72A76C752009818FF /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; }; - 2F9CBED82A76C75E009818FF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F6D139928F5F386007C25D6 /* Assets.xcassets */; }; - 2F9CBEDA2A76C795009818FF /* TemplatePackage in Frameworks */ = {isa = PBXBuildFile; productRef = 2F9CBED92A76C795009818FF /* TemplatePackage */; }; - 2F9CBEDB2A76C7EC009818FF /* TestAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8A431229130A8C005D2B8F /* TestAppUITests.swift */; }; + 2F8A431329130A8C005D2B8F /* RemoteNotificationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8A431229130A8C005D2B8F /* RemoteNotificationsTests.swift */; }; 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; }; - 2FB5B6E02C2F6C50009162E6 /* OperatingSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB5B6DF2C2F6C50009162E6 /* OperatingSystem.swift */; }; - 2FB5B6E12C2F6DBC009162E6 /* OperatingSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB5B6DF2C2F6C50009162E6 /* OperatingSystem.swift */; }; - 2FB5B6E22C2F707A009162E6 /* OperatingSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB5B6DF2C2F6C50009162E6 /* OperatingSystem.swift */; }; - 2FB5B6E32C2F707B009162E6 /* OperatingSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB5B6DF2C2F6C50009162E6 /* OperatingSystem.swift */; }; + A902FE552CAAB75800C80383 /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A902FE542CAAB75800C80383 /* TestAppDelegate.swift */; }; + A902FE572CAAC04300C80383 /* PendingNotificationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A902FE562CAAC03800C80383 /* PendingNotificationsTests.swift */; }; + A95C45EB2CAAB34600EBB08D /* XCTSpeziNotificationsUI in Frameworks */ = {isa = PBXBuildFile; productRef = A95C45EA2CAAB34600EBB08D /* XCTSpeziNotificationsUI */; }; + A9E0315E2CAAA8F600E13BD9 /* ControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0315D2CAAA8F400E13BD9 /* ControlsView.swift */; }; + A9E031602CAAAA9F00E13BD9 /* NotificationAuthorizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0315F2CAAAA8B00E13BD9 /* NotificationAuthorizationTests.swift */; }; + A9E031662CAAACF900E13BD9 /* XCTestExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = A9E031652CAAACF900E13BD9 /* XCTestExtensions */; }; + A9E031682CAAAF6800E13BD9 /* XCTSpeziNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = A9E031672CAAAF6800E13BD9 /* XCTSpeziNotifications */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -30,27 +28,6 @@ remoteGlobalIDString = 2F6D139128F5F384007C25D6; remoteInfo = Example; }; - 2F9CBEC02A76C412009818FF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 2F6D138A28F5F384007C25D6 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 2F9CBEA52A76C40E009818FF; - remoteInfo = "TestAppWatchOS Watch App"; - }; - 2F9CBEC72A76C412009818FF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 2F6D138A28F5F384007C25D6 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 2F9CBEA52A76C40E009818FF; - remoteInfo = "TestAppWatchOS Watch App"; - }; - 2FF8922F2A770DE200903A5A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 2F6D138A28F5F384007C25D6 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 2F6D139128F5F384007C25D6; - remoteInfo = TestApp; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -60,7 +37,6 @@ dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; dstSubfolderSpec = 16; files = ( - 2F9CBEC92A76C412009818FF /* TestApp Watch App.app in Embed Watch Content */, ); name = "Embed Watch Content"; runOnlyForDeploymentPostprocessing = 0; @@ -68,17 +44,18 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 2F68C3C6292E9F8F00B3E12C /* TemplatePackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TemplatePackage; path = ../..; sourceTree = ""; }; + 2F68C3C6292E9F8F00B3E12C /* SpeziNotifications */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SpeziNotifications; path = ../..; sourceTree = ""; }; 2F6D139228F5F384007C25D6 /* TestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2F6D139928F5F386007C25D6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 2F8A431229130A8C005D2B8F /* TestAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppUITests.swift; sourceTree = ""; }; - 2F9CBEA62A76C40E009818FF /* TestApp Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TestApp Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 2F9CBEBF2A76C412009818FF /* TestAppWatchAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestAppWatchAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2F8A431229130A8C005D2B8F /* RemoteNotificationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteNotificationsTests.swift; sourceTree = ""; }; 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestApp.swift; sourceTree = ""; }; 2FB0758A299DDB9000C0B37F /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestApp.xctestplan; sourceTree = ""; }; - 2FB5B6DF2C2F6C50009162E6 /* OperatingSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatingSystem.swift; sourceTree = ""; }; - 2FF8922E2A770D4200903A5A /* TestAppWatchApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestAppWatchApp.xctestplan; sourceTree = ""; }; + A902FE542CAAB75800C80383 /* TestAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppDelegate.swift; sourceTree = ""; }; + A902FE562CAAC03800C80383 /* PendingNotificationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingNotificationsTests.swift; sourceTree = ""; }; + A902FE582CAAC63900C80383 /* TestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TestApp.entitlements; sourceTree = ""; }; + A9E0315D2CAAA8F400E13BD9 /* ControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsView.swift; sourceTree = ""; }; + A9E0315F2CAAAA8B00E13BD9 /* NotificationAuthorizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAuthorizationTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -86,7 +63,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2F68C3C8292EA52000B3E12C /* TemplatePackage in Frameworks */, + 2F68C3C8292EA52000B3E12C /* SpeziNotifications in Frameworks */, + A95C45EB2CAAB34600EBB08D /* XCTSpeziNotificationsUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,21 +72,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 2F9CBEA32A76C40E009818FF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 2F9CBEDA2A76C795009818FF /* TemplatePackage in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 2F9CBEBC2A76C412009818FF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( + A9E031662CAAACF900E13BD9 /* XCTestExtensions in Frameworks */, + A9E031682CAAAF6800E13BD9 /* XCTSpeziNotifications in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -119,8 +84,7 @@ isa = PBXGroup; children = ( 2FB0758A299DDB9000C0B37F /* TestApp.xctestplan */, - 2FF8922E2A770D4200903A5A /* TestAppWatchApp.xctestplan */, - 2F68C3C6292E9F8F00B3E12C /* TemplatePackage */, + 2F68C3C6292E9F8F00B3E12C /* SpeziNotifications */, 2F6D139428F5F384007C25D6 /* TestApp */, 2F6D13AF28F5F386007C25D6 /* TestAppUITests */, 2F6D139328F5F384007C25D6 /* Products */, @@ -133,8 +97,6 @@ children = ( 2F6D139228F5F384007C25D6 /* TestApp.app */, 2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */, - 2F9CBEA62A76C40E009818FF /* TestApp Watch App.app */, - 2F9CBEBF2A76C412009818FF /* TestAppWatchAppUITests.xctest */, ); name = Products; sourceTree = ""; @@ -142,8 +104,10 @@ 2F6D139428F5F384007C25D6 /* TestApp */ = { isa = PBXGroup; children = ( + A902FE582CAAC63900C80383 /* TestApp.entitlements */, + A9E0315D2CAAA8F400E13BD9 /* ControlsView.swift */, 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, - 2FB5B6DF2C2F6C50009162E6 /* OperatingSystem.swift */, + A902FE542CAAB75800C80383 /* TestAppDelegate.swift */, 2F6D139928F5F386007C25D6 /* Assets.xcassets */, ); path = TestApp; @@ -152,7 +116,9 @@ 2F6D13AF28F5F386007C25D6 /* TestAppUITests */ = { isa = PBXGroup; children = ( - 2F8A431229130A8C005D2B8F /* TestAppUITests.swift */, + A902FE562CAAC03800C80383 /* PendingNotificationsTests.swift */, + A9E0315F2CAAAA8B00E13BD9 /* NotificationAuthorizationTests.swift */, + 2F8A431229130A8C005D2B8F /* RemoteNotificationsTests.swift */, ); path = TestAppUITests; sourceTree = ""; @@ -179,11 +145,11 @@ buildRules = ( ); dependencies = ( - 2F9CBEC82A76C412009818FF /* PBXTargetDependency */, ); name = TestApp; packageProductDependencies = ( - 2F68C3C7292EA52000B3E12C /* TemplatePackage */, + 2F68C3C7292EA52000B3E12C /* SpeziNotifications */, + A95C45EA2CAAB34600EBB08D /* XCTSpeziNotificationsUI */, ); productName = Example; productReference = 2F6D139228F5F384007C25D6 /* TestApp.app */; @@ -207,45 +173,6 @@ productReference = 2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; - 2F9CBEA52A76C40E009818FF /* TestAppWatchApp */ = { - isa = PBXNativeTarget; - buildConfigurationList = 2F9CBECA2A76C412009818FF /* Build configuration list for PBXNativeTarget "TestAppWatchApp" */; - buildPhases = ( - 2F9CBEA22A76C40E009818FF /* Sources */, - 2F9CBEA32A76C40E009818FF /* Frameworks */, - 2F9CBEA42A76C40E009818FF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = TestAppWatchApp; - packageProductDependencies = ( - 2F9CBED92A76C795009818FF /* TemplatePackage */, - ); - productName = "TestAppWatchOS Watch App"; - productReference = 2F9CBEA62A76C40E009818FF /* TestApp Watch App.app */; - productType = "com.apple.product-type.application"; - }; - 2F9CBEBE2A76C412009818FF /* TestAppWatchAppUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 2F9CBED32A76C412009818FF /* Build configuration list for PBXNativeTarget "TestAppWatchAppUITests" */; - buildPhases = ( - 2F9CBEBB2A76C412009818FF /* Sources */, - 2F9CBEBC2A76C412009818FF /* Frameworks */, - 2F9CBEBD2A76C412009818FF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 2F9CBEC12A76C412009818FF /* PBXTargetDependency */, - 2FF892302A770DE200903A5A /* PBXTargetDependency */, - ); - name = TestAppWatchAppUITests; - productName = "TestAppWatchOS Watch AppUITests"; - productReference = 2F9CBEBF2A76C412009818FF /* TestAppWatchAppUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -254,7 +181,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1500; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1600; TargetAttributes = { 2F6D139128F5F384007C25D6 = { CreatedOnToolsVersion = 14.1; @@ -263,17 +190,9 @@ CreatedOnToolsVersion = 14.1; TestTargetID = 2F6D139128F5F384007C25D6; }; - 2F9CBEA52A76C40E009818FF = { - CreatedOnToolsVersion = 15.0; - }; - 2F9CBEBE2A76C412009818FF = { - CreatedOnToolsVersion = 15.0; - TestTargetID = 2F9CBEA52A76C40E009818FF; - }; }; }; buildConfigurationList = 2F6D138D28F5F384007C25D6 /* Build configuration list for PBXProject "UITests" */; - compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -281,14 +200,16 @@ Base, ); mainGroup = 2F6D138928F5F384007C25D6; + packageReferences = ( + A9E031642CAAACF900E13BD9 /* XCRemoteSwiftPackageReference "XCTestExtensions" */, + ); + preferredProjectObjectVersion = 77; productRefGroup = 2F6D139328F5F384007C25D6 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 2F6D139128F5F384007C25D6 /* TestApp */, 2F6D13AB28F5F386007C25D6 /* TestAppUITests */, - 2F9CBEA52A76C40E009818FF /* TestAppWatchApp */, - 2F9CBEBE2A76C412009818FF /* TestAppWatchAppUITests */, ); }; /* End PBXProject section */ @@ -309,21 +230,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 2F9CBEA42A76C40E009818FF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 2F9CBED82A76C75E009818FF /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 2F9CBEBD2A76C412009818FF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -331,8 +237,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A9E0315E2CAAA8F600E13BD9 /* ControlsView.swift in Sources */, 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */, - 2FB5B6E02C2F6C50009162E6 /* OperatingSystem.swift in Sources */, + A902FE552CAAB75800C80383 /* TestAppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -340,26 +247,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2FB5B6E22C2F707A009162E6 /* OperatingSystem.swift in Sources */, - 2F8A431329130A8C005D2B8F /* TestAppUITests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 2F9CBEA22A76C40E009818FF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 2FB5B6E12C2F6DBC009162E6 /* OperatingSystem.swift in Sources */, - 2F9CBED72A76C752009818FF /* TestApp.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 2F9CBEBB2A76C412009818FF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 2FB5B6E32C2F707B009162E6 /* OperatingSystem.swift in Sources */, - 2F9CBEDB2A76C7EC009818FF /* TestAppUITests.swift in Sources */, + A9E031602CAAAA9F00E13BD9 /* NotificationAuthorizationTests.swift in Sources */, + A902FE572CAAC04300C80383 /* PendingNotificationsTests.swift in Sources */, + 2F8A431329130A8C005D2B8F /* RemoteNotificationsTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -371,22 +261,6 @@ target = 2F6D139128F5F384007C25D6 /* TestApp */; targetProxy = 2F6D13AD28F5F386007C25D6 /* PBXContainerItemProxy */; }; - 2F9CBEC12A76C412009818FF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 2F9CBEA52A76C40E009818FF /* TestAppWatchApp */; - targetProxy = 2F9CBEC02A76C412009818FF /* PBXContainerItemProxy */; - }; - 2F9CBEC82A76C412009818FF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - platformFilter = ios; - target = 2F9CBEA52A76C40E009818FF /* TestAppWatchApp */; - targetProxy = 2F9CBEC72A76C412009818FF /* PBXContainerItemProxy */; - }; - 2FF892302A770DE200903A5A /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 2F6D139128F5F384007C25D6 /* TestApp */; - targetProxy = 2FF8922F2A770DE200903A5A /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -449,6 +323,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; TVOS_DEPLOYMENT_TARGET = 17.0; WATCHOS_DEPLOYMENT_TARGET = 10.0; XROS_DEPLOYMENT_TARGET = 1.0; @@ -507,6 +382,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 6.0; TVOS_DEPLOYMENT_TARGET = 17.0; VALIDATE_PRODUCT = YES; WATCHOS_DEPLOYMENT_TARGET = 10.0; @@ -517,9 +393,9 @@ 2F6D13B728F5F386007C25D6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; @@ -532,12 +408,14 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezinotifications.testapp; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator"; SUPPORTS_MACCATALYST = NO; @@ -547,15 +425,17 @@ SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,7"; + TVOS_DEPLOYMENT_TARGET = 18.0; + XROS_DEPLOYMENT_TARGET = 2.0; }; name = Debug; }; 2F6D13B828F5F386007C25D6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; @@ -568,12 +448,14 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezinotifications.testapp; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator"; SUPPORTS_MACCATALYST = NO; @@ -583,19 +465,22 @@ SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,7"; + TVOS_DEPLOYMENT_TARGET = 18.0; + XROS_DEPLOYMENT_TARGET = 2.0; }; name = Release; }; 2F6D13BD28F5F386007C25D6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 637867499T; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp.uitests; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezinotifications.testapp.uitests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator"; @@ -606,19 +491,22 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,7"; TEST_TARGET_NAME = TestApp; + TVOS_DEPLOYMENT_TARGET = 18.0; + XROS_DEPLOYMENT_TARGET = 2.0; }; name = Debug; }; 2F6D13BE28F5F386007C25D6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 637867499T; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp.uitests; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezinotifications.testapp.uitests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator"; @@ -629,173 +517,8 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,7"; TEST_TARGET_NAME = TestApp; - }; - name = Release; - }; - 2F9CBECB2A76C412009818FF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 637867499T; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - INFOPLIST_KEY_WKCompanionAppBundleIdentifier = edu.stanford.templatepackage.testapp; - INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = YES; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp.watchkitapp; - PRODUCT_NAME = "TestApp Watch App"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - }; - name = Debug; - }; - 2F9CBECC2A76C412009818FF /* Test */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 637867499T; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - INFOPLIST_KEY_WKCompanionAppBundleIdentifier = edu.stanford.templatepackage.testapp; - INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = YES; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp.watchkitapp; - PRODUCT_NAME = "TestApp Watch App"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - }; - name = Test; - }; - 2F9CBECD2A76C412009818FF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 637867499T; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - INFOPLIST_KEY_WKCompanionAppBundleIdentifier = edu.stanford.templatepackage.testapp; - INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = YES; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp.watchkitapp; - PRODUCT_NAME = "TestApp Watch App"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - }; - name = Release; - }; - 2F9CBED42A76C412009818FF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 637867499T; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp.watchkitapp.uitests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - TEST_TARGET_NAME = TestAppWatchApp; - }; - name = Debug; - }; - 2F9CBED52A76C412009818FF /* Test */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 637867499T; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp.watchkitapp.uitests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - TEST_TARGET_NAME = TestAppWatchApp; - }; - name = Test; - }; - 2F9CBED62A76C412009818FF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 637867499T; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp.watchkitapp.uitests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - TEST_TARGET_NAME = TestAppWatchApp; + TVOS_DEPLOYMENT_TARGET = 18.0; + XROS_DEPLOYMENT_TARGET = 2.0; }; name = Release; }; @@ -858,6 +581,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = TEST; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; TVOS_DEPLOYMENT_TARGET = 17.0; WATCHOS_DEPLOYMENT_TARGET = 10.0; XROS_DEPLOYMENT_TARGET = 1.0; @@ -867,9 +591,9 @@ 2FB07588299DDB6000C0B37F /* Test */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; @@ -882,12 +606,14 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezinotifications.testapp; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator"; SUPPORTS_MACCATALYST = NO; @@ -897,19 +623,22 @@ SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,7"; + TVOS_DEPLOYMENT_TARGET = 18.0; + XROS_DEPLOYMENT_TARGET = 2.0; }; name = Test; }; 2FB07589299DDB6000C0B37F /* Test */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 637867499T; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.templatepackage.testapp.uitests; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezinotifications.testapp.uitests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator"; @@ -920,6 +649,8 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,7"; TEST_TARGET_NAME = TestApp; + TVOS_DEPLOYMENT_TARGET = 18.0; + XROS_DEPLOYMENT_TARGET = 2.0; }; name = Test; }; @@ -956,36 +687,36 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 2F9CBECA2A76C412009818FF /* Build configuration list for PBXNativeTarget "TestAppWatchApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 2F9CBECB2A76C412009818FF /* Debug */, - 2F9CBECC2A76C412009818FF /* Test */, - 2F9CBECD2A76C412009818FF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 2F9CBED32A76C412009818FF /* Build configuration list for PBXNativeTarget "TestAppWatchAppUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 2F9CBED42A76C412009818FF /* Debug */, - 2F9CBED52A76C412009818FF /* Test */, - 2F9CBED62A76C412009818FF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + A9E031642CAAACF900E13BD9 /* XCRemoteSwiftPackageReference "XCTestExtensions" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordBDHG/XCTestExtensions.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ - 2F68C3C7292EA52000B3E12C /* TemplatePackage */ = { + 2F68C3C7292EA52000B3E12C /* SpeziNotifications */ = { + isa = XCSwiftPackageProductDependency; + productName = SpeziNotifications; + }; + A95C45EA2CAAB34600EBB08D /* XCTSpeziNotificationsUI */ = { + isa = XCSwiftPackageProductDependency; + productName = XCTSpeziNotificationsUI; + }; + A9E031652CAAACF900E13BD9 /* XCTestExtensions */ = { isa = XCSwiftPackageProductDependency; - productName = TemplatePackage; + package = A9E031642CAAACF900E13BD9 /* XCRemoteSwiftPackageReference "XCTestExtensions" */; + productName = XCTestExtensions; }; - 2F9CBED92A76C795009818FF /* TemplatePackage */ = { + A9E031672CAAAF6800E13BD9 /* XCTSpeziNotifications */ = { isa = XCSwiftPackageProductDependency; - productName = TemplatePackage; + productName = XCTSpeziNotifications; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj.license b/Tests/UITests/UITests.xcodeproj/project.pbxproj.license index d77e33d..f2f23af 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj.license +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj.license @@ -1,4 +1,4 @@ -This source file is part of the TemplatePackage open-source project +This source file is part of the SpeziNotifications open-source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/contents.xcworkspacedata.license b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/contents.xcworkspacedata.license index d77e33d..f2f23af 100644 --- a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/contents.xcworkspacedata.license +++ b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/contents.xcworkspacedata.license @@ -1,4 +1,4 @@ -This source file is part of the TemplatePackage open-source project +This source file is part of the SpeziNotifications open-source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist.license b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist.license index d77e33d..f2f23af 100644 --- a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist.license +++ b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist.license @@ -1,4 +1,4 @@ -This source file is part of the TemplatePackage open-source project +This source file is part of the SpeziNotifications open-source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme index 2d60ede..5d3fa24 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme @@ -1,6 +1,6 @@ diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme.license b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme.license index d77e33d..f2f23af 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme.license +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme.license @@ -1,4 +1,4 @@ -This source file is part of the TemplatePackage open-source project +This source file is part of the SpeziNotifications open-source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme index 50c8406..847735d 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme @@ -1,6 +1,6 @@ diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme.license b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme.license index d77e33d..f2f23af 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme.license +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme.license @@ -1,4 +1,4 @@ -This source file is part of the TemplatePackage open-source project +This source file is part of the SpeziNotifications open-source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)