When my app is active and I receive a silent notif from the server to update my tableview with the new data, I'm calling the following function in which I'm making a request to the server to bring latest data and then reload that specific row.
func updateCell(path: Int, messageId: String) {
let indexPath = IndexPath(item: path, section: 0)
if let visibleIndexPaths = mainTableView.indexPathsForVisibleRows?.index(of: indexPath as IndexPath) {
if visibleIndexPaths != NSNotFound {
if let id = self.userData?.id {
let conversationID = String(describing: id)
ServerConnection.getSpecificMessage(conversationId: conversationID, messageId: messageId) { (dataMessage) in
if let message = dataMessage {
self.chat[path] = message
self.mainTableView.beginUpdates()
self.mainTableView.reloadRows(at: [indexPath], with: .fade)
self.mainTableView.endUpdates()
}
}
}
}
}
}
My problem is when my app is in the foreground the flow doesn't work anymore because of the API request which can't be done in the foreground / background .
Console log shows :
load failed with error Error Domain=NSPOSIXErrorDomain Code=53 "Software caused connection abort"
I've tried to modify my function with
let state = UIApplication.shared.applicationState
if state == .background || state == .inactive {
NotificationCenter.default.addObserver(self, selector: #selector(self.reloadData(_:)), name: NSNotification.Name("BackgroundReload"), object: nil)
}
and posted this "BackgroundRelod" notification in AppDelegate
func applicationWillEnterForeground(_ application: UIApplication)
but this will always trigger my function even though I didn't receive any silent notification to update the UI.
You should not depend on background mode in updates , you need to only modify a var say needsUpdate whenever a silent notification comes in background here
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
Then
NotificationCenter.default.addObserver(self, selector: #selector(update), name: UIApplication.willEnterForegroundNotification, object: nil)
#objc func ppp(_ no:NSNotification) {
if needsUpdate {
// pull here
}
}
Related
Platform
Swift 5
iOS 13+
xCode 11
Node v14.2.0
Firebase/Firestore latest
Setting
Alice send push notification to Bob, while Bob's phone is .inactive or .background. Bob's phone should get notification and immediately trigger code.
Problem
This question has plenty of answers, but most of what I can find revolves around hacking the PushKit and CallKit native API to send .voIP pushes. Per this question (iOS 13 not getting VoIP Push Notifications in background), Apple no longer allow you to send .voIP pushes w/o triggering CallKit's native phone ring routine.
On iOS side, I have the following bits in AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
registerForPushNotifications()
}
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void)
{
print(">>> I would like this to be triggered even if the app is asleep")
switch application.applicationState {
case .active:
print(">>>>>>> the app is in [FOREGROUND]: \(userInfo)")
break
case .inactive, .background:
print(">>>>>>>> the app is in [BACKGROUND]: \(userInfo)")
break
default:
break
}
}
func registerForPushNotifications() {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter
.current()
.requestAuthorization(options:[.alert, .sound, .badge]) {[weak self] granted, error in
guard granted else { return }
self?.getNotificationSettings()
}
}
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
guard settings.authorizationStatus == .authorized else { return }
Messaging.messaging().delegate = self
DispatchQueue.main.async {
// Register with Apple Push Notification service
UIApplication.shared.registerForRemoteNotifications()
/// cache token client side and save in `didRegisterForRemoteNotificationsWithDeviceToken`
if let token = Messaging.messaging().fcmToken {
self.firebaseCloudMessagingToken = token
}
}
}
}
//#Use: listen for device token and save it in DB, so notifications can be sent to this phone
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
if (firebaseCloudMessagingToken != nil){
self.updateMyUserData(
name : nil
, pushNotificationToken: firebaseCloudMessagingToken!
)
}
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
///print(">>> Failed to register: \(error)")
}
#available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
// #NOTE: this fires when the app is open. So you can go the call screen right away
let payload = notification.request.content.userInfo as! [String:Any?]
let type = payload["notificationType"]
print(">> this fires if the app is currently open")
}
/// #NOTE: we are using backward compatible API to access user notification when the app is in the background
/// #source: https://firebase.google.com/docs/cloud-messaging/ios/receive#swift:-ios-10
#available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
print(" this fires when the user taps on the notification message")
}
On the server/Node.js side, I send push notification this way:
// Declare app push notification provider for PushKit
const _ApnConfig_ = {
token: {
key : fileP8
, keyId : "ABCDEFG"
, teamId: "opqrst"
},
production: false
};
var apnProvider = new apn.Provider(_ApnConfig_);
exports.onSendNotification = functions.https.onRequest((request, response) => {
var date = new Date();
var timeStamp = date.getTime();
const deviceTok = "..."
var recepients = [apn.token( deviceTok )]
const notification = new apn.Notification();
notification.topic = "com.thisisnt.working"
notification.body = "Hello, world!";
notification.payload = {
from: "node-apn"
, source: "web"
, aps: {
"content-available": 1
, "data" : { "custom_key":"custom value", "custom_key_2":"custom value 2" }
}
};
notification.body = "Hello, world # " + timeStamp;
return apnProvider.send(notification, recepients).then(function(res) {
console.log("res.sent: ", res.sent)
console.log("res.failed: ", res.failed)
res.failed.forEach( (item) => {
console.log(" \t\t\t failed with error:", item.error)
})
return response.send("finished!");
}).catch( function (error) {
console.log("Faled to send message: ", error)
return response.send("failed!");
})
})
Both are pretty standard. I have set the content-availabe to 1. Right now the messages are coming through and displayed by Apple Push Notification center, they're just not triggering the block with didReceiveRemoteNotification as intended.
You need to enable the background mode - remote notifications capability.
To receive background notifications, you must add the remote notifications background mode to your app. In the Signing & Capability tab, add the Background Modes capability, then select the Remote notification checkbox.
Enabling the remote notifications background mode:
For watchOS, add this capability to your WatchKit Extension.
Source: Pushing Background Updates to Your App | Apple Developer Documentation
I was handling push notification data and then after call API based in push notification custom data. This will work fine when app is in Active and background state.
But when app is not running and then click on notification, I was able to get custom data from custom date But, API is not called and app getting stuck.
I checked in iOS 10 and 11, but not working
Handling push is like this.
AppDelegate
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
redirectToScreen(notificaiton: userInfo)
completionHandler(UIBackgroundFetchResult.newData)
}
//Notify through Notification center
func redirectToScreen(notificaiton: [AnyHashable: Any]) {
let dictPayload = notificaiton as NSDictionary
print(dictPayload)
if let type = dictPayload.value(forKey: "type") as? String {
var dict = ["type" : type]
NotificationCenter.default.post(name: NSNotification.Name.init(rawValue: "handlePush"), object: dict)
}
}
HomeViewController
//Notification Observer goes here and call API
let spinner = showLoader(view: self.view) // App goes stuck here and loaded process continuously, response is not getting
Alamofire.request(kURl, method: .post, parameters: param, encoding: URLEncoding.httpBody, headers: nil).authenticate(user: R.string.keys.basicAuthUsername(), password: R.string.keys.basicAuthPassword()).responseSwiftyJSON(completionHandler: {
spinner.dismissLoader()
})
Swift 4.0
As per #TarasChernyshenko statement, I put Post Notification code in DispatchQueue.main.async { } block and now it works fine.
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name.init(rawValue: "handlePush"), object: dict)
}
As mentioned by #TarasChernyshenko,
When you get callback from Notification Observer via didRecieveNotification(_:), app remains in background. Any UI updates such as :
let spinner = showLoader(view: self.view)
Must keep in Main thread queue as given below :
DispatchQueue.main.async {
let spinner = showLoader(view: self.view)
//other ui stuffs...
}
I am using firebase notification. When applicationState is background it's working fine. But when app is not running / terminated func getPushNotiData(_ notification: NSNotification) is not being call
Implementing NotificationCenter in appDelegate file to handle notification in dashView
func application(_ application: UIApplication, didReceiveRemoteNotification
userInfo: [AnyHashable : Any]) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue:
"getPushNotificationData"), object: nil, userInfo: userInfo)
}
And if app is not running / terminated Implementing NotificationCenter in didFinishLaunchingWithOptions delegate method
if (launchOptions != nil)
{
let userInfo = launchOptions![.remoteNotification] as? [AnyHashable: Any]
if userInfo != nil {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "getPushNotificationData"), object: nil, userInfo: userInfo)
}
}
Handling NotificationCenter in dashView
override func viewDidAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(self, selector: #selector(self.getPushNotiData(_:)), name: NSNotification.Name(rawValue: "getPushNotificationData"), object: nil)
}
#objc func getPushNotiData(_ notification: NSNotification){
if let url = notification.userInfo!["url"] as? String {
let destination = storyboard?.instantiateViewController(withIdentifier: "NotificationDlsViewController") as! NotificationDlsViewController
destination.notiUrl = url
self.navigationController?.pushViewController(destination, animated: true)
}
}
If the app is not running (killed state) , you can't execute any methods or code , only if the user clicks the push notification you can receive object of it in didFinishLaunchingWithOptions launchingOptions and handle it ....
It's probabile that when you check launchOptions in your didFinishLaunchingWithOptions your ViewController is not loaded yet so viewDidAppear has not been called and the ViewController is not registered in the Notification Center. Try to put some print statement and check the order of the calls when the app is launched from the notification.
I am having a bit of trouble getting an in-app badge icon label to be updated correctly. I want something that does this:
This red icon appears anytime a push notification is received. I get the push notification badge on the app icon on the iPhone correctly; however, this red icon inside the app only appears if I press on the banner for the push notification, OR if I'm already inside the app.
My problem is that it does not appear if I press on the actual app icon. I'd like for the label within the app to be updated even when the app is in the background kind of like how the facebook app has icons on top of the notifications globe icon.
I'll show the relevant methods in the AppDelegate (omitting tokens, etc):
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let userInfo: AnyObject? = launchOptions?[UIApplicationLaunchOptionsRemoteNotificationKey]
if userInfo != nil {
handleRemoteNotifications(application, userInfo: userInfo! as! NSDictionary)
return true
}
if application.applicationState != UIApplicationState.Background {
let oldPushHandlerOnly = !self.respondsToSelector(Selector("application:didReceiveRemoteNotification:fetchCompletionHandler:"))
let noPushPayload: AnyObject? = launchOptions?[UIApplicationLaunchOptionsRemoteNotificationKey]
if oldPushHandlerOnly || noPushPayload != nil {
PFAnalytics.trackAppOpenedWithLaunchOptionsInBackground(launchOptions, block: nil)
}
}
return true
}
func handleRemoteNotifications(application: UIApplication, userInfo: NSDictionary) {
if let type: String = userInfo["type"] as? String {
switch (type) {
case "follow":
NSNotificationCenter.defaultCenter().postNotificationName("commentNotification", object: self)
case "comment":
NSNotificationCenter.defaultCenter().postNotificationName("commentNotification", object: self)
default:
return
}
}
}
func applicationDidBecomeActive(application: UIApplication) {
if (application.applicationIconBadgeNumber != 0) {
application.applicationIconBadgeNumber = 0
}
let installation = PFInstallation.currentInstallation()
if installation.badge != 0 {
installation.badge = 0
installation.saveEventually()
}
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler handler: (UIBackgroundFetchResult) -> Void) {
if let badgeNumber: Int = userInfo["badge"] as? Int {
application.applicationIconBadgeNumber = badgeNumber
}
handleRemoteNotifications(application, userInfo: userInfo)
if application.applicationState == .Inactive {
PFAnalytics.trackAppOpenedWithRemoteNotificationPayloadInBackground(userInfo, block: nil)
}
handler(.NewData)
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
if application.applicationState == .Inactive {
// The application was just brought from the background to the foreground,
// so we consider the app as having been "opened by a push notification."
PFAnalytics.trackAppOpenedWithRemoteNotificationPayloadInBackground(userInfo, block: nil)
handleRemoteNotifications(application, userInfo: userInfo)
}
}
In the ViewController, I am calling the methods in viewDidAppear and making it update the label and increase the number by 1 each time a push notification is received:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
followLabelNumber = NSUserDefaults.standardUserDefaults().integerForKey("followNumberKey")
commentLabelNumber = NSUserDefaults.standardUserDefaults().integerForKey("commentNumberKey")
NSNotificationCenter.defaultCenter().removeObserver(self, name: "followNotification", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector:"followNotificationReceived:", name:"followNotification", object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: "commentNotification", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector:"commentNotificationReceived:", name:"commentNotification", object: nil)
self.navigationController?.navigationBarHidden = true
}
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
func followNotificationReceived(notification: NSNotification) {
if let number = followLabelNumber {
let aNumber = makeIncrementer(forIncrement: 1)
followLabelNumber = number + aNumber()
NSUserDefaults.standardUserDefaults().setInteger(followLabelNumber!, forKey: "followNumberKey")
NSUserDefaults.standardUserDefaults().synchronize()
profileNotificationLabel.hidden = false
profileNotificationLabel.text = String(followLabelNumber!)
hasReceivedFollowNotification = true
}
}
func commentNotificationReceived(notification: NSNotification) {
if let number = commentLabelNumber {
let aNumber = makeIncrementer(forIncrement: 1)
commentLabelNumber = number + aNumber()
NSUserDefaults.standardUserDefaults().setInteger(commentLabelNumber!, forKey: "commentNumberKey")
NSUserDefaults.standardUserDefaults().synchronize()
commentsNotificationLabel.hidden = false
commentsNotificationLabel.text = String(commentLabelNumber!)
hasReceivedCommentNotification = true
}
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
I'd greatly appreciate anyone's help as I've been stuck on this for a few days now.
EDIT: changed title
Firstly, you shouldn't need to handle application.applicationIconBadgeNumber yourself since you're using Parse:
//You don't need these...
if (application.applicationIconBadgeNumber != 0) {
application.applicationIconBadgeNumber = 0
}
//Because these lines will set the application's icon badge to zero
let installation = PFInstallation.currentInstallation()
if installation.badge != 0 {
installation.badge = 0
installation.saveEventually()
}
You don't need this as well:
//Because Parse handles that for you
if let badgeNumber: Int = userInfo["badge"] as? Int {
application.applicationIconBadgeNumber = badgeNumber
}
Also, the problem seems to be that you're not updating the button's badge when your View Controller loads. You're only updating them when you receive a new notification AND the View Controller is visible. In short, try this on your View Controller:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
followLabelNumber = NSUserDefaults.standardUserDefaults().integerForKey("followNumberKey")
commentLabelNumber = NSUserDefaults.standardUserDefaults().integerForKey("commentNumberKey")
//BEGIN SUGGESTED CODE
profileNotificationLabel.hidden = followLabelNumber > 0
profileNotificationLabel.text = String(followLabelNumber!)
commentsNotificationLabel.hidden = commentLabelNumber > 0
commentsNotificationLabel.text = String(commentLabelNumber!)
//END SUGGESTED CODE
NSNotificationCenter.defaultCenter().removeObserver(self, name: "followNotification", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector:"followNotificationReceived:", name:"followNotification", object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: "commentNotification", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector:"commentNotificationReceived:", name:"commentNotification", object: nil)
self.navigationController?.navigationBarHidden = true
}
Last, but definitely not least, whenever you receive a remote notification, you're passing it to func handleRemoteNotifications(application: UIApplication, userInfo: NSDictionary) inside your AppDelegate. This in turn posts an NSNotification to objects that are listening to it. However, there may or may not be a ViewController, because it might have been deallocated while the app was in the background. Thus, these lines of code never get called when you receive a remote notification:
NSUserDefaults.standardUserDefaults().setInteger(followLabelNumber!, forKey: "followNumberKey")
NSUserDefaults.standardUserDefaults().synchronize()
Try moving the lines above to your AppDelegate's func handleRemoteNotifications(application: UIApplication, userInfo: NSDictionary) method.
I am listneing to actions pressed for my local notifications, but is there a way to determine when the user dismisses a notification?
Here is how I'm listening to my actions in my AppDelegate, but the dismiss doesn't fire this:
func application(application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, completionHandler: () -> Void) {
var actionName: String? = nil
if let identifier = identifier {
switch identifier {
case "snoozeAction":
actionName = "snoozeActionTapped"
break
default: break
}
if let name = actionName {
NSNotificationCenter.defaultCenter().postNotificationName(name, object: nil)
}
}
completionHandler()
}
Dismissing a notification does not wake your application up so there is no way to capture this.
You should try this:
func application(application: UIApplication!,
handleActionWithIdentifier identifier:String!,
forLocalNotification notification:UILocalNotification!,
completionHandler: (() -> Void)!){
if (identifier == "FIRST_ACTION"){
NSNotificationCenter.defaultCenter().postNotificationName("actionOnePressed", object: nil)
}else if (identifier == "SECOND_ACTION"){
NSNotificationCenter.defaultCenter().postNotificationName("actionTwoPressed", object: nil)
}
completionHandler()
}