Update 2018-05-25:
I replaced datatask with downloadTask after reading Rob's answer here: https://stackoverflow.com/a/44140059/4666760 . It still does not work when the app is backgrounded.
Hello
I need some help with iOS background tasks. I want to use Apple Push Notification service (APNs) to wake up my app in the background so that it can do a simple RESTful API call to my server. I am able to make it work when the app is in the foreground, but not in the background. I think I do something wrong with the configuration of the URLSession, but I don't know. The entire code for the app and the server is at my repo linked below. Please, clone it and do whatever you like - I just want your help :)
https://github.com/knutvalen/ping
In AppDelegate.swift the app listen for remote notifications:
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
// MARK: - Properties
var window: UIWindow?
// MARK: - Private functions
private func registerForPushNotifications() {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
guard granted else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
// MARK: - Delegate functions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
Login.shared.username = "foo"
registerForPushNotifications()
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenParts = deviceToken.map { data -> String in
return String(format: "%02.2hhx", data)
}
let token = tokenParts.joined()
os_log("AppDelegate application(_:didRegisterForRemoteNotificationsWithDeviceToken:) token: %#", token)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
os_log("AppDelegate application(_:didFailToRegisterForRemoteNotificationsWithError:) error: %#", error.localizedDescription)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
os_log("AppDelegate application(_:didReceiveRemoteNotification:fetchCompletionHandler:)")
if let aps = userInfo["aps"] as? [String: AnyObject] {
if aps["content-available"] as? Int == 1 {
RestController.shared.onPing = { () in
RestController.shared.onPing = nil
completionHandler(.newData)
os_log("AppDelegate onPing")
}
RestController.shared.pingBackground(login: Login.shared)
// RestController.shared.pingForeground(login: Login.shared)
}
}
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
RestController.shared.backgroundSessionCompletionHandler = completionHandler
}
}
The RestController.swift handles URLSessions with background configurations:
class RestController: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate {
// MARK: - Properties
static let shared = RestController()
let identifier = "no.qassql.ping.background"
let ip = "http://123.456.7.89:3000"
var backgroundUrlSession: URLSession?
var backgroundSessionCompletionHandler: (() -> Void)?
var onPing: (() -> ())?
// MARK: - Initialization
override init() {
super.init()
let configuration = URLSessionConfiguration.background(withIdentifier: identifier)
configuration.isDiscretionary = false
configuration.sessionSendsLaunchEvents = true
backgroundUrlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
// MARK: - Delegate functions
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
if let completionHandler = self.backgroundSessionCompletionHandler {
self.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
os_log("RestController urlSession(_:task:didCompleteWithError:) error: %#", error.localizedDescription)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let data = try Data(contentsOf: location)
let respopnse = downloadTask.response
let error = downloadTask.error
self.completionHandler(data: data, response: respopnse, error: error)
} catch {
os_log("RestController urlSession(_:downloadTask:didFinishDownloadingTo:) error: %#", error.localizedDescription)
}
}
// MARK: - Private functions
private func completionHandler(data: Data?, response: URLResponse?, error: Error?) {
guard let data = data else { return }
if let okResponse = OkResponse.deSerialize(data: data) {
if okResponse.message == ("ping_" + Login.shared.username) {
RestController.shared.onPing?()
}
}
}
// MARK: - Public functions
func pingBackground(login: Login) {
guard let url = URL(string: ip + "/ping") else { return }
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 20)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = login.serialize()
if let backgroundUrlSession = backgroundUrlSession {
backgroundUrlSession.downloadTask(with: request).resume()
}
}
func pingForeground(login: Login) {
guard let url = URL(string: ip + "/ping") else { return }
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = login.serialize()
URLSession.shared.dataTask(with: request) { (data, response, error) in
return self.completionHandler(data: data, response: response, error: error)
}.resume()
}
}
By adding App provides Voice over IP services as Required Background Mode in info.plist and using PushKit to handle the APNs payloads I were able to do what I wanted. A SSCCE (example) is available at my repository:
https://github.com/knutvalen/ping
Related
I working now on the notification system for my app.
I used This func to send Notification to specific token device:
func sendPushNotification(to token: String, title: String, body: String) {
let urlString = "https://fcm.googleapis.com/fcm/send"
let url = NSURL(string: urlString)!
let paramString: [String : Any] = ["to" : token,
"content-available": 1,
"priority":"high",
"notification" : ["title" : title, "body" : body],
"data" : ["user" : "test_id"]
]
let request = NSMutableURLRequest(url: url as URL)
request.httpMethod = "POST"
request.httpBody = try? JSONSerialization.data(withJSONObject:paramString, options: [.prettyPrinted])
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("key=My Key", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request as URLRequest) { (data, response, error) in
do {
if let jsonData = data {
if let jsonDataDict = try JSONSerialization.jsonObject(with: jsonData, options: JSONSerialization.ReadingOptions.allowFragments) as? [String: AnyObject] {
NSLog("Received data:\n\(jsonDataDict))")
}
}
} catch let err as NSError {
print("Error Notification: \(err.debugDescription)")
}
}
task.resume()
}
I got the notification like I expected but when the app at background I get the Notification alert but 'didReceiveRemoteNotification' not called even when I added ' "content-available": 1 ' to my Notification params.
My Capability Settings
AppDelegate:
import UIKit
import Firebase
import FirebaseMessaging
import UserNotifications
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate, UNUserNotificationCenterDelegate {
var userDeviceToken = ""
var userModel: UserModel?
var userID = ""
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// check if have user
self.userModel = KeychainUserModel.shared.retrive(key: .user)
if let userID = self.userModel?.userID {
self.userID = userID
KeychainModel.shared.saveString(value: userID, key: .userID)
}
FirebaseApp.configure()
Messaging.messaging().delegate = self
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { sccsess, _ in
guard sccsess else {
return
}
print("Success in APNS Registry")
}
application.registerForRemoteNotifications()
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
//MARK: - notification
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
self.userDeviceToken = deviceToken.hexString
let userToken = KeychainModel.shared.retrieveString(key: .userDeviceToken) ?? ""
let userID = KeychainModel.shared.retrieveString(key: .userID) ?? ""
Messaging.messaging().token { stringToken, error in
print("Firebase Token: \(stringToken)")
}
KeychainModel.shared.saveString(value: self.userDeviceToken, key: .userDeviceToken)
print("self.userDeviceToken: \(self.userDeviceToken), userTokenKeychain: \(userToken)")
// if have new device token need to update the deviceToken at "userModel"
if self.userDeviceToken != userToken && userID != "" {
let updateSet: [String: String] = [FirestoreModel.userParams.deviceToken.rawValue : self.userDeviceToken]
FirestoreModel.shared.generalUpdate(collectionName: .users, documentName: userID, updateValue: updateSet) { error in
if let error = error {
print("ERROR update token device: \(error)")
} else {
print("update token device successed")
}
}
}
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("didFailToRegisterForRemoteNotificationsWithError: \(error) ")
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let data = userInfo["user"] as? String
print("GET NOTIFICATION, userID: \(data)")
}
}
Thanks.
I have a WKWebview applying AWS Cognito.
Every request to the server has to be added Authorization into request header.
let access_token = "Bearer \(key)"
let header: [String: String] = [
"Authorization": access_token
]
if let url = URL(string: "https://myserverdomain.amazonaws.com/api/v3/graphs?date=2020-08-28") {
var request: URLRequest = URLRequest(url: url)
request.allHTTPHeaderFields = header
wkWebview.load(request)
}
With this code, I already can load the page content but CSS in the page. I checked with chrome (using ModHeader chrome extension to add header) and it works, show correctly, also Android.
I inspected by Chrome and the CSS link in < head > tag like this, it is not the same folder with the HTML file (I don't know if it is the reason).
<link rel="stylesheet" type="text/css" href="https://myserverdomain.amazonaws.com/assets/graphs/style.css"></script>
I can load the css content only with the code:
let access_token = "Bearer \(key)"
let header: [String: String] = [
"Authorization": access_token
]
if let url = URL(string: "https://myserverdomain.amazonaws.com/assets/graphs/style.css") {
var request: URLRequest = URLRequest(url: url)
request.allHTTPHeaderFields = header
wkWebview.load(request)
}
UIWebview was deprecated, Is there any way to set WKWebview with a global header as always?
Thank you for your help.
You can redirect all webview's requests to your URLSession with your configuration. To do that you can register your custom URLProtocol for https scheme. There is a hack for WKWebView to intercept url requests with WKBrowsingContextController private class and your URLProtocol implementation e.g.:
class MiddlewareURLProtocol : URLProtocol {
static let handledKey = "handled"
lazy var session : URLSession = {
// Config your headers
let configuration = URLSessionConfiguration.default
//configuration.httpAdditionalHeaders = ["Authorization" : "..."]
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
var sessionTask : URLSessionTask?
override var task: URLSessionTask? {
return sessionTask
}
static func registerClass() {
let sel = NSSelectorFromString("registerSchemeForCustomProtocol:")
if let cls = NSClassFromString("WKBrowsingContextController") as? NSObject.Type, cls.responds(to:sel) {
// Register https protocol
cls.perform(sel, with: "https")
}
URLProtocol.registerClass(Self.self)
}
override class func canInit(with request: URLRequest) -> Bool {
return URLProtocol.property(forKey: Self.handledKey, in: request) == nil
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override class func requestIsCacheEquivalent(_ a: URLRequest, to b: URLRequest) -> Bool {
super.requestIsCacheEquivalent(a, to: b)
}
override func startLoading() {
let redirect = (request as NSURLRequest).mutableCopy() as! NSMutableURLRequest
URLProtocol.setProperty(true, forKey: Self.handledKey, in: redirect)
sessionTask = session.dataTask(with: redirect as URLRequest)
task?.resume()
}
override func stopLoading() {
task?.cancel()
}
}
extension MiddlewareURLProtocol : URLSessionDataDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let err = error {
client?.urlProtocol(self, didFailWithError: err)
}
else {
client?.urlProtocolDidFinishLoading(self)
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
client?.urlProtocol(self, didLoad: data)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: #escaping (CachedURLResponse?) -> Void) {
completionHandler(proposedResponse)
}
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
let redirect = (request as NSURLRequest).mutableCopy() as! NSMutableURLRequest
Self.removeProperty(forKey: Self.handledKey, in: redirect)
client?.urlProtocol(self, wasRedirectedTo: redirect as URLRequest, redirectResponse: response)
self.task?.cancel()
let error = NSError(domain: NSCocoaErrorDomain, code: CocoaError.Code.userCancelled.rawValue, userInfo: nil)
client?.urlProtocol(self, didFailWithError: error)
}
}
Just register your protocol on app start to handle all requests:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
MiddlewareURLProtocol.registerClass()
...
}
NOTE: To prevent Apple static checks for private classes you can store class names in the array:
let className = ["Controller", "Context", "Browsing", "WK"].reversed().joined()
I want to implement push notification using Firebase Cloud Messaging
I have setup my project and uploaded APN certificate as explained
and I am sending Test messages using fcmtoken to my real device
my configuration is as follows in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
registerForPushNotifications(app: application)
return true
}
func registerForPushNotifications(app: UIApplication) {
UNUserNotificationCenter.current().delegate = self
Messaging.messaging().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { (authorized, error) in
if let error = error {
print(error.localizedDescription)
return
}
if authorized {
print("authorized")
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
} else {
print("denied")
}
}
app.registerForRemoteNotifications()
}
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
print("Firebase registration token: \(fcmToken)")
let dataDict:[String: String] = ["token": fcmToken]
NotificationCenter.default.post(name: Notification.Name("FCMToken"), object: nil, userInfo: dataDict)
// TODO: If necessary send token to application server.
// Note: This callback is fired at each app startup and whenever a new token is generated.
}
func application(_ application: UIApplication,
didReceiveRemoteNotification notification: [AnyHashable : Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
print("notofication arrivied")
if Auth.auth().canHandleNotification(notification) {
completionHandler(.noData)
return
}
// This notification is not auth related, developer should handle it.
}
it is supposed to see notofication arrivied but it didn't Also set a beak point It seems this part is never being excused thus message is not coming
I don’t see this in your AppDelegate unless you have Swizzling enabled
func application(application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
Messaging.messaging().apnsToken = deviceToken
}
This code maps your APNs device token to FCM token, which is necessary because APNs token is the only way you can send a push notification.
func sendPushNotification(to token: String, title: String, body: String, userInfo: [String: Any]) {
let payload: [String: Any] = ["title": title, "body": body, "sound": "sound.caf"]
let paramString: [String: Any] = ["to": token, "notification": payload, "data": userInfo]
let urlString = "https://fcm.googleapis.com/fcm/send"
let url = NSURL(string: urlString)!
let request = NSMutableURLRequest(url: url as URL)
request.httpMethod = "POST"
request.httpBody = try? JSONSerialization.data(withJSONObject:paramString, options: [.prettyPrinted])
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("key=\(Keys.gmsServerKey)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request as URLRequest) { (data, response, error) in
do {
if let data = data {
if let object = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? [String: Any] {
NSLog("Received data: \(object))")
}
}
} catch let err as NSError {
print(err.debugDescription)
}
}
task.resume()
}
I'm using Xcode Version 9.4.1 (9F2000).
I have this code in AppDelegate.swift:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
showPushButtons()
return true
}
func httpRequest(file: String, postKey1: String, postValue1: String, postKey2: String, postValue2: String) {
let url = URL(string: "https://www.example.com/\(file)")!
var request = URLRequest(url: url)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
let postString = "\(postKey1)=\(postValue1)&\(postKey2)=\(postValue2)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print("error=\(String(describing: error))")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(String(describing: response))")
}
let responseString = String(data: data, encoding: .utf8)
print("responseString = \(String(describing: responseString))")
}
task.resume()
}
func showPushButtons(){
let replyAction = UNTextInputNotificationAction(
identifier: "reply.action",
title: "Reply to message",
textInputButtonTitle: "Send",
textInputPlaceholder: "Write some text here")
let pushNotificationButtons = UNNotificationCategory(
identifier: "allreply.action",
actions: [replyAction],
intentIdentifiers: [],
options: [])
UNUserNotificationCenter.current().setNotificationCategories([pushNotificationButtons])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
if response.actionIdentifier == "reply.action" {
if let textResponse = response as? UNTextInputNotificationResponse {
let sendText = textResponse.userText
print("Received text message: \(sendText)")
httpRequest(file: "message.php", postKey1: "message", postValue1: "Hello!", postKey2: "chat_user", postValue2: "Peter")
}
}
completionHandler()
}
What it does:
When receiving a push notification and making a force touch, a textfield and the keyboard will appear (as known from messaging apps like WhatsApp). You can write some text and submit/send it.
You can get and print that submitted message with this line:
print("Received text message: \(sendText)")
This is working without any problems.
But when trying to send the data to my server like this:
httpRequest(file: "message.php", postKey1: "message", postValue1: "Hello!", postKey2: "chat_user", postValue2: "David")
it's not working. There's no access to my server and I'm getting errors like this in console log:
Received text message: First try
2018-07-19 08:45:00.643935+0200 MyApp[4307:1502538] +[CATransaction
synchronize] called within transaction
2018-07-19 08:45:00.644639+0200 MyApp[4307:1502538] +[CATransaction
synchronize] called within transaction
2018-07-19 08:45:13.091958+0200 MyApp[4307:1502647] TIC TCP Conn
Failed [1:0x1c4169a80]: 1:50 Err(50)
2018-07-19 08:45:13.093089+0200 MyApp[4307:1502647] Task
<1E8151BB-7098-46CD-9F68-8AA0E320CB7D>.<1> HTTP load failed (error
code: -1009 [1:50])
Received text message: Second try
2018-07-19 08:45:13.094756+0200 MyApp[4307:1503029] Task
<1E8151BB-7098-46CD-9F68-8AA0E320CB7D>.<1> finished with error - code:
-1009
2018-07-19 08:45:13.096208+0200 MyApp[4307:1502538] +[CATransaction
synchronize] called within transaction
2018-07-19 08:45:13.096580+0200 MyApp[4307:1502538] +[CATransaction
synchronize] called within transaction error=Optional(Error
Domain=NSURLErrorDomain Code=-1009 "The Internet connection appears to
be offline." UserInfo={NSUnderlyingError=0x1cc047320 {Error
Domain=kCFErrorDomainCFNetwork Code=-1009 "(null)"
UserInfo={_kCFStreamErrorCodeKey=50, _kCFStreamErrorDomainKey=1}},
NSErrorFailingURLStringKey=https://www.example.com/message.php,
NSErrorFailingURLKey=https://www.example.com/message.php,
_kCFStreamErrorDomainKey=1, _kCFStreamErrorCodeKey=50, NSLocalizedDescription=The Internet connection appears to be
offline.})
My function httpRequest() seems to work because I can e.g. call it from didFinishLaunchingWithOptions like this:
httpRequest(file: "message.php", postKey1: "message", postValue1: "Hello!", postKey2: "chat_user", postValue2: "David")
without any problem. That also means that my domain and my server are working fine.
But why can't I call my httpRequest() function from my UNUserNotificationCenter function?
When receiving a push notification, my app is of course in background or closed. Do I need some special code to make it work in background mode or so?
Here is my working code from AppDelegate.swift:
// AppDelegate.swift
import UIKit
import UserNotifications
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var window: UIWindow?
var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
pushAction()
return true
}
func registerForPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
(granted, error) in
print("\nPermission granted: \(granted)\n")
self.pushAction()
guard granted else { return }
self.getNotificationSettings()
}
}
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
print("\nNotification settings: \(settings)\n")
guard settings.authorizationStatus == .authorized else { return }
DispatchQueue.main.async(execute: {
UIApplication.shared.registerForRemoteNotifications()
})
}
}
func httpRequest(file: String, postKey1: String, postValue1: String, postKey2: String, postValue2: String) {
let url = URL(string: "https://www.example.com/\(file)")!
var request = URLRequest(url: url)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
let postString = "\(postKey1)=\(postValue1)&\(postKey2)=\(postValue2)"
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print("\nerror=\(String(describing: error))\n")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
print("\nstatusCode should be 200, but is \(httpStatus.statusCode)\n")
print("\nresponse = \(String(describing: response))\n")
}
let responseString = String(data: data, encoding: .utf8)
print("\nresponseString = \(String(describing: responseString))\n")
}
task.resume()
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenParts = deviceToken.map { data -> String in
return String(format: "%02.2hhx", data)
}
let token = tokenParts.joined()
print("\nDevice Token: \(token)\n")
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("\nFailed to register: \(error)\n")
}
func pushAction(){
let replyAction = UNTextInputNotificationAction(
identifier: "reply.action",
title: "Reply to message",
options:[],
textInputButtonTitle: "Send",
textInputPlaceholder: "Input/write text here")
let pushNotificationButtons = UNNotificationCategory(
identifier: "allreply.action",
actions: [replyAction],
intentIdentifiers: [],
options: [])
UNUserNotificationCenter.current().setNotificationCategories([pushNotificationButtons])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
// If you don’t want to show notification when app is open, do something else here and make a return here.
// Even if you don’t implement this delegate method, you will not see the notification on the specified controller. So, you have to implement this delegate and make sure the below line execute. i.e. completionHandler.
completionHandler([.sound,.alert,.badge])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
if response.actionIdentifier == "reply.action" {
if let textResponse = response as? UNTextInputNotificationResponse {
if UIApplication.shared.applicationState != .active{
self.registerBackgroundTask()
}
let sendText = textResponse.userText
print("\nReceived text message: \(sendText)\n")
DispatchQueue.global(qos: .background).async {
self.httpRequest(file: "message.php", postKey1: "message", postValue1: sendText, postKey2: "user", postValue2: "Peter")
}
}
}
completionHandler()
}
func registerBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
self?.endBackgroundTask()
}
assert(backgroundTask != UIBackgroundTaskInvalid)
}
func endBackgroundTask() {
print("\nBackground task ended.\n")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = UIBackgroundTaskInvalid
}
}
Don't forget:
– Create a push certificate
– You'll see your device token in console log
– Add "category":"allreply.action" in your aps payload like this:
{
"aps":{
"alert":{
"title":"Hello",
"body":"This is a test!"
},
"badge":0,
"sound":"default",
"category":"allreply.action"
}
}
Enable Push Notifications and Background Modes in Capabilities:
Big thank you to Raza K. from Freelancer.com!
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)
}
}