Skip to content

Commit

Permalink
Regactor some infrastructure
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Sep 30, 2024
1 parent cbbe126 commit c73eaaa
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 129 deletions.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// 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 SpeziFoundation
import SwiftUI


@MainActor
@_spi(APISupport)
public final class RemoteNotificationRegistrationSupport: KnowledgeSource, Sendable {
public typealias Anchor = SpeziAnchor

private let logger = Logger(subsystem: "edu.stanford.spezi", category: "RemoteNotificationRegistrationSupport")

fileprivate(set) var continuation: CheckedContinuation<Data, Error>?
fileprivate(set) var access = AsyncSemaphore()


nonisolated init() {}


func handleDeviceTokenUpdate(_ deviceToken: Data) {
// 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.

resume(with: .success(deviceToken))
}

func handleFailedRegistration(_ error: Error) {
let resumed = resume(with: .failure(error))

if !resumed {
logger.warning("Received a call to \(#function) while we were not waiting for a notifications registration request.")
}
}


@discardableResult
private func resume(with result: Result<Data, Error>) -> Bool {
if let continuation {
self.continuation = nil
access.signal()
continuation.resume(with: result)
return true
}
return false
}

public func callAsFunction() async throws -> Data {
try await access.waitCheckingCancellation()

#if targetEnvironment(simulator)
async let _ = withTimeout(of: .seconds(5)) { @MainActor in
logger.warning("Registering for remote notifications seems to be not possible on this simulator device. Timing out ...")
self.continuation?.resume(with: .failure(TimeoutError()))
}
#endif

return try await withCheckedThrowingContinuation { continuation in
assert(self.continuation == nil, "continuation wasn't nil")
self.continuation = continuation
_Application.shared.registerForRemoteNotifications()
}
}
}


extension Spezi {
/// Provides support to call the `registerForRemoteNotifications()` method on the application.
///
/// This helper type makes sure to bridge access to the delegate methods that will be called when executing `registerForRemoteNotifications()`.
@MainActor
@_spi(APISupport)
public var remoteNotificationRegistrationSupport: RemoteNotificationRegistrationSupport {
let support: RemoteNotificationRegistrationSupport
if let existing = spezi.storage[RemoteNotificationRegistrationSupport.self] {
support = existing
} else {
support = RemoteNotificationRegistrationSupport()
spezi.storage[RemoteNotificationRegistrationSupport.self] = support
}
return support
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension Spezi {
public struct RegisterRemoteNotificationsAction: Sendable {
private weak var spezi: Spezi?

init(_ spezi: Spezi) {
fileprivate init(_ spezi: Spezi) {
self.spezi = spezi
}

Expand All @@ -43,38 +43,10 @@ extension Spezi {
@MainActor
public func callAsFunction() async throws -> Data {
guard let spezi else {
preconditionFailure("RegisterRemoteNotificationsAction was used in a scope where Spezi was not available anymore!")
preconditionFailure("\(Self.self) was used in a scope where Spezi was not available anymore!")
}


#if os(watchOS)
let application = _Application.shared()
#else
let application = _Application.shared
#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()

#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()
}
return try await spezi.remoteNotificationRegistrationSupport()
}
}

Expand Down Expand Up @@ -126,59 +98,9 @@ extension Spezi {
/// ## Topics
/// ### Action
/// - ``RegisterRemoteNotificationsAction``
@_disfavoredOverload
@available(*, deprecated, message: "Please migrate to the new SpeziNotifications package.")
public var registerRemoteNotifications: RegisterRemoteNotificationsAction {
RegisterRemoteNotificationsAction(self)
}
}


extension Spezi.RegisterRemoteNotificationsAction {
@MainActor
static func handleDeviceTokenUpdate(_ spezi: Spezi, _ deviceToken: Data) {
guard let registration = spezi.storage[Spezi.RemoteNotificationContinuation.self] else {
return
}

// 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) {
guard let registration = spezi.storage[Spezi.RemoteNotificationContinuation.self] else {
return
}

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))
}
}


extension Spezi {
@MainActor
private final class RemoteNotificationContinuation: KnowledgeSource, Sendable {
typealias Anchor = SpeziAnchor

fileprivate(set) var continuation: CheckedContinuation<Data, Error>?
fileprivate(set) var access = AsyncSemaphore()


init() {}


@MainActor
func resume(with result: Result<Data, Error>) {
if let continuation {
self.continuation = nil
access.signal()
continuation.resume(with: result)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,15 @@ extension Spezi {
/// Unregisters for all remote notifications received through Apple Push Notification service.
///
/// Refer to the documentation of ``Spezi/unregisterRemoteNotifications``.
@available(*, deprecated, message: "Please migrate to the new SpeziNotifications package.")
public struct UnregisterRemoteNotificationsAction: Sendable {
init() {}
fileprivate init() {}


/// Unregisters for all remote notifications received through Apple Push Notification service.
@MainActor
public func callAsFunction() {
#if os(watchOS)
let application = _Application.shared()
#else
let application = _Application.shared
#endif

application.unregisterForRemoteNotifications()
_Application.shared.unregisterForRemoteNotifications()
}
}

Expand All @@ -60,6 +55,8 @@ extension Spezi {
/// ## Topics
/// ### Action
/// - ``UnregisterRemoteNotificationsAction``
@_disfavoredOverload // TODO: is this cross module?

Check failure on line 58 in Sources/Spezi/Notifications/Spezi+UnregisterRemoteNotifications.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (is this cross module?) (todo)
@available(*, deprecated, message: "Please migrate to the new SpeziNotifications package.")
public var unregisterRemoteNotifications: UnregisterRemoteNotificationsAction {
UnregisterRemoteNotificationsAction()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ class SpeziNotificationCenterDelegate: NSObject {
// The completion handler would also be called on a background thread which results in a crash.
// Declaring the method as @MainActor requires a @preconcurrency inheritance from the delegate to silence Sendable warnings.

await withTaskGroup(of: Void.self) { @MainActor group in
// Moving this inside here (@MainActor isolated task group body) helps us avoid making the whole delegate method @MainActor.
// Apparently having the non-Sendable `UNNotificationResponse` as a parameter to a @MainActor annotated method doesn't suppress
// the warning with @preconcurrency, but capturing `response` in a @MainActor isolated closure does.
await withTaskGroup(of: Void.self) { group in
guard let delegate = SpeziAppDelegate.appDelegate else {
return
}
Expand All @@ -44,8 +41,7 @@ class SpeziNotificationCenterDelegate: NSObject {
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
await withTaskGroup(of: UNNotificationPresentationOptions?.self) { @MainActor group in
// See comment in method above.
await withTaskGroup(of: UNNotificationPresentationOptions?.self) { group in
guard let delegate = SpeziAppDelegate.appDelegate else {
return []
}
Expand Down
28 changes: 0 additions & 28 deletions Sources/Spezi/Spezi/EnvironmentValues+Spezi.swift

This file was deleted.

11 changes: 8 additions & 3 deletions Sources/Spezi/Spezi/SpeziAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate, Sendable {

private(set) var _spezi: Spezi? // swiftlint:disable:this identifier_name

var spezi: Spezi {
/// Access the Spezi instance.
///
/// Use this property as a basis for creating your own APIs (e.g., providing SwiftUI Environment values that use information from Spezi).
/// To not make it directly available to the user.
@_spi(APISupport)
public var spezi: Spezi {
guard let spezi = _spezi else {
let spezi = Spezi(from: configuration)
self._spezi = spezi
Expand Down Expand Up @@ -142,7 +147,7 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate, Sendable {

open func application(_ application: _Application, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
MainActor.assumeIsolated { // on macOS there is a missing MainActor annotation
Spezi.RegisterRemoteNotificationsAction.handleDeviceTokenUpdate(spezi, deviceToken)
spezi.remoteNotificationRegistrationSupport.handleDeviceTokenUpdate(deviceToken)

// notify all notification handlers of an updated token
for handler in spezi.notificationTokenHandler {
Expand All @@ -153,7 +158,7 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate, Sendable {

open func application(_ application: _Application, didFailToRegisterForRemoteNotificationsWithError error: Error) {
MainActor.assumeIsolated { // on macOS there is a missing MainActor annotation
Spezi.RegisterRemoteNotificationsAction.handleFailedRegistration(spezi, error)
spezi.remoteNotificationRegistrationSupport.handleFailedRegistration(error)
}
}

Expand Down
1 change: 0 additions & 1 deletion Sources/Spezi/Spezi/View+Spezi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ struct SpeziViewModifier: ViewModifier {
func body(content: Content) -> some View {
spezi.viewModifiers
.modify(content)
.environment(\.spezi, spezi)
}
}

Expand Down

0 comments on commit c73eaaa

Please sign in to comment.