I am trying to fetch data by invoking a lambda function in response to a remote push notification while my app is in the background. My notifications are configured correctly and the didReceiveRemoteNotification is called while the app is in the background.
I have the following code in that method:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let lambdaInvoker = AWSLambdaInvoker.default()
lambdaInvoker.invokeFunction("lambdaFunctionName", jsonObject: jsonObject).continueWith(block: {(task:AWSTask<AnyObject>) -> Any? in
if let error = task.error as NSError? {
print(task.error!.localizedDescription)
print(task.error!)
DispatchQueue.main.async(execute: {
if (error.domain == AWSLambdaInvokerErrorDomain) && (AWSLambdaInvokerErrorType.functionError == AWSLambdaInvokerErrorType(rawValue: error.code)) {
print("Function error: \(String(describing: error.userInfo[AWSLambdaInvokerFunctionErrorKey]))")
} else {
print("Error: \(error)")
}
})
return nil
}
// Handle response in task.result
DispatchQueue.main.async(execute: {
if let jsonArray = task.result as? NSArray {
// do stuff
}
})
return nil
})
}
However the block is not executed in the lambda function. I have not used background fetch before and not sure how to make this work with a lambda function.
What I was missing was the completion handler. After adding that, the code and block executes as expected:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let lambdaInvoker = AWSLambdaInvoker.default()
lambdaInvoker.invokeFunction("lambdaFunctionName", jsonObject: jsonObject).continueWith(block: {(task:AWSTask<AnyObject>) -> Any? in
if let error = task.error as NSError? {
print(task.error!.localizedDescription)
print(task.error!)
DispatchQueue.main.async(execute: {
if (error.domain == AWSLambdaInvokerErrorDomain) && (AWSLambdaInvokerErrorType.functionError == AWSLambdaInvokerErrorType(rawValue: error.code)) {
print("Function error: \(String(describing: error.userInfo[AWSLambdaInvokerFunctionErrorKey]))")
completionHandler(UIBackgroundFetchResult.newData)
} else {
print("Error: \(error)")
completionHandler(UIBackgroundFetchResult.newData)
}
})
return nil
}
// Handle response in task.result
DispatchQueue.main.async(execute: {
if let jsonArray = task.result as? NSArray {
// do stuff
completionHandler(UIBackgroundFetchResult.newData)
}
})
return nil
})
}
Related
I have integrated Push notification on to my project and for some reason the banner appears on when I'm in the background mood and not when I'm in the foreground. I'm sure I've missed something, my code as bellow. Any help would much appreciate.
import UserNotifications
class AppDelegate: UIResponder, UIApplicationDelegate,UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let center = UNUserNotificationCenter.current()
center.delegate = self //not sure this is appropriate
registerForPushNotifications()
}
func registerForPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
(granted, error) in
guard granted else { return }
self.getNotificationSettings()
}
}
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
print("Notification settings: \(settings)")
guard settings.authorizationStatus == .authorized else { return }
DispatchQueue.main.async(execute: {
UIApplication.shared.registerForRemoteNotifications()
})
}
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenParts = deviceToken.map { data -> String in
return String(format: "%02.2hhx", data)
}
let token = tokenParts.joined()
if let valueInDB = UserDefaults.standard.value(forKey: "Permission_Granted") {
UserDefaults.standard.setValue(token, forKey: "PUSH_TOKEN")
UserDefaults.standard.synchronize()
print("Device Push Token: \(token)")
}
This gets called every time I gets a notification despite if its running on foreground or background. Thus the batch icon gets updated.
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void)
{
print("Recived: \(userInfo)")
completionHandler(.newData)
self.onPushRecieved = MP3ViewController().catchPushNotifications
let notification = JSON(userInfo)
print(notification)
let mediaId = (notification["media_id"]).stringValue
print(mediaId)
var badgeCount = 0
var pushAvailable = false
if let pushCount = UserDefaults.standard.value(forKey: "PUSH_COUNT"){
badgeCount = pushCount as! Int
print(badgeCount)
badgeCount = badgeCount + 1
UIApplication.shared.applicationIconBadgeNumber = badgeCount
}
print(badgeCount)
UserDefaults.standard.setValue(badgeCount, forKey: "PUSH_COUNT")
UserDefaults.standard.synchronize()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
print(isOnBackGround)
if isOnBackGround {
if mediaId != nil{
DispatchQueue.main.async {
let mP3ViewController = storyboard.instantiateViewController(withIdentifier:"MP3ViewController") as! MP3ViewController
mP3ViewController.media_ID = mediaId
self.navigationController?.pushViewController(mP3ViewController, animated:true)
self.isOnBackGround = false
}
}
}else{
print("Running on foreground")
}
}
}
You may want to implement the following function, eg:
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Swift.Void) {
completionHandler([.badge, .alert, .sound])
}
of this delegate: UNUserNotificationCenterDelegate
I have following function to download JSON data in my SeachVC (UIViewController) which works perfect.
func downloadJSON(){
guard let url = URL(string: "myURL") else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let downloadedCurrencies = try JSONDecoder().decode([Currency].self, from: data)
// Adding downloaded data into Local Array
Currencies = downloadedCurrencies
} catch let jsonErr {
print("Here! Error serializing json", jsonErr)
}
}.resume()
}
To implement Background App Refresh, I added following functions into App Delegate;
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Background App Refresh Config
UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
return true
}
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let VC = window?.rootViewController as? SearchVC {
// Update JSON data
VC.downloadJSON()
completionHandler(.newData)
}
}
However, when I simulate Background App Refresh on the simulator, I get warning:
Warning: Application delegate received call to -application:performFetchWithCompletionHandler: but the completion handler was never called.
Where I am going to implement completion handler and how?
Thank you
You will need to move your downloading code from the view controller and into another class or at least modify you current background refresh method to instantiate the view controller if required. Background refresh can be triggered when your app hasn't been launched in the foreground, so the if let will fall through.
Consider the code in your question:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let VC = window?.rootViewController as? SearchVC {
// Update JSON data
VC.downloadJSON()
completionHandler(.newData)
}
}
If the if let... doesn't pass then you exit from the function without calling the completionHandler, so you get the runtime warning that the completion handler was not called.
You could modify your code to include a call to the completionHandler in an else case, but in this case no fetch will have taken place:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let VC = window?.rootViewController as? SearchVC {
// Update JSON data
VC.downloadJSON()
completionHandler(.newData)
} else {
completionHandler(.noData)
}
Or you could instantiate the view controller (or I would suggest another data fetching class) if required:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let vc = (window?.rootViewController as? SearchVC) ?? SearchVC()
// Update JSON data
vc.downloadJSON()
completionHandler(.newData)
}
You should also modify your downloadJSON function to include a completion handler argument, which you invoke when the JSON download is complete. This will let you call the background fetch completion handler once you have actually downloaded the data:
func downloadJSON(completion: ((Bool,Error?) -> Void )? = nil)) {
guard let url = URL(string: "myURL") else {
completion?(false, nil)
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard nil == err else {
completion?(false, err)
return
}
guard let data = data else {
completion?(false, nil)
return
}
do {
let downloadedCurrencies = try JSONDecoder().decode([Currency].self, from: data)
// Adding downloaded data into Local Array
Currencies = downloadedCurrencies
completion(true,nil)
} catch let jsonErr {
print("Here! Error serializing json", jsonErr)
completion?(false,jsonErr)
}
}.resume()
}
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let vc = (window?.rootViewController as? SearchVC) ?? SearchVC()
// Update JSON data
vc.downloadJSON() { (newData,error) in
if let err = error {
NSLog("Background fetch error: \(err.localizedDescription)")
completionHandler(.fail)
} else {
completionHandler(newData ? .newData:.noData)
}
}
}
Update September 2019
Note that iOS 13 introduces new background fetch and processing functionality. Refer to this WWDC session for more details
It's propably because you don't call the completionHandler at the else-case (which will never happen but the compiler doesn't know)
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let VC = window?.rootViewController as? SearchVC {
// Update JSON data
VC.downloadJSON()
completionHandler(.newData)
} else {
completionHandler(.failed)
}
}
I am downloading my events in background mode, for that I will receive a push and a silent Notification. As I have to download data when my app is in background mode, so I am using silent notification for downloading data through a web service and before that I am updating applicationIconBadgeNumber based on an increment value. When I am updating applicationIconBadgeNumber with my auto increased value all my push notifications are getting cleared in notification center. If I am not setting any applicationIconBadgeNumber they are remaining same. As I have posted my code below, please do let me know if I am missing out something.
var autoIncrementForNotification:Int = 0
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
UIApplication.shared.setMinimumBackgroundFetchInterval(60)
if #available(iOS 10, *) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {(accepted, error) in
if !accepted {
print("Notification access denied.")
} else {
application.registerForRemoteNotifications()
}
}
} else {
application.registerForRemoteNotifications(matching: [.badge, .sound, .alert])
}
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let deviceTokenString = deviceToken.reduce("", {$0 + String(format: "%02X", $1)})
UserDefaults.standard.removeObject(forKey: "Device_Token")
UserDefaults.standard.setValue(deviceTokenString, forKey: "Device_Token")
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Swift.Void){
let aps = userInfo["aps"] as! [String: AnyObject]
if ((aps["content-available"] as? NSString)?.integerValue == 1)
{
let type = (aps["type"] as? NSString)
if(type == Constants.action_type_Events)
{
if (application.applicationState == .inactive || application.applicationState == .background)
{
autoIncrementForNotification += 1
application.applicationIconBadgeNumber = autoIncrementForNotification
// Firing my Web service
completionHandler(UIBackgroundFetchResult.newData)
}
}
}
}
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void){
completionHandler(UIBackgroundFetchResult.newData)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("i am not available in simulator \(error)")
}
func applicationDidBecomeActive(_ application: UIApplication) {
application.applicationIconBadgeNumber = 0
autoIncrementForNotification = 0
}
Please try the following :
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void){
var contentAvailable: Int
let aps = userInfo["aps"] as! [String: AnyObject]
if let content = aps["content-available"] as? String {
contentAvailable = Int(content)!
} else {
contentAvailable = aps["content-available"] as! Int
}
if (contentAvailable == 1)
{
let type = (aps["type"] as? NSString)
if(type == Constants.action_type_Events)
{
if (application.applicationState == .inactive || application.applicationState == .background)
{
autoIncrementForNotification += 1
application.applicationIconBadgeNumber = autoIncrementForNotification
// Firing my Web service
completionHandler(UIBackgroundFetchResult.newData)
}
}
}
}
Reason :
If your Payload file is like this {"aps":{"content-available":1}} below condition is not satisfying, so everytime applicationIconBadgeNumber is becoming 0
if ((aps["content-available"] as? NSString)?.integerValue == 1)
If your Payload file is like this {"aps":{"content-available":"1"}} then your code will work fine.
I call setValue() in Background like this:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
let ref = FIRDatabase.database().reference().child("data")
let data: NSDictionary = [
"test" : "test"
]
ref.setValue(data, withCompletionBlock:{
(error, reference) in
if error != nil {
print(error!)
} else {
print("success")
semaphore.signal()
}
})
_ = semaphore.wait(timeout: .distantFuture)
completionHandler(.newData)
}
But completion block never called. Is it impossible to upload data in background?
Apple allows few of the features to be called in background
microphone
location updates
bluetooth accessory
Voip
keepSynced(true) did the trick for me, just add to your code
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
let ref = FIRDatabase.database().reference().child("data")
let data: NSDictionary = [
"test" : "test"
]
ref.keepSynced(true)
ref.setValue(data, withCompletionBlock:{
(error, reference) in
if error != nil {
print(error!)
} else {
print("success")
semaphore.signal()
}
})
_ = semaphore.wait(timeout: .distantFuture)
completionHandler(.newData)
I have a function to get data with json and i append all the data to an array. I try to create semaphore and wait until sending a signal to semaphore to continue but it doesn't work(I'm not sure if i do it correct or not), then i saw a question in Stackoverflow, answer was creating a completion handler like that
func application(application: UIApplication!, performFetchWithCompletionHandler completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
getUrunGrup(completionHandler)
}
so i changed my function like that
func getUrunGrup(completionHandler: ((UIBackgroundFetchResult) -> Void)!){
Alamofire.request(.GET, "http://213.136.86.160:27701/Thunder/DataService/GetUrunGrup")
.responseJSON {(request, response, jsonObj, error) in
if let jsonresult:NSDictionary = jsonObj as? NSDictionary{
if let result: AnyObject = jsonresult["Result"] {
let elementCount = result.count
for (var i = 0; i<elementCount; ++i){
if let name: AnyObject = result[i]["Adi"]!{
if let kod:AnyObject = result[i]["Kod"]!{
urunUstGrup.append(["Adi": "\(name)", "Kod": "\(kod)"])
println("getUrunGrup \(i)")
}
}
}
}
}
}
completionHandler(UIBackgroundFetchResult.NewData)
println("Background Fetch Complete")
}
But there is no answer for how should i call this function?
you have to pass your async function the handler to call later on,like this:
func application(application: UIApplication!, performFetchWithCompletionHandler completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
loadShows(completionHandler)
}
func loadShows(completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
//....
//DO IT
//....
completionHandler(UIBackgroundFetchResult.NewData)
println("Background Fetch Complete")
}
OR (cleaner way IMHO)
add an intermediate completionHandler
func application(application: UIApplication!, performFetchWithCompletionHandler completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
loadShows() {
completionHandler(UIBackgroundFetchResult.NewData)
println("Background Fetch Complete")
}
}
func loadShows(completionHandler: (() -> Void)!) {
//....
//DO IT
//....
completionHandler()
}