Handling where the user goes when clicking on a notification? - ios

I have several different types of notifications, all of which should take the user to a different view controller when the notification is clicked on.
How should this be handled (I'm using Swift 5, by the way)? From my research, I see that people tend to present a new view controller in the AppDelegate's didReceive function, but doing all the logic, for several different view controllers, all in the AppDelegate seems wrong. Is this really the right way of doing it?
Further, I'm using Firebase to send messages to the device from the backend. I have a separate class, FirebaseUtils, where I handle all logic for the data that's passed along. Would it be better to present the view controller from here? If so, how would I do that without the root view controller?

I typically set up something along these lines (untested):
Create a NotificationHandler protocol for things that may handle notifications
protocol NotificationHandler {
static func canHandle(notification: [AnyHashable : Any])
static func handle(notification: [AnyHashable : Any], completionHandler: #escaping (UIBackgroundFetchResult) -> Void)
}
Create a notificationHandlers variable in AppDelegate and populate it with things that may want to handle notifications.
let notificationHandlers = [SomeHandler.self, OtherHandler.self]
In didReceive, loop over the handlers, ask each one if it can handle the notification, and if it can, then tell it to do so.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
guard let handler = notificationHandlers.first(where:
{ $0.canHandle(notification: userInfo) }) {
else {
return
}
handler.handle(notification: userInfo, completionHandler: completionHandler)
}
This approach keeps the logic out of the AppDelegate, which is correct, and keeps other types from poking around inside the AppDelegate, which is also correct.

would something like this work for you?
struct NotificationPresenter {
func present(notification: [AnyHashable: Any], from viewController: UIViewController) {
let notificationViewController: UIViewController
// decide what type of view controller to show and set it up
viewController.present(notificationViewController, animated: true, completion: nil)
}
}
extension UIViewController {
static func topViewController(_ parentViewController: UIViewController? = nil) -> UIViewController {
guard let parentViewController = parentViewController else {
return topController(UIApplication.shared.keyWindow!.rootViewController!)
}
return parentViewController.presentedViewController ?? parentViewController
}
}
let notificationPresenter = NotificationPresenter()
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
notificationPresenter.present(userInfo, from: UIViewController.topViewController())
}

Related

Pushwoosh IOS notifications, onOpen invoked without click

I have an IOS native app with Pushwoosh notifications.
When I put the app in background mode (Not killed), and then received a push, methon onMessageOpened called, even the user didn’t open the push.
My onMessageOpened method:
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, PWMessagingDelegate {
...
...
func pushwoosh(_ pushwoosh: Pushwoosh!, onMessageOpened message: PWMessage!) {
var url = message.payload["url"] as! String
let myURL = URL(string:url)
let myRequest = URLRequest(url: myURL!)
self.webView.load(myRequest)
}
}
This is in my AppDelegate:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
Pushwoosh.sharedInstance()?.handlePushReceived(userInfo)
completionHandler(.noData)
}
In addition I have a difficult with debugging this issue. Because I received push only in production version. Not in debug mode.
EDIT:
After I add a log calls, I can see it called to onMessageOpened just after receiving a push. You can see the low latency between records.
This is my new code:
func pushwoosh(_ pushwoosh: Pushwoosh!, onMessageReceived message: PWMessage!) {
NSLog("PushWoosh: message recevied!")
}
func pushwoosh(_ pushwoosh: Pushwoosh!, onMessageOpened message: PWMessage!) {
NSLog("PushWoosh: message opened!")
...
}
And this is my logs after a few hours of device was untouched.

How to generate a Combine Publisher for didReceiveRemoteNotification?

I am trying to generate a Combine publisher off didReceiveRemoteNotification
Similar to this code below:
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
I want to use SwiftUI Lifecycle and don't want to use AppDelegate methods using #UIApplicationDelegateAdaptor
There is no Notification named didReceiveRemoteNotification, but you can declare it in an extension on UIApplication:
extension UIApplication {
static let didReceiveRemoteNotification = Notification.Name("didReceiveRemoteNotification")
}
Then you need to post the Notification from the AppDelegate:
extension AppDelegate {
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
NotificationCenter.default.post(name: UIApplication.didReceiveRemoteNotification,
object: nil)
// etc...
}
}
That will allow you to use the usual syntax:
NotificationCenter.default.publisher(for: UIApplication.didReceiveRemoteNotification)

How to navigate the user to required view controller on tap of deep link for fresh installation of app with firebase

I want to navigate the user to specific view controller on click of deep linking
If a user already installed the app I am able do by using below method in app delegate
application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool
But how to navigate the user to desired ViewController for first time installation of app
Maybe you can use UserDefaults...
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let alreadyInitialized = UserDefaults.standard.bool(forKey: "AlreadyInitialized")
if !alreadyInitialized {
UserDefaults.set(true, forKey: "AlreadyInitialized")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let destinationViewController = storyboard.instantiateViewControllerWithIdentifier("FirstTimeViewController") as FirstTimeViewController
let navigationController = self.window?.rootViewController as! UINavigationController
navigationController?.pushViewController(destinationViewController, animated: false, completion: nil)
}
}

How to subscribe to changes for a public database in CloudKit?

What is the best way to subscribe to a public database in CloudKit?
I have a table with persons. Every person contains a name and a location.
Once the location changes, the location is updated in CloudKit.
That part is working fine.
But I am not able to make it work to get a notification when there is a record update.
Some example would be really helpful, as I have looked into the possible option already.
I have looked into the options where I save the subscription in the database and also the CKModifySubscriptionsOperation option.
Currently, my code to subscribe looks like this:
let predicate = NSPredicate(format: "TRUEPREDICATE")
let newSubscription = CKQuerySubscription(recordType: "Person", predicate: predicate, options: [.firesOnRecordCreation, .firesOnRecordDeletion, .firesOnRecordUpdate])
let info = CKSubscription.NotificationInfo()
info.shouldSendContentAvailable = true
newSubscription.notificationInfo = info
database.save(newSubscription, completionHandler: {
(subscription, error) in
if error != nil {
print("Error Creating Subscription")
print(error)
} else {
userSettings.set(true, forKey: "subscriptionSaved")
}
})
Can someone also show me how my AppDelegate should look like?
I have added the didReceiveRemoteNotification function to my AppDelegate. I also called application.registerForRemoteNotifications(). This is how my didReceiveRemoteNotification function looks like:
The print is not even coming for me.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
print("Notification!!")
let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) as? CKDatabaseNotification
if notification != nil {
AppData.checkUpdates(finishClosure: {(result) in
OperationQueue.main.addOperation {
completionHandler(result)
}
})
}
}
Here are a few other things you can check:
= 1 =
Make sure the CloudKit container defined in your code is the same one you are accessing in the CloudKit dashboard. Sometimes we overlook what we selected in Xcode as the CloudKit container when we create and test multiple containers.
= 2 =
Check the Subscriptions tab in the CloudKit dashboard and make sure your Person subscription is being created when you launch your app. If you see it, try deleting it in the CK Dashboard and then run your app again and make sure it shows up again.
= 3 =
Check the logs in the CK Dashboard. They will show a log entry of type push whenever a push notification is sent. If it's logging it when you update/add a record in the CK Dashboard, then you know the issue lies with your device.
= 4 =
Remember that push notifications don't work in the iOS simulator. You need an actual device (or a Mac if you are making a macOS app).
= 5 =
Through extensive testing, I've found notifications are more reliable if you always set the alertBody even if it's blank. Like this:
let info = CKSubscription.NotificationInfo()
info.shouldSendContentAvailable = true
info.alertBody = "" //This needs to be set or pushes don't always get sent
subscription.notificationInfo = info
= 6 =
For an iOS app, my app delegate handles notifications like this:
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//Ask Permission for Notifications
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound], completionHandler: { authorized, error in
DispatchQueue.main.async {
if authorized {
UIApplication.shared.registerForRemoteNotifications()
}
}
})
return true
}
//MARK: Background & Push Notifications
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]{
let dict = userInfo as! [String: NSObject]
let notification = CKNotification(fromRemoteNotificationDictionary: dict)
if let sub = notification.subscriptionID{
print("iOS Notification: \(sub)")
}
}
//After we get permission, register the user push notifications
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
//Add your CloudKit subscriptions here...
}
}
Getting permission for notifications isn't required if you are only doing background pushes, but for anything the user sees in the form of a popup notification, you must get permission. If your app isn't asking for that permission, try deleting it off your device and building again in Xcode.
Good luck! : )
I am using RxCloudKit library, here's an a code snippet of how it handles query notifications -
public func applicationDidReceiveRemoteNotification(userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let dict = userInfo as! [String: NSObject]
let notification = CKNotification(fromRemoteNotificationDictionary: dict)
switch notification.notificationType {
case CKNotificationType.query:
let queryNotification = notification as! CKQueryNotification
self.delegate.query(notification: queryNotification, fetchCompletionHandler: completionHandler)
...
This method is called from func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void)
Before you can receive notifications, you will need to do the following -
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
application.registerForRemoteNotifications()
...
UPDATE:
Info.plist should contain the following -
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
Update: As Reinhard mentioned in his comment: you can in fact still subscribe to changes from the public database and manually import the changes into Core Data. Still I am unsure whether it is a good idea to rely on subscriptions in the public database if Apple specifically mentioned these differences
Original answer:
I don't think the accepted answer fully answers the question here.
Short answer would be that the CloudKit public database does not support subscriptions like the private database. Instead only a polling mechanism can be used. NSPersistentCloudKitContainer handles this automatically, but only updates very rarely.
This talk from WWDC2020 explains this in detail and I recommend watching it because there are other important details mentioned where public database differs from private database: https://developer.apple.com/wwdc20/10650
In the talk they mentioned thet a pull is initiated on each app start and after about 30 mins of application usage.

How to call completionHandler from Singleton class?

I'm writing an App in Swift where I have implemented a singleton class handling the data retrieval via Moscapsule.
When I inform my app that there is new data with content-available: 1 the right function gets called where I'm currently only doing
let mqtt = MQTTManager.Instance
completionHandler(UIBackgroundFetchResult.NewData)
The manager creates a connection on init and the onMessageCallback then works with the new data.
If the app has run recently, is in background and the singleton class keeps up the network connection, this works. But this is not the right way.
I'm very new to iOS and Swift development, how can I wait the maximum amount of time? When the data comes in I could then send UIBackgroundFetchResult.NewData, if nothing comes, I could then send UIBackgroundFetchResult.Failed.
My first idea was to write a loop checking a variable from that class if a new message has come in every 500ms, and if so, call the completionHandler. But this doesn't feel right to me, so, what would you do?
EDIT 2015/02/18:
Heres the code from my AppDelegate.swift that gets executed once the app receives the silent push notification:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
log.debug("Got remote notification: \(userInfo)")
log.debug("Setting up MQTT client...")
let mqtt = MQTTManager.sharedInstance
completionHandler(UIBackgroundFetchResult.NewData)
}
You could define a property within that class to hold the completion handler (making it an optional):
var completionHandler: ((UIBackgroundFetchResult) -> Void)?
Then your didReceiveRemoteNotification would do something like:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
log.debug("Got remote notification: \(userInfo)")
log.debug("Setting up MQTT client...")
let mqtt = MQTTManager.sharedInstance
mqtt.completionHandler = completionHandler
}
Then, the onMessageCallback could then do something like:
self.completionHandler?(.NewData) // or .NoData or whatever
self.completionHandler = nil
I must confess that this doesn't feel right. It feels like the init function is not only instantiating the singleton, but also starting a connection, making a request, what have you. You haven't shared it with us, so it's hard to say, but it feels incorrect. The init should really only be creating the singleton. Then, let us configure it (e.g. specify the completionHandler) and only then would it call the separate connect function.

Resources