PushKit background notification triggers viewDidAppear on initial view controller - ios

My app can receive background notifications through PushKit. These PushKit notifications trigger an action to clean up previously delivered regular notifications.
When I terminate the app and receive a PushKit notification, viewDidAppear is triggered on my initial view controller (as configured in the storyboard). This is causing some problems for me. I understand that PushKit launches your app in the background, but I don't understand why viewDidAppear is triggered; as the app is actually never opened.
pseudo AppDelegate.swift:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
voipRegistration()
Messaging.messaging().delegate = self
// setup Firebase/Bugfender
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) {granted, _ in
// setup regular notifications
}
application.registerForRemoteNotifications()
return true
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: #escaping () -> Void) {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
}
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
// register token with server
}
fileprivate func voipRegistration() {
let voipRegistry = PKPushRegistry(queue: nil)
voipRegistry.delegate = self
voipRegistry.desiredPushTypes = [.voIP]
}
I wonder if it's normal behaviour that viewDidAppear gets triggered through PushKit? The viewDidAppear in the initial controller starts my authentication process and I don't want that to happen while the app is in the background.

As described in the link #fewlinesofcode posted, viewDidAppear doesn't trigger when it physically appears on the screen, it triggers when it's added to the view controller hierarchy. So it makes sense that this triggers on startup.
I solved this using UIApplication.shared.applicationState, like so:
pseudo AppDelegate.swift:
func applicationWillEnterForeground(_ application: UIApplication) {
center.post(name: .ApplicationWillEnterForeground, object: self, userInfo: nil)
}
pseudo BaseViewController.swift:
class BaseViewController: UIViewController {
var applicationWillEnterForegroundObserver: NSObjectProtocol?
let center = NotificationCenter.default
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if UIApplication.shared.applicationState != .background {
authenticate()
} else {
if applicationWillEnterForegroundObserver == nil {
applicationWillEnterForegroundObserver = center.addObserver(
forName: .ApplicationWillEnterForeground,
object: nil, queue: nil) { (_) in
self.center.removeObserver(self.applicationWillEnterForegroundObserver!)
self.authenticate()
}
}
}
}
fileprivate func authenticate() {
// do authentication logic
}
}
This checks in viewDidAppear if the app is running in the background (like when PushKit launches the app). If it doesn't, simply authenticate and proceed. If it does, schedule a listener for applicationWillEnterForeground that authenticates as soon as the app actually comes to the foreground.

As you can find from Apple official documentation
Upon receiving a PushKit notification, the system automatically launches your app if it isn't running. By contrast, user notifications aren't guaranteed to launch your app.
So yes, it is correct behaviour.
UPD: The key point is, that application is woken up after receiving of the PushKit notification. As soon as app is running it has some execution time and your code is executing.

Related

pushRegistry(:didUpdate:for) not being called

I have looked at all questions about this but the answers to those have already been applied to my project but the pushRegistry() delegate method isn't getting invoked.
First here's the code:
import UIKit
import PushKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
print("App Launched");
self.registerVoIPPush();
return true
}
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
print("didUpdate");
print(pushCredentials);
print(type);
if type == PKPushType.voIP {
let tokenData = pushCredentials.token
let voipPushToken = String(data: tokenData, encoding: .utf8)
print(voipPushToken);
//send token to server
}
}
func registerVoIPPush() {
print("registerVoIPPush");
let voipPushResgistry = PKPushRegistry(queue: DispatchQueue.main)
voipPushResgistry.delegate = self
voipPushResgistry.desiredPushTypes = [PKPushType.voIP]
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
print(payload);
ATCallManager.shared.incommingCall(from: "jane#example.com", delay: 0)
}
}
I have also enabled following app capabilities: - Push Notifications -
Background Modes - Background Fetch - Remote Notifications - Voice
Over IP
I'm using Xcode11.3 & building using iOS13.2
On the provisioning portal Push Notifications have been enabled for the app id. (A VoIP Push cert has been generated & imported into keychain access as well but obviously can't use this to send the pushes yet).
Any help at all would be highly appreciated as I am trying to get this done since last 3 days.
Your PKPushRegistry object is deallocated as soon as registerVoIPPush() method is finished
Check if you set up appropriate background mode

open specific view controller upon tapping local notification

title says it all. i've went through a number of posts trying to put together a solution but to no luck..
i have a notification whose name i'm not sure of...
let request = UNNotificationRequest(identifier: "timerDone", content: content, trigger: trigger)
q1: is the name timerDone?
in viewDidLoad():
NotificationCenter.default.addObserver(self,
selector: "SomeNotificationAct:",
name: NSNotification.Name(rawValue: "timerDone"),
object: nil)
and then i have this method:
#objc func SomeNotificationAct(notification: NSNotification){
DispatchQueue.main.async() {
self.performSegue(withIdentifier: "NotificationView", sender: self)
}
}
with this in AppDelegate.swift:
private func application(application: UIApplication, didReceiveRemoteNotification userInfo: Any?){
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "SomeNotification"), object:nil)
}
any ideas how to do this? thanks in advance!
UPDATE: #Sh_Khan
first, i am coding in swift, i tried translating your code from obj-c to swift as:
if (launchOptions![UIApplicationLaunchOptionsKey.localNotification] != nil)
{
var notification =
launchOptions![UIApplicationLaunchOptionsKey.localNotification]
[self application:application didReceiveLocalNotification:notification]; //how to translate?
}
what should the last line be translated into?
when you wrote:
should store a boolean variable in user defaults in didReceiveLocalNotification method and check it in viewDidAppear method of the rootViewcontroller to make the segue and then make it false as the notificationCenter will work only when app is in foreground or in background if it's not yet suspended
let's say the boolean is notiView and we set it to true when we received the local notification and thus the segue will be to a different view controller. is this what you mean?
I found the documentation for user notification a bit confusing and incomplete. The tutorials are better than most other Apple frameworks. However, the tutorials are mostly incomplete and assume that every app implement the notification center delegate inside the AppDelegate. Not!
For many apps handling the notification delegate in a view controller (instead of in the AppDelegate), the view controller would need to be set as the user notification center delegate inside the AppDelegate didFinishLaunchingWithOptions method. Otherwise, your notification firing would not be visible to the notification delegate when your app is launched from the background mode. Your view controller is loaded after the notification fires. You need a way to launch the delegate methods after your view controller has completed its loading.
For example: suppose you are using a split view controller as your initial view controller for your app and you have implemented the split VC's master view controller as your notification delegate, you would need to let UNUserNotificationCenter know that the master VC is its delegate when your application launches (not inside the master VC's viewDidLoad() as most tutorials suggest). Eg,
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
let splitViewController = window!.rootViewController as! UISplitViewController
...
if #available(iOS 10.0, *) {
if let masterViewController = splitViewController.viewControllers.first as? MasterViewController {
UNUserNotificationCenter.current().delegate = masterViewController
}
}
}
This would allow iOS to call your notification delegate methods after the master VC is loaded when your app is launched cold or from the background mode.
In addition, if you need your master VC know that it was loaded because a user notification firing (and not from normal loading), you will use the NSUserDefaults to communicated this information. Hence, the AppDelegate would look as follows:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
let splitViewController = window!.rootViewController as! UISplitViewController
...
if #available(iOS 10.0, *) {
if let _ = launchOptions?[UIApplicationLaunchOptionsKey.localNotification] {
UserDefaults.standard.set("yes", forKey: kMyAppLaunchedByUserNotification)
UserDefaults.standard.synchronize()
}
if let masterViewController = splitViewController.viewControllers.first as? MasterViewController {
UNUserNotificationCenter.current().delegate = masterViewController
}
}
}
where kMyAppLaunchedByUserNotification is a key you use to communicate with the master VC. In the viewDidAppear() for Master View Controller, you would check User Defaults to see whether it is being loaded because of notification.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if #available(iOS 10.0, *) {
if let _ = UserDefaults.standard.object(forKey: kMyAppLaunchedByUserNotification) {
UserDefaults.standard.removeObject(forKey: kMyAppLaunchedByUserNotification)
UserDefaults.standard.synchronize()
//Handle any special thing that you need to do when your app is launched from User notification as opposed to regular app launch here
// NOTE: the notification delegate methods will be called after viewDidAppear() regardless of you doing any special thing here because you told iOS already in AppDelegate didFinishLaunchingWithOptions
}
}
}
I hope this helps you.
If the app is closed and you tapped a local notification then check it in didFinishLaunchingWithOptions method
/// Objective-C
if (launchOptions[UIApplicationLaunchOptionsLocalNotificationKey] != nil)
{
UILocalNotification *notification =
launchOptions[UIApplicationLaunchOptionsLocalNotificationKey];
[self application:application didReceiveLocalNotification:notification];
}
/// Swift
if (launchOptions![UIApplicationLaunchOptionsKey.localNotification] != nil)
{
var notification =
launchOptions![UIApplicationLaunchOptionsKey.localNotification]
self.application(application, didReceive: notification)
}
note: The viewDidLoad of the rootViewcontroller isn't yet called so observer won't be triggered , so you should store a boolean variable in user defaults in didReceiveLocalNotification method and check it in viewDidAppear method of the rootViewcontroller to make the segue and then make it false as the notificationCenter will work only when app is in foreground or in background if it's not yet suspended . . .
UPDATE: #Shi_Zhang
Yes,this is what I mean

User Notifications Swift 3

I want to send simple User Notifications when a Button is pressed. I used a tutorial from the internet, bit it still doesn't work.
The error is "Thread 1: Signal SIGABRT" in the AppDelegate
Here ist the Code:
import UIKit
import UserNotifications
class Map: UIViewController, UNUserNotificationCenterDelegate {
override func viewDidLoad() {
super.viewDidLoad()
//Senden von Mitteilungen?
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound]) { (granted, error) in
}
// Do any additional setup after loading the view.
}
#IBAction func Notification(_ sender: Any) {
//Set the content of the notification
let content = UNMutableNotificationContent()
content.title = "Achtung!"
content.subtitle = "Verbindung zum Beacon wurde getrennt"
content.body = "---"
//Set the trigger of the notification -- here a timer.
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: 10.0,
repeats: false)
//Set the request for the notification from the above
let request = UNNotificationRequest(
identifier: "10.second.message",
content: content,
trigger: trigger
)
//Add the notification to the currnet notification center
UNUserNotificationCenter.current().add(
request, withCompletionHandler: nil)
}
}
This is the App Delegate:
import UIKit
import Firebase
import CoreLocation
import UserNotifications
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
FIRApp.configure()
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
//EXTENSIONS
// Die Tastatur wird geschlossen, sobald der User außerhalb von ihr klickt
extension UIViewController {
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
}
func dismissKeyboard() {
view.endEditing(true)
}
}
I hope someone knows my mistake :)
Rename your #IBAction method to something else. You have a namespace conflict.
Ensure the #IBAction is actually linked to a button and fired (add a comment or breakpoint to the method).
Edit: Move your FIRApp.configure() out of applicationDidFinishLaunchingWithOptions. This has been known to cause crashes.
...
override init() {
FIRApp.configure()
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
return true
}
...
Tip: start method names with a lowercase letter. Classes with uppercase letters. This way you can easily identify these later.

Checking if iOS app was launched from local notification?

How do you detect if an app that is NOT in an active, inactive, or background state (terminated) is launched from a local notification? So far, I've tried two methods in the App Delegate's didFinishLaunchingWithOptions:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// METHOD 1:
if let options = launchOptions {
if let key = options[UIApplicationLaunchOptionsLocalNotificationKey] {
notificationCenter.postNotification(NSNotification(name: "applicationLaunchedFromNotification", object: nil))
}
}
// METHOD 2:
let notification = launchOptions?[UIApplicationLaunchOptionsLocalNotificationKey] as! UILocalNotification!
if (notification != nil) {
notificationCenter.postNotification(NSNotification(name: "applicationLaunchedFromNotification", object: nil))
}
return true
}
In my View Controller, I observe for the notification in ViewDidLoad and in response, set a UILabel's text:
override func viewDidLoad() {
super.viewDidLoad()
notificationCenter.addObserver(self, selector: "handleAppLaunchFromNotification", name: "applicationLaunchedFromNotification", object: nil)
}
func handleAppLaunchFromNotification() {
debugLabel.text = "app launched from notification"
}
But the UILabel's text is never set after launching the terminated app from a local notification.
My questions are:
What am I doing wrong?
Is there an easier way to debug a situation like this other than setting a UILabel? Once the app is terminated, Xcode's debugger detaches the app and I can't use print().
You are checking local notification in didFinishLaunchingWithOptions this methods contains launchOptions for only remote notifications. If app is in terminated state and you perform action on local notification then didReceiveLocalNotification gets call after didFinishLaunchingWithOptions method.

Clicking in UILocalNotifications when the app is closed

I'm doing this app that needs to send notifications when an event is coming.
Everything is working but when the app is closed and i open through the notification it doesn't fire how it is supposed to.
My App Delegate didFinishLaunchingWithOptions and didReceiveLocalNotification
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
createCopyOfDatabaseIfNeeded()
application.registerUserNotificationSettings(UIUserNotificationSettings(forTypes: UIUserNotificationType.Sound |
UIUserNotificationType.Alert | UIUserNotificationType.Badge, categories: nil))
if let options = launchOptions {
let value = options[UIApplicationLaunchOptionsLocalNotificationKey] as? UILocalNotification
if let notification = value {
self.application(application, didReceiveLocalNotification: notification)
}
}
return true
}
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
// Do something serious in a real app.
println("Received Local Notification:")
println(notification.alertBody)
if notification.alertAction == "editList" {
NSNotificationCenter.defaultCenter().postNotificationName("modifyListNotification", object: nil);
}
}
In my main tab controller:
override func viewDidLoad() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "handleModifyListNotification", name: "modifyListNotification", object: nil)
super.viewDidLoad()
self.repaint()
}
func handleModifyListNotification(){
dispatch_async(dispatch_get_main_queue(), {
// Show the alert
})
}
The main objective is when a notification is pressed with the app closed, it opens, executes didFinishLaunchingWithOptions and checks for UILocalNotification in launchOptions and calls didReceiveLocalNotification.
ps: i know with debug that the notifications is being well received, the system is just not calling the didReceiveLocalNotification method, like it should.
1º Edit
I'm really calling the didReceiveLocalNotifaction method because i just tried this and it worked
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
// Do something serious in a real app.
println("Received Local Notification:")
println(notification.alertBody)
if notification.alertAction == "editList" {
var xpto = UIAlertView()
xpto.title = notification.alertAction!
xpto.show() NSNotificationCenter.defaultCenter().postNotificationName("modifyListNotification", object: nil);
}
}
So i gess this must be a timing issue. I really want my tab controller to react to this notification.
NSNotificationCenter.defaultCenter().postNotificationName("modifyListNotification", object: nil);
If I'm not mistaken, -didReceiveLocalNotification: is only called when the app is in active state and being used.
If you're attempting to launch the app from a background state, I would suggest using -didFinishLaunchingWithOptions: and querying your options.
For this reason, maybe have your notification handling code in a separate method and have it called from both -didReceiveLocalNotification & -didFinishLaunchingWithOptions ?

Resources