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)
Related
When app is running and it receive push notification then didReceive is called.
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void
)
So when above delegate is called then i present a screen using the payload i receive. There is no problem here.
When app is not running and user tap the notification then it should present the same screen like above. It's not working because i didn't added a code in didFinishLaunchingWithOptions.
So, then i added the following code -
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
......
if let userInfo = launchOptions?[UIApplication.LaunchOptionsKey.remoteNotification] as? [AnyHashable: Any] {
......
}
return true
}
But this is not working and i cannot debug because when in debug mode i have to kill the app from background and tap the notification but in this case the debugger won't work. I tried alternative method i.e. showing alert but then alert is also not working
let aps = remoteNotif["aps"] as? [AnyHashable: Any]
let string = "\n Custom: \(String(describing: aps))"
let string1 = "\n Custom: \(String(describing: remoteNotif))"
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
if var topController = application.windows.first?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
let ac = UIAlertController(title: string1, message: string, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
topController.present(ac, animated: true)
}
}
How should i solve this problem ?
“but in this case the debugger won't work” Not true! You can attach the debugger on launch even though Xcode did not launch it.
Edit the scheme, and in the Run action, under Info, where it says Launch, click the second radio button: “Wait for the executable to be launched.” Run the app; it doesn’t launch. Now launch the app through the push notification. The debugger works.
I have solved it by implementing sceneDelegate willConnectTo method. There is no need to handle it in didFinishLaunchingWithOptions
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
//Remote notification response
if let response = connectionOptions.notificationResponse{
print(response.notification.request.content.userInfo)
}
....
}
This is enough
You will have to create a NotificationServiceExtension and handle the payload there
In XCode,
Select your project
From bottom left select Add Target
Add Notification Service Extension
And then try doing something like this. The below code is for FCM but you can amend it according to your own payload.
For example -
import UserNotifications
import UIKit
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
// Modify the notification content here...
bestAttemptContent.title = "\(bestAttemptContent.title)"
guard let fcmOptions = bestAttemptContent.userInfo["fcm_options"] as? [String: Any] else {
contentHandler(bestAttemptContent)
return
}
guard let imageURLString = fcmOptions["image"] as? String else {
contentHandler(bestAttemptContent)
return
}
getMediaAttachment(for: imageURLString) { [weak self] (image, error) in
guard let self = self,
let image = image,
let fileURL = self.saveImageAttachment(image: image, forIdentifier: "attachment.png")
else {
// bestAttemptContent.body = "Error - \(String(describing: error))"
contentHandler(bestAttemptContent)
return
}
let imageAttachment = try? UNNotificationAttachment(
identifier: "image",
url: fileURL,
options: nil)
if let imageAttachment = imageAttachment {
bestAttemptContent.attachments = [imageAttachment]
}
contentHandler(bestAttemptContent)
}
}
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
private func saveImageAttachment(image: UIImage, forIdentifier identifier: String) -> URL? {
let tempDirectory = URL(fileURLWithPath: NSTemporaryDirectory())
let directoryPath = tempDirectory.appendingPathComponent(
ProcessInfo.processInfo.globallyUniqueString,
isDirectory: true)
do {
try FileManager.default.createDirectory(
at: directoryPath,
withIntermediateDirectories: true,
attributes: nil)
let fileURL = directoryPath.appendingPathComponent(identifier)
guard let imageData = image.pngData() else {
return nil
}
try imageData.write(to: fileURL)
return fileURL
} catch {
return nil
}
}
private func getMediaAttachment(for urlString: String, completion: #escaping (UIImage?, Error?) -> Void) {
guard let url = URL(string: urlString) else {
completion(nil, NotificationError.cannotParseURL)
return
}
ImageDownloader.shared.downloadImage(forURL: url) { (result) in
switch result {
case .success(let image):
completion(image, nil)
case .failure(let error):
completion(nil, error)
}
}
}
}
enum NotificationError: Error {
case cannotParseURL
}
I am calling this function below in the simulator to simulate background fetch.
Then I get this warning in the log:
Swift Warning: Application delegate received call to -application:performFetchWithCompletionHandler: but the completion handler was never called.
I have seen other Stack Iverflow answers say I just need to add completionhandler(). I've tried this and it says I need to add a parameter and that's where I am lost.
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler:#escaping (UIBackgroundFetchResult) -> Void) {
let db = Firestore.firestore()
guard let uid = Auth.auth().currentUser?.uid else { return }
//check if user online
let docRef = db.collection("Users").document(uid)
docRef.getDocument { (document, error) in
if let document = document {
if document.exists {
guard let dictionary = document.data() else { return }
guard let onlineOfflineStatus = dictionary["Online Offline Status"] as? String else { return }
// if online create value to set offline an alert
if onlineOfflineStatus == "Online" {
print("user is Online and inactive, will set value to trigger notification asking if they would like to go offline")
db.collection("sendGoOffline").document(uid).setData(["OfflineAlert" : 1200], completion: { (error) in
if let error = error {
print("there was an error", error)
}
})
}
}
}
if let error = error {
print("failed to fetch user", error)
}
}
}
The warning is telling you to add this method:
completionHandler(argument)
where the argument is one of the following:
UIBackgroundFetchResult.noData
UIBackgroundFetchResult.newData
UIBackgroundFetchResult.failed
The purpose is to tell the system that you are done.
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler:#escaping (UIBackgroundFetchResult) -> Void) {
// do backgound data fetch
// process it
// finished
completionHandler(UIBackgroundFetchResult.newData)
}
Read more here:
documentation
old but good article
UIBackgroundFetchResult
Remote notification related articles
How to handle remote notification with background mode enabled
Multiple scenarios with Push Notification in iOS
application(_:didReceiveRemoteNotification:fetchCompletionHandler:)
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
})
}
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 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()
}