From f1f6fb4d275d5685b612ef84bf847f9740fed47f Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 26 Aug 2024 21:26:21 +0200 Subject: [PATCH] Fix registerRemoteNotifications action would never return (#115) # Fix registerRemoteNotifications action would never return ## :recycle: Current situation & Problem #98 introduced the `registerRemoteNotifications` action. However, the knowledge source that stores the continuation was never persisted in the store. Therefore, the continuation was never resumed and allocated resources forever. This was fixed with this PR. We originally discussed supporting cancellation of the registration processed, however I decided against as it wasn't clear to properly handle cancellation (e.g., automatically call the unregister action? How to handle the delegate call that might arrive later. Especially when another call to the action might incur in the mean time). This PR changes the behavior when concurrently calling the registerRemoteNotifications action. Now, the second caller will wait for the first call to complete instead of throwing a concurrence access error. This helps to share this resources with, e.g., multiple modules trying to retrieve an device token. **To emphasize: `registerRemoteNotifications` works on simulator devices!** ## :gear: Release Notes * Fixed an issue where the `registerRemoteNotifications` actions would never return. ## :books: Documentation Slightly updated docs to provide more guidance around requesting authorization for notifications. ## :white_check_mark: Testing Added a unit test that verifies that the action returns. ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md). --------- Co-authored-by: Paul Schmiedmayer --- .../RegisterRemoteNotificationsAction.swift | 135 +++++++++++++----- .../UnregisterRemoteNotificationsAction.swift | 2 +- Sources/Spezi/Spezi/Spezi.swift | 8 +- .../LifecycleHandlerTestsView.swift | 2 +- .../NotificationModule.swift | 19 +++ .../RemoteNotificationsTestView.swift | 75 ++++++++++ .../TestApp/Shared/TestAppDelegate.swift | 1 + Tests/UITests/TestApp/SpeziTests.swift | 5 +- Tests/UITests/TestApp/TestApp.entitlements | 10 ++ .../TestApp/TestApp.entitlements.license | 5 + .../RemoteNotificationsTests.swift | 47 ++++++ .../UITests/UITests.xcodeproj/project.pbxproj | 38 ++++- 12 files changed, 301 insertions(+), 46 deletions(-) create mode 100644 Tests/UITests/TestApp/RemoteNotifications/NotificationModule.swift create mode 100644 Tests/UITests/TestApp/RemoteNotifications/RemoteNotificationsTestView.swift create mode 100644 Tests/UITests/TestApp/TestApp.entitlements create mode 100644 Tests/UITests/TestApp/TestApp.entitlements.license create mode 100644 Tests/UITests/TestAppUITests/RemoteNotificationsTests.swift diff --git a/Sources/Spezi/Capabilities/Notifications/RegisterRemoteNotificationsAction.swift b/Sources/Spezi/Capabilities/Notifications/RegisterRemoteNotificationsAction.swift index e0d3f426..2cae9e1e 100644 --- a/Sources/Spezi/Capabilities/Notifications/RegisterRemoteNotificationsAction.swift +++ b/Sources/Spezi/Capabilities/Notifications/RegisterRemoteNotificationsAction.swift @@ -10,15 +10,25 @@ import SpeziFoundation import SwiftUI -private final class RemoteNotificationContinuation: DefaultProvidingKnowledgeSource, Sendable { +@MainActor +private final class RemoteNotificationContinuation: KnowledgeSource, Sendable { typealias Anchor = SpeziAnchor - static let defaultValue = RemoteNotificationContinuation() + fileprivate(set) var continuation: CheckedContinuation? + fileprivate(set) var access = AsyncSemaphore() - @MainActor - var continuation: CheckedContinuation? init() {} + + + @MainActor + func resume(with result: Result) { + if let continuation { + self.continuation = nil + access.signal() + continuation.resume(with: result) + } + } } @@ -33,24 +43,40 @@ private final class RemoteNotificationContinuation: DefaultProvidingKnowledgeSou /// /// Below is a short code example on how to use this action within your ``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 handleNotificationsAllowed() async throws { -/// let deviceToken = try await registerRemoteNotifications() +/// 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 /// } /// } /// ``` -public struct RegisterRemoteNotificationsAction { - /// Errors occurring when registering for remote notifications. - public enum ActionError: Error { - /// The action was called while we were still waiting to receive a response from the previous one. - case concurrentAccess - } - +/// +/// > Tip: Make sure to request authorization by calling [`requestAuthorization(options:completionHandler:)`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/requestauthorization(options:completionhandler:)) +/// to have your remote notifications be able to display alerts, badges or use sound. Otherwise, all remote notifications will be delivered silently. +public struct RegisterRemoteNotificationsAction: Sendable { private weak var spezi: Spezi? init(_ spezi: Spezi) { @@ -65,28 +91,41 @@ public struct RegisterRemoteNotificationsAction { /// 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 in the - /// rare circumstance where you make a call to this method while another one is still ongoing. - /// Try again to register at a later point in time. - @MainActor + /// 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!") } + #if os(watchOS) let application = _Application.shared() #else let application = _Application.shared -#endif +#endif // os(watchOS) + + let registration: RemoteNotificationContinuation + if let existing = spezi.storage[RemoteNotificationContinuation.self] { + registration = existing + } else { + registration = RemoteNotificationContinuation() + spezi.storage[RemoteNotificationContinuation.self] = registration + } + + try await registration.access.waitCheckingCancellation() - let registration = spezi.storage[RemoteNotificationContinuation.self] - if registration.continuation != nil { - throw ActionError.concurrentAccess +#if targetEnvironment(simulator) + async let _ = withTimeout(of: .seconds(5)) { @MainActor in + spezi.logger.warning("Registering for remote notifications seems to be not possible on this simulator device. Timing out ...") + spezi.storage[RemoteNotificationContinuation.self]?.resume(with: .failure(TimeoutError())) } +#endif return try await withCheckedThrowingContinuation { continuation in + assert(registration.continuation == nil, "continuation wasn't nil") registration.continuation = continuation application.registerForRemoteNotifications() } @@ -100,20 +139,46 @@ extension Spezi { /// 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``. /// + /// - 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 handleNotificationsAllowed() async throws { - /// let deviceToken = try await registerRemoteNotifications() + /// 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 [`requestAuthorization(options:completionHandler:)`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/requestauthorization(options:completionhandler:)) + /// to have your remote notifications be able to display alerts, badges or use sound. Otherwise, all remote notifications will be delivered silently. + /// /// ## Topics /// ### Action /// - ``RegisterRemoteNotificationsAction`` @@ -126,24 +191,26 @@ extension Spezi { extension RegisterRemoteNotificationsAction { @MainActor static func handleDeviceTokenUpdate(_ spezi: Spezi, _ deviceToken: Data) { - let registration = spezi.storage[RemoteNotificationContinuation.self] - guard let continuation = registration.continuation else { - // might also be called if, e.g., app is restored from backup and is automatically registered for remote notifications. - // This can be handled through the `NotificationHandler` protocol. + guard let registration = spezi.storage[RemoteNotificationContinuation.self] else { return } - registration.continuation = nil - continuation.resume(returning: deviceToken) + + // might also be called if, e.g., app is restored from backup and is automatically registered for remote notifications. + // This can be handled through the `NotificationHandler` protocol. + + registration.resume(with: .success(deviceToken)) } @MainActor static func handleFailedRegistration(_ spezi: Spezi, _ error: Error) { - let registration = spezi.storage[RemoteNotificationContinuation.self] - guard let continuation = registration.continuation else { - spezi.logger.warning("Received a call to \(#function) while we were not waiting for a notifications registration request.") + guard let registration = spezi.storage[RemoteNotificationContinuation.self] else { return } - registration.continuation = nil - continuation.resume(throwing: error) + + if registration.continuation == nil { + spezi.logger.warning("Received a call to \(#function) while we were not waiting for a notifications registration request.") + } + + registration.resume(with: .failure(error)) } } diff --git a/Sources/Spezi/Capabilities/Notifications/UnregisterRemoteNotificationsAction.swift b/Sources/Spezi/Capabilities/Notifications/UnregisterRemoteNotificationsAction.swift index 1d2e1167..7b1bebea 100644 --- a/Sources/Spezi/Capabilities/Notifications/UnregisterRemoteNotificationsAction.swift +++ b/Sources/Spezi/Capabilities/Notifications/UnregisterRemoteNotificationsAction.swift @@ -27,7 +27,7 @@ import SwiftUI /// } /// } /// ``` -public struct UnregisterRemoteNotificationsAction { +public struct UnregisterRemoteNotificationsAction: Sendable { init() {} diff --git a/Sources/Spezi/Spezi/Spezi.swift b/Sources/Spezi/Spezi/Spezi.swift index c7d9e318..22f01052 100644 --- a/Sources/Spezi/Spezi/Spezi.swift +++ b/Sources/Spezi/Spezi/Spezi.swift @@ -79,9 +79,13 @@ import XCTRuntimeAssertions /// - ``launchOptions`` /// - ``spezi`` /// -/// ### Actions +/// ### Remote Notifications /// - ``registerRemoteNotifications`` /// - ``unregisterRemoteNotifications`` +/// +/// ### Dynamically Loading Modules +/// - ``loadModule(_:ownership:)`` +/// - ``unloadModule(_:)`` @Observable public final class Spezi: Sendable { static let logger = Logger(subsystem: "edu.stanford.spezi", category: "Spezi") @@ -93,7 +97,7 @@ public final class Spezi: Sendable { /// A shared repository to store any `KnowledgeSource`s restricted to the ``SpeziAnchor``. /// /// Every `Module` automatically conforms to `KnowledgeSource` and is stored within this storage object. - fileprivate(set) nonisolated(unsafe) var storage: SpeziStorage // nonisolated, writes are all isolated to @MainActor, just reads are non-isolated + nonisolated(unsafe) var storage: SpeziStorage // nonisolated, writes are all isolated to @MainActor, just reads are non-isolated /// Key is either a UUID for `@Modifier` or `@Model` property wrappers, or a `ModuleReference` for `EnvironmentAccessible` modifiers. @MainActor private var _viewModifiers: OrderedDictionary = [:] diff --git a/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestsView.swift b/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestsView.swift index 90ece242..34e33b38 100644 --- a/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestsView.swift +++ b/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestsView.swift @@ -14,7 +14,7 @@ import XCTSpezi struct LifecycleHandlerTestsView: View { @Environment(LifecycleHandlerModel.self) var model - + var body: some View { VStack { diff --git a/Tests/UITests/TestApp/RemoteNotifications/NotificationModule.swift b/Tests/UITests/TestApp/RemoteNotifications/NotificationModule.swift new file mode 100644 index 00000000..b6353d6c --- /dev/null +++ b/Tests/UITests/TestApp/RemoteNotifications/NotificationModule.swift @@ -0,0 +1,19 @@ +// +// 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 + + +@MainActor +class NotificationModule: Module, EnvironmentAccessible { + @Application(\.registerRemoteNotifications) + var registerRemoteNotifications + + @Application(\.unregisterRemoteNotifications) + var unregisterRemoteNotifications +} diff --git a/Tests/UITests/TestApp/RemoteNotifications/RemoteNotificationsTestView.swift b/Tests/UITests/TestApp/RemoteNotifications/RemoteNotificationsTestView.swift new file mode 100644 index 00000000..30240743 --- /dev/null +++ b/Tests/UITests/TestApp/RemoteNotifications/RemoteNotificationsTestView.swift @@ -0,0 +1,75 @@ +// +// 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 + + +struct RemoteNotificationsTestView: View { + @Environment(NotificationModule.self) + private var notificationModule + + @State private var token: Data? + @State private var error: Error? + + @State private var task: Task? + + var body: some View { + List { // swiftlint:disable:this closure_body_length + Section("Token") { + HStack { + Text(verbatim: "Token") + Spacer() + if let token { + Text(token.description) + .foregroundStyle(.green) + } else if let error = error as? LocalizedError, + let description = error.errorDescription ?? error.failureReason { + Text(verbatim: description) + .foregroundStyle(.red) + } else if error != nil { + Text(verbatim: "failed") + .foregroundStyle(.red) + } else { + Text(verbatim: "none") + .foregroundStyle(.secondary) + } + } + .accessibilityElement(children: .combine) + .accessibilityIdentifier("token-field") + } + + Section("Actions") { + Button("Register") { + task = Task { @MainActor in + do { + token = try await notificationModule.registerRemoteNotifications() + } catch { + self.error = error + } + } + } + Button("Unregister") { + notificationModule.unregisterRemoteNotifications() + token = nil + error = nil + } + } + } + .onDisappear { + task?.cancel() + } + } +} + + +#if DEBUG +#Preview { + RemoteNotificationsTestView() + .environment(NotificationModule()) +} +#endif diff --git a/Tests/UITests/TestApp/Shared/TestAppDelegate.swift b/Tests/UITests/TestApp/Shared/TestAppDelegate.swift index 82b8f372..15633b0a 100644 --- a/Tests/UITests/TestApp/Shared/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/Shared/TestAppDelegate.swift @@ -19,6 +19,7 @@ class TestAppDelegate: SpeziAppDelegate { } ModuleWithModifier() ModuleWithModel() + NotificationModule() } } } diff --git a/Tests/UITests/TestApp/SpeziTests.swift b/Tests/UITests/TestApp/SpeziTests.swift index b8357818..2194dd2a 100644 --- a/Tests/UITests/TestApp/SpeziTests.swift +++ b/Tests/UITests/TestApp/SpeziTests.swift @@ -14,7 +14,8 @@ enum SpeziTests: String, TestAppTests { case viewModifier = "ViewModifier" case lifecycleHandler = "LifecycleHandler" case model = "Model" - + case notifications = "Remote Notifications" + func view(withNavigationPath path: Binding) -> some View { switch self { @@ -24,6 +25,8 @@ enum SpeziTests: String, TestAppTests { LifecycleHandlerTestsView() case .model: ModelTestView() + case .notifications: + RemoteNotificationsTestView() } } } diff --git a/Tests/UITests/TestApp/TestApp.entitlements b/Tests/UITests/TestApp/TestApp.entitlements new file mode 100644 index 00000000..a28da917 --- /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 00000000..a648e99b --- /dev/null +++ b/Tests/UITests/TestApp/TestApp.entitlements.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/Tests/UITests/TestAppUITests/RemoteNotificationsTests.swift b/Tests/UITests/TestAppUITests/RemoteNotificationsTests.swift new file mode 100644 index 00000000..0fa5be52 --- /dev/null +++ b/Tests/UITests/TestAppUITests/RemoteNotificationsTests.swift @@ -0,0 +1,47 @@ +// +// 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.buttons["Remote Notifications"].waitForExistence(timeout: 2.0)) + app.buttons["Remote Notifications"].tap() + + XCTAssertTrue(app.navigationBars["Remote 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/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index ed958839..bba55d60 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 2F5FA4D029E0A4050047A644 /* XCTestApp in Frameworks */ = {isa = PBXBuildFile; productRef = 2F5FA4CF29E0A4050047A644 /* XCTestApp */; }; 2F5FA4D229E0B38C0047A644 /* XCTSpezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2F5FA4D129E0B38C0047A644 /* XCTSpezi */; }; 2F6D139A28F5F386007C25D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F6D139928F5F386007C25D6 /* Assets.xcassets */; }; + 2F79AE292C7563C1005282F4 /* NotificationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F79AE262C7563C1005282F4 /* NotificationModule.swift */; }; + 2F79AE2A2C7563C1005282F4 /* RemoteNotificationsTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F79AE272C7563C1005282F4 /* RemoteNotificationsTestView.swift */; }; 2F9F07F129090B0500CDC598 /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F9F07F029090B0500CDC598 /* TestAppDelegate.swift */; }; 2F9F07F529090BA900CDC598 /* ViewModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F9F07F429090BA900CDC598 /* ViewModifierTests.swift */; }; 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; }; @@ -20,6 +22,7 @@ 2FFDAD092A4AAEF400488F42 /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FFDAD082A4AAEF400488F42 /* FeatureFlags.swift */; }; 2FFDAD0B2A4AAF3700488F42 /* LifecycleHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FFDAD0A2A4AAF3600488F42 /* LifecycleHandlerTests.swift */; }; 2FFDAD0D2A4AB0CE00488F42 /* XCTestExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 2FFDAD0C2A4AB0CE00488F42 /* XCTestExtensions */; }; + A9AD5FF92C74860E00F3FBA8 /* RemoteNotificationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9AD5FF82C74860A00F3FBA8 /* RemoteNotificationsTests.swift */; }; A9F2ECD42AF0B85C0057C7DD /* ModuleWithModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECD32AF0B85C0057C7DD /* ModuleWithModifier.swift */; }; A9F2ECD62AF0BA500057C7DD /* ViewModifierTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECD52AF0BA500057C7DD /* ViewModifierTestView.swift */; }; A9F2ECD92AF198510057C7DD /* ModuleWithModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECD82AF198510057C7DD /* ModuleWithModel.swift */; }; @@ -43,6 +46,8 @@ 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; }; + 2F79AE262C7563C1005282F4 /* NotificationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationModule.swift; sourceTree = ""; }; + 2F79AE272C7563C1005282F4 /* RemoteNotificationsTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteNotificationsTestView.swift; sourceTree = ""; }; 2F7B6CB4294C03C800FDC494 /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = TestApp.xctestplan; path = UITests.xcodeproj/TestApp.xctestplan; sourceTree = ""; }; 2F9F07F029090B0500CDC598 /* TestAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppDelegate.swift; sourceTree = ""; }; 2F9F07F429090BA900CDC598 /* ViewModifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModifierTests.swift; sourceTree = ""; }; @@ -53,6 +58,8 @@ 2FFDAD062A4AAA2E00488F42 /* LifecycleHandlerTestModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LifecycleHandlerTestModule.swift; sourceTree = ""; }; 2FFDAD082A4AAEF400488F42 /* FeatureFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = ""; }; 2FFDAD0A2A4AAF3600488F42 /* LifecycleHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LifecycleHandlerTests.swift; sourceTree = ""; }; + A9AD5FF12C74827600F3FBA8 /* TestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TestApp.entitlements; sourceTree = ""; }; + A9AD5FF82C74860A00F3FBA8 /* RemoteNotificationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteNotificationsTests.swift; sourceTree = ""; }; A9F2ECD32AF0B85C0057C7DD /* ModuleWithModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleWithModifier.swift; sourceTree = ""; }; A9F2ECD52AF0BA500057C7DD /* ViewModifierTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifierTestView.swift; sourceTree = ""; }; A9F2ECD82AF198510057C7DD /* ModuleWithModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleWithModel.swift; sourceTree = ""; }; @@ -106,14 +113,16 @@ 2F6D139428F5F384007C25D6 /* TestApp */ = { isa = PBXGroup; children = ( - 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, - 2F5FA4CD29E0A3BA0047A644 /* SpeziTests.swift */, + 2F6D139928F5F386007C25D6 /* Assets.xcassets */, + 2F01E8CE291493560089C46B /* Info.plist */, 2FFDAD032A4AA99800488F42 /* LifecycleHandler */, A9F2ECD72AF198330057C7DD /* ModelTests */, + 2F79AE282C7563C1005282F4 /* RemoteNotifications */, 2F9F07ED29090AF500CDC598 /* Shared */, A9F2ECD22AF0B8460057C7DD /* ViewModifierTests */, - 2F6D139928F5F386007C25D6 /* Assets.xcassets */, - 2F01E8CE291493560089C46B /* Info.plist */, + 2F5FA4CD29E0A3BA0047A644 /* SpeziTests.swift */, + A9AD5FF12C74827600F3FBA8 /* TestApp.entitlements */, + 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, ); path = TestApp; sourceTree = ""; @@ -122,9 +131,10 @@ isa = PBXGroup; children = ( 2FB926E42974B0FC008E7B03 /* Info.plist */, - 2F9F07F429090BA900CDC598 /* ViewModifierTests.swift */, 2FFDAD0A2A4AAF3600488F42 /* LifecycleHandlerTests.swift */, A9F2ECDC2AF198690057C7DD /* ModelTests.swift */, + A9AD5FF82C74860A00F3FBA8 /* RemoteNotificationsTests.swift */, + 2F9F07F429090BA900CDC598 /* ViewModifierTests.swift */, ); path = TestAppUITests; sourceTree = ""; @@ -136,6 +146,15 @@ name = Frameworks; sourceTree = ""; }; + 2F79AE282C7563C1005282F4 /* RemoteNotifications */ = { + isa = PBXGroup; + children = ( + 2F79AE262C7563C1005282F4 /* NotificationModule.swift */, + 2F79AE272C7563C1005282F4 /* RemoteNotificationsTestView.swift */, + ); + path = RemoteNotifications; + sourceTree = ""; + }; 2F9F07ED29090AF500CDC598 /* Shared */ = { isa = PBXGroup; children = ( @@ -288,6 +307,8 @@ A9F2ECD62AF0BA500057C7DD /* ViewModifierTestView.swift in Sources */, 2F9F07F129090B0500CDC598 /* TestAppDelegate.swift in Sources */, 2FFDAD072A4AAA2E00488F42 /* LifecycleHandlerTestModule.swift in Sources */, + 2F79AE292C7563C1005282F4 /* NotificationModule.swift in Sources */, + 2F79AE2A2C7563C1005282F4 /* RemoteNotificationsTestView.swift in Sources */, A9F2ECDB2AF198590057C7DD /* ModelTestView.swift in Sources */, A9F2ECD42AF0B85C0057C7DD /* ModuleWithModifier.swift in Sources */, 2FFDAD052A4AA9D300488F42 /* LifecycleHandlerTestsView.swift in Sources */, @@ -301,6 +322,7 @@ files = ( A9F2ECDD2AF198690057C7DD /* ModelTests.swift in Sources */, 2FFDAD0B2A4AAF3700488F42 /* LifecycleHandlerTests.swift in Sources */, + A9AD5FF92C74860E00F3FBA8 /* RemoteNotificationsTests.swift in Sources */, 2F9F07F529090BA900CDC598 /* ViewModifierTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -449,6 +471,7 @@ buildSettings = { 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 = ""; @@ -485,6 +508,7 @@ buildSettings = { 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 = ""; @@ -603,8 +627,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/XCTestExtensions"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.4.9; + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; }; }; /* End XCRemoteSwiftPackageReference section */