-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
596 additions
and
27 deletions.
There are no files selected for viewing
95 changes: 95 additions & 0 deletions
95
.swiftpm/xcode/xcshareddata/xcschemes/NotificationsClients.xcscheme
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<Scheme | ||
LastUpgradeVersion = "1400" | ||
version = "1.3"> | ||
<BuildAction | ||
parallelizeBuildables = "YES" | ||
buildImplicitDependencies = "YES"> | ||
<BuildActionEntries> | ||
<BuildActionEntry | ||
buildForTesting = "YES" | ||
buildForRunning = "YES" | ||
buildForProfiling = "YES" | ||
buildForArchiving = "YES" | ||
buildForAnalyzing = "YES"> | ||
<BuildableReference | ||
BuildableIdentifier = "primary" | ||
BlueprintIdentifier = "NotificationsClients" | ||
BuildableName = "NotificationsClients" | ||
BlueprintName = "NotificationsClients" | ||
ReferencedContainer = "container:"> | ||
</BuildableReference> | ||
</BuildActionEntry> | ||
<BuildActionEntry | ||
buildForTesting = "YES" | ||
buildForRunning = "YES" | ||
buildForProfiling = "YES" | ||
buildForArchiving = "YES" | ||
buildForAnalyzing = "YES"> | ||
<BuildableReference | ||
BuildableIdentifier = "primary" | ||
BlueprintIdentifier = "RemoteNotificationsClient" | ||
BuildableName = "RemoteNotificationsClient" | ||
BlueprintName = "RemoteNotificationsClient" | ||
ReferencedContainer = "container:"> | ||
</BuildableReference> | ||
</BuildActionEntry> | ||
<BuildActionEntry | ||
buildForTesting = "YES" | ||
buildForRunning = "YES" | ||
buildForProfiling = "YES" | ||
buildForArchiving = "YES" | ||
buildForAnalyzing = "YES"> | ||
<BuildableReference | ||
BuildableIdentifier = "primary" | ||
BlueprintIdentifier = "UserNotificationsClient" | ||
BuildableName = "UserNotificationsClient" | ||
BlueprintName = "UserNotificationsClient" | ||
ReferencedContainer = "container:"> | ||
</BuildableReference> | ||
</BuildActionEntry> | ||
</BuildActionEntries> | ||
</BuildAction> | ||
<TestAction | ||
buildConfiguration = "Debug" | ||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||
shouldUseLaunchSchemeArgsEnv = "YES"> | ||
<Testables> | ||
</Testables> | ||
</TestAction> | ||
<LaunchAction | ||
buildConfiguration = "Debug" | ||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" | ||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" | ||
launchStyle = "0" | ||
useCustomWorkingDirectory = "NO" | ||
ignoresPersistentStateOnLaunch = "NO" | ||
debugDocumentVersioning = "YES" | ||
debugServiceExtension = "internal" | ||
allowLocationSimulation = "YES"> | ||
</LaunchAction> | ||
<ProfileAction | ||
buildConfiguration = "Release" | ||
shouldUseLaunchSchemeArgsEnv = "YES" | ||
savedToolIdentifier = "" | ||
useCustomWorkingDirectory = "NO" | ||
debugDocumentVersioning = "YES"> | ||
<MacroExpansion> | ||
<BuildableReference | ||
BuildableIdentifier = "primary" | ||
BlueprintIdentifier = "NotificationsClients" | ||
BuildableName = "NotificationsClients" | ||
BlueprintName = "NotificationsClients" | ||
ReferencedContainer = "container:"> | ||
</BuildableReference> | ||
</MacroExpansion> | ||
</ProfileAction> | ||
<AnalyzeAction | ||
buildConfiguration = "Debug"> | ||
</AnalyzeAction> | ||
<ArchiveAction | ||
buildConfiguration = "Release" | ||
revealArchiveInOrganizer = "YES"> | ||
</ArchiveAction> | ||
</Scheme> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"pins" : [ | ||
{ | ||
"identity" : "xctest-dynamic-overlay", | ||
"kind" : "remoteSourceControl", | ||
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", | ||
"state" : { | ||
"revision" : "30314f1ece684dd60679d598a9b89107557b67d9", | ||
"version" : "0.4.1" | ||
} | ||
} | ||
], | ||
"version" : 2 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,146 @@ | ||
# NotificationsClient | ||
# ✉️️ NotificationsClients | ||
![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange) ![Platforms](https://img.shields.io/badge/platforms-iOS%20-lightgrey.svg) | ||
|
||
A set of dependencies to easily implement remote or local notifications in your Swift app. | ||
|
||
### Dependencies included | ||
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. | ||
|
||
### Dependency format | ||
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](https://www.pointfree.co/) 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. | ||
|
||
## 📝 Requirements | ||
|
||
iOS 13 | ||
Swift 5.7 | ||
|
||
## 📦 Installation | ||
|
||
### Swift Package Manager | ||
Copy this repository URL, and add the repo into your Package Dependencies: | ||
``` | ||
https://github.com/nodes-ios/notificationsClients | ||
``` | ||
|
||
## 💻 Usage | ||
|
||
### Integrating the clients in your dependencies / environment | ||
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. | ||
```swift | ||
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()) | ||
} | ||
``` | ||
|
||
### Requesting the push notifications authorization | ||
```swift | ||
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)") | ||
} | ||
} | ||
``` | ||
|
||
### Receiving remote notifications | ||
Listen to the async stream of notification events. | ||
```swift | ||
// 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 | ||
} | ||
} | ||
``` | ||
### Testing | ||
See the `Testing` DocC article inside the targets. | ||
|
||
|
||
## Why the heck is Firebase not integrated in this already? 🔥 | ||
Two reasons: | ||
1. `FirebaseMessaging` works unreliably outside the app's main target. You really want to set it up inside your app delegate. | ||
2. This is a generic implementation that should work regardless the push notifications source. | ||
|
||
If you want to quickly integrate `FirebaseMessaging` with this, is most cases you just need to: | ||
- Configure Firebase | ||
- Assign the Messaging delegate | ||
- Send the Firebase token to your backend | ||
|
||
```swift | ||
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 | ||
} | ||
} | ||
``` | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
A description of this package. |
This file was deleted.
Oops, something went wrong.
16 changes: 16 additions & 0 deletions
16
Sources/RemoteNotificationsClient/Documentation.docc/RemoteNotificationsClient.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# ``RemoteNotificationsClient`` | ||
|
||
Client that can be used as a dependency to handle remote notification registation tasks. | ||
|
||
## Overview | ||
|
||
Basically just `UIApplication` remote notifications related APIs transformed into a dependency. | ||
This particular type of dependency makes use of `structs` and `closures` to define its interface instead of `protocols`. This mechanism allows to override the interface implementation easily for better mocking and testing. | ||
|
||
## Topics | ||
|
||
### Essentials | ||
|
||
- <doc:Testing> | ||
|
||
### Subtopic |
26 changes: 26 additions & 0 deletions
26
Sources/RemoteNotificationsClient/Documentation.docc/Testing.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Testing | ||
|
||
## Preparing the dependency for a test | ||
|
||
The main advantage of using this dependency structure is the simplicity of overriding its properties. This is very useful for testing. | ||
|
||
Usually you want to start with the ``RemoteNotificationsClient/failing`` object and override only those properties that you intend to use in the test. Calling any other properties will make the test fail, thus recognizing and unintended use of the dependency. | ||
|
||
Simple example: | ||
|
||
```swift | ||
func sillyTestExample async { | ||
var didRegister = false | ||
var client = RemoteNotificationsClient.failing | ||
client.registerForRemoteNotifications = { | ||
didRegister = true | ||
} | ||
|
||
await viewModel.userTappedRegisterNotifications() | ||
// Calling any other closure on the client apart from the one overriden will make the test fail here | ||
XCAssertTrue(didRegister) | ||
} | ||
``` | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// | ||
// RemoteNotificationsClient.swift | ||
// | ||
// | ||
// Created by Jiří Buček on 21.09.2022. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Interface for a client that handles app registration for remote notifications | ||
public struct RemoteNotificationsClient { | ||
|
||
/// A Boolean value that indicates whether the app is currently registered for remote notifications. | ||
public var isRegisteredForRemoteNotifications: () -> Bool | ||
|
||
/// Register the app to receive remote notifications | ||
public var registerForRemoteNotifications: () -> Void | ||
|
||
/// Unregister the app to receive remote notifications | ||
public var unregisterForRemoteNotifications: () async -> Void | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// | ||
// File.swift | ||
// | ||
// | ||
// Created by Jiří Buček on 21.09.2022. | ||
// | ||
|
||
import UIKit | ||
|
||
extension RemoteNotificationsClient { | ||
/// Live implementation of the RemoteNotificationClient interface. | ||
/// | ||
/// Transforms the remote notification related APIs of the `UIApplication` to the `RemoteNotificationsClient` interface | ||
public static let live = Self( | ||
isRegisteredForRemoteNotifications: { UIApplication.shared.isRegisteredForRemoteNotifications }, | ||
registerForRemoteNotifications: { UIApplication.shared.registerForRemoteNotifications() }, | ||
unregisterForRemoteNotifications: { await UIApplication.shared.unregisterForRemoteNotifications() } | ||
) | ||
} |
Oops, something went wrong.