A set of dependencies to easily implement remote or local notifications in your Swift app.
Two dependencies included:
-
UserNotificationsClient - Manages notification-related activities for your app. Basically just
UNUserNotificationCenter
and its delegate transformed into a testable async dependency. -
RemoteNotificationsClient - Handles app registration for remote notifications.
UIApplication
's remote notifications API transformed into a testable async dependency.
Every client has its own target inside this SPM package called NotificationsClients
. These clients are usually used in conjunction when implementing the remote notifications, thus I put them inside one package.
Both dependencies use structs
with closure
properties instead of protocols to define their interface. This makes for easy mocking and testing. This format is inspired by PointFree composable architecture.
The interface is async await
. The UNUserNotificationCenterDelegate
is transformed from a delegate pattern to an AsyncStream
of delegate events which is much more readable in the target code.
iOS 13
Swift 5.7
Copy this repository URL, and add the repo into your Package Dependencies:
https://github.com/nodes-ios/notificationsClients
The package also includes an example project. If in doubt, check the code in the Example folder.
For production code, just use the .live()
static property.
❗️ IMPORTANT❗️
You must call the..live()
of UserNotificationsClient
syncronously before the app delegate's didFinishWithLaunching
returns. This makes sure the correct delegate is assigned to the UNUserNotificationCenter.current()
and therefore the app can react to push notifications when opened from suspended state.
import UserNotificationsClient
import RemoteNotificationsClient
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Set up Dependencies
setupAppEnvironment()
return true
}
func setupAppEnvironment() {
let environment = AppEnvironment(remoteNotificationsClient: RemoteNotificationsClient.live,
userNotificationsClient: UserNotificationsClient.live())
}
Both the RemoteNotificationsClient
and the UserNotificationsClient
are already prepared for integration using the PointFree Dependencies library. You can also define them as:
@Dependency(\.remoteNotificationsClient) var remoteNotificationsClient
@Dependency(\.userNotificationClient) var userNotificationsClient
func requestPushAuthorization() async {
do {
// Get the authorization status
switch await environment.userNotificationsClient.getAuthorizationStatus() {
case .notDetermined:
// Request the authorization
let allowed = try await environment.userNotificationsClient.requestAuthorization([.alert, .badge, .sound])
if allowed {
// Register the app to receive remote notifications
// You probably want to call this also on app start
// for case when the user allows the push permissions in the iOS settings
environment.remoteNotificationsClient.registerForRemoteNotifications()
}
default: return
}
} catch {
print("❗️ Could not request remote notifications authorization: \(error)")
}
}
Listen to the async stream of notification events.
// Listen to incoming notification events
// Call this in the app delegate preferably
Task {
for await event in environment.userNotificationsClient.delegate() {
handleNotificationEvent(event)
}
}
func handleNotificationEvent(_ event: UserNotificationClient.DelegateEvent) {
switch event {
case .willPresentNotification(_, let completion):
// Notification presented when the app is in foregroud
// Decide how to present it depending on the context
// Pass the presentation options to the completion
// If you do not pass anythig, the notification will not be presented
completion([.banner, .sound, .list, .badge])
case .didReceiveResponse(let response, let completion):
// User tapped on the notification
// Is triggered both when the app is in foreground and background
handleNotificationResponse(response)
completion()
case .openSettingsForNotification:
return
}
}
See the Testing
DocC article inside the targets.
Two reasons:
FirebaseMessaging
works unreliably outside the app's main target. You really want to set it up inside your app's delegate.- This is a generic implementation that should work regardless the push notifications source.
If you want to quickly integrate FirebaseMessaging
with this, in most cases you just need to:
- Configure Firebase
- Assign the Messaging delegate
- Send the Firebase token to your backend
import Firebase
import FirebaseMessaging
extension AppDelegate: MessagingDelegate {
func startFirebase() {
FirebaseApp.configure()
Messaging.messaging().delegate = self
}
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
// Send the Firebase token to your backend here
}
}