I'm trying to implement rich push notifications but I don't get the image at passed url to get displayed. When I open the notification I can only see title, subtitle and body. I'm following the tutorial at https://www.pluralsight.com/guides/creating-ios-rich-push-notifications as I found it suggested in other posts but I'm stuck at showing the image. I also read in one post that I shouldn't run the app but the NotificationExtension but when I do the app crashes when receiving the notification with
Message from debugger: Terminated due to signal 9 Program ended with
exit code: 0
message.
This is the print of userInfo when running the app instead :
[AnyHashable("attachment-url"):
https://firebasestorage.googleapis.com/v0/b/fix-it-b4b00.appspot.com/o/Prodotti%2FPrato.jpeg?alt=media&token=5d0fde09-2b86-45b0-a383-a11e7e8e241c,
AnyHashable("gcm.message_id"): 1560360808567562,
AnyHashable("productId"): 9290CEBE-393C-4285-BE7B-B9E2968A1AA0,
AnyHashable("aps"): {
alert = {
body = "Nuova promozione per articolo: Prato";
subtitle = "Negozio: vincenzo calia";
title = "Promozione negozio";
};
"content-available" = 1;
"mutable-content" = 1;
sound = true; }, AnyHashable("price"): 10.00, AnyHashable("gcm.notification.priority"): high,
AnyHashable("google.c.a.e"): 1]
I checked and the url is correct.
Could the problem be that the url is not in string format? I put a print to check it but I don't even get the print at beginning of 'didReceive' method. Can you see where is possibly going wrong?
As always Many thanks for your interest and time.
These are the function in NotificationService.swift :
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void) {
print("NotificationService: dide receive called")
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
func failEarly() {
contentHandler(request.content)
}
guard let content = (request.content.mutableCopy() as? UNMutableNotificationContent) else {
return failEarly()
}
guard let apnsData = content.userInfo["data"] as? [String: Any] else {
return failEarly()
}
guard let attachmentURL = apnsData["attachment-url"] as? String else {
print("url is not in string form")
return failEarly()
}
guard let imageData = NSData(contentsOf:NSURL(string: attachmentURL)! as URL) else { return failEarly() }
guard let attachment = UNNotificationAttachment.create(imageFileIdentifier: "image.gif", data: imageData, options: nil) else { return failEarly() }
content.attachments = [attachment]
contentHandler(content.copy() as! UNNotificationContent)
}
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)
}
}
extension UNNotificationAttachment {
static func create(imageFileIdentifier: String, data: NSData, options: [NSObject : AnyObject]?) -> UNNotificationAttachment? {
let fileManager = FileManager.default
let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString
let fileURLPath = NSURL(fileURLWithPath: NSTemporaryDirectory())
let tmpSubFolderURL = fileURLPath.appendingPathComponent(tmpSubFolderName, isDirectory: true)
do {
try fileManager.createDirectory(at: tmpSubFolderURL!, withIntermediateDirectories: true, attributes: nil)
let fileURL = tmpSubFolderURL?.appendingPathComponent(imageFileIdentifier)
try data.write(to: fileURL!, options: [])
let imageAttachment = try UNNotificationAttachment.init(identifier: imageFileIdentifier, url: fileURL!, options: options)
return imageAttachment
} catch let error {
print("error \(error)")
}
return nil
}
}
And this is function that sends the push notification:
static func sendTopicPushNotification(to topic: String, title: String, subtitle: String, body: String, dataUrl: String, productId: String, price: String) {
let serverKey = firebaseServerKey // AAAA8c3j2...
// let topic = "/topics/<your topic here>" // replace it with partnerToken if you want to send a topic
let url = NSURL(string: "https://fcm.googleapis.com/fcm/send")
let postParams: [String : Any] = [
"to": topic,
// "priority": "high",
// "content_available": true,
// "mutable_content": true,
"notification": [
// "badge" : 1, sendig the badge number, will cause aglitch
"body": body,
"title": title,
"subtitle": subtitle,
"text": "some text",
"sound" : true, // or specify audio name to play
"priority": "high",
"content_available": true,
"mutable_content": true
// "category" : "pushNotificationCategory" // "topicPushNotification"
// "click_action" : "🚀", // action when user click notification (categoryIdentifier)
],
"data" : [
"attachment-url": dataUrl,
"productId": productId,
"price": price
]
]
let request = NSMutableURLRequest(url: url! as URL)
request.httpMethod = "POST"
request.setValue("key=\(serverKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
do {
// request.httpBody = try JSONSerialization.data(withJSONObject: postParams, options: JSONSerialization.WritingOptions())
request.httpBody = try JSONSerialization.data(withJSONObject: postParams, options: [.prettyPrinted]) // working
print("sendTopicPushNotification : My paramaters: \(postParams)")
} catch {
print("sendTopicPushNotification : Caught an error: \(error)")
}
let task = URLSession.shared.dataTask(with: request as URLRequest) { (data, response, error) in
if let realResponse = response as? HTTPURLResponse {
if realResponse.statusCode != 200 {
print("sendTopicPushNotification : Not a 200 response : \(realResponse)")
}
print("sendTopicPushNotification : response : \(realResponse)")
}
if let postString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue) as String? {
print("sendTopicPushNotification : POST: \(postString)")
}
}
task.resume()
}
I'm finally able to show the picture with the notification. It might had to do with the fact that the tutorial I'm following is for swift 5, and I'm on swift 4.
I changed the didReceivecode and omitted the extension UNNotificationAttachmentcode and it's now displaying it correctly.
Next step is to add actions based on the category received in the payload, but I still have to figure out if that is possible without implementing also a Notification Content Extension, which for what I understood should be only to have a custom view to display the notification. Any hint will be very appreciated.
I hope this will help others as tutorials I found on the subject are a bit confusing and misleading at times. Also many thanks for up voting to the question.
NotificationServiceExtension didReceiveis now:
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void) {
print("NotificationService: dide receive called")
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
if let urlString = bestAttemptContent.userInfo["attachment-url"] as? String,
let data = NSData(contentsOf: URL(string:urlString)!) as Data? {
let path = NSTemporaryDirectory() + "attachment"
_ = FileManager.default.createFile(atPath: path, contents: data, attributes: nil)
do {
let file = URL(fileURLWithPath: path)
let attachment = try UNNotificationAttachment(identifier: "attachment", url: file,options:[UNNotificationAttachmentOptionsTypeHintKey : "public.jpeg"])
bestAttemptContent.attachments = [attachment]
} catch {
print(error)
}
}
contentHandler(bestAttemptContent)
}
Related
I am getting a crash reported in firebase related with - partial apply for closure. I tried answers on SO but they mainly says about use of [Weak self], I used it but still getting crash report of same. However not facing crash on my device.
Snapshot of crash report is this:
Related code of Network Manager is this:
//MARK: POST Methods for API Calls
func postRequest(urlString: String,
params:[String : Any],
view:UIView,
token:String,
success:#escaping (SuccessHandler),
failure:#escaping (ErrorHandler)) {
showProgressView(in: view)
let url = self.baseURL.appendingPathComponent(urlString)
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
if token != "" {
urlRequest.addValue("\(token)", forHTTPHeaderField: "token")
}
urlRequest.networkServiceType = .default
urlRequest.cachePolicy = .reloadRevalidatingCacheData
urlRequest.timeoutInterval = 100
urlRequest.httpShouldHandleCookies = true
urlRequest.httpShouldUsePipelining = false
urlRequest.allowsCellularAccess = true
do {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: params, options: [])
}
catch let error as NSError {
print(error.localizedDescription)
}
let task = defaultSession.dataTask(with: urlRequest, completionHandler: { [weak self] (data, response, error) -> () in
self?.hideProgressView()
guard error == nil else {
failure(error!)
return
}
let statusCode = (response as? HTTPURLResponse)?.statusCode
if urlString.contains("signin") {
let token = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "token")
UserDefaults.standard.setValue(token, forKey: "authToken")
}
switch statusCode {
case 200:
if let aData = data {
do {
let responseJSON = try JSONSerialization.jsonObject(with: aData, options: [])
success(responseJSON) // Line no 160 mentioned in crash report
}
catch let error as NSError {
failure(error)
}
}
case 404:
if let aData = data {
do {
let responseJSON = try JSONSerialization.jsonObject(with: aData, options: [])
let dictObj = responseJSON as? [String: Any] ?? [String: Any]()
let message = dictObj["message"] ?? ""
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : message])
failure(error)
}
catch let error as NSError {
failure(error)
}
}
default:
self?.logger.error("New Http Response Code")
}
})
task.resume()
}
Login Class method from where this api call is invocated:
func loginAPICall() {
email = txtFieldUsername.text ?? ""
let password = txtFieldPassword.text ?? ""
let params = ["username" : email,"password":password]
NetworkManager.shared().postRequest(urlString: API.signIn, params: params, view: self.view, token: "") { (response) in
let arrObj = response as? [AnyObject] ?? [AnyObject]()
let dictObj = arrObj[0] as? [String: Any] ?? [String: Any]()
self.saveUserdata(dict: dictObj)
DispatchQueue.main.async {
let vc = self.storyboard?.instantiateViewController(identifier: ViewControllerIdentifier.ProfileLandingId) as! ProfileLandingVC
vc.modalPresentationStyle = .fullScreen
self.present(vc, animated: true, completion: nil)
}
} failure: { (error) in
print("error")
}
}
Earlier i was not using [weak self], recently added after following some answers. Can someone suggest a solution for it?
UNNotificationServiceExtension is not getting called when i am trying to call the following custom payload data.
{
"aps": {
"alert": {
"title": "Hello",
"body": "body.."
},
"mutable-content":1,
"sound": "default",
"badge": 1,
},
"data": {
"attachment-url": "https://pusher.com/static_logos/320x320.png"
},
}
But when i call this below custom payload data,then UNNotificationServiceExtension is getting called
{
"aps": {
"alert": {
"title": "Hello",
"body": "body.."
},
"mutable-content":1,
"sound": "default",
"badge": 1,
},
"data": {
"attachment-url": ""
},
}
My problem is that if i put any http url in attachment-url key then UNNotificationServiceExtension it's not getting called but if i put sometext or leave it as empty instead of Url then it's will call.
I am sending custom payload from https://pushtry.com/
Here is my NotificationServiceExtension class
import UserNotifications
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)
// Get the custom data from the notification payload
if let notificationData = request.content.userInfo["data"] as? [String: String] {
// Grab the attachment
if let urlString = notificationData["attachment-url"], let fileUrl = URL(string: urlString) {
// Download the attachment
URLSession.shared.downloadTask(with: fileUrl) { (location, response, error) in
if let location = location {
// Move temporary file to remove .tmp extension
let tmpDirectory = NSTemporaryDirectory()
let tmpFile = "file://".appending(tmpDirectory).appending(fileUrl.lastPathComponent)
let tmpUrl = URL(string: tmpFile)!
try! FileManager.default.moveItem(at: location, to: tmpUrl)
// Add the attachment to the notification content
if let attachment = try? UNNotificationAttachment(identifier: "", url: tmpUrl) {
self.bestAttemptContent?.attachments = [attachment]
}
}
// Serve the notification content
self.contentHandler!(self.bestAttemptContent!)
}.resume()
}
} }
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)
}
}
}
extension UNNotificationAttachment {
/// Save the image to disk
static func create(imageFileIdentifier: String, data: NSData, options: [NSObject : AnyObject]?) -> UNNotificationAttachment? {
let fileManager = FileManager.default
let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString
let tmpSubFolderURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true)
do {
try fileManager.createDirectory(at: tmpSubFolderURL!, withIntermediateDirectories: true, attributes: nil)
let fileURL = tmpSubFolderURL?.appendingPathComponent(imageFileIdentifier)
try data.write(to: fileURL!, options: [])
let imageAttachment = try UNNotificationAttachment(identifier: imageFileIdentifier, url: fileURL!, options: options)
return imageAttachment
} catch let error {
print("error \(error)")
}
return nil
}
}
UPDATE
After some comments I changed the resolveVisita method to use URLSession in the background, but it looks like the background session will only start when the app is in the foreground, here is the updated method that runs when a notification action is tapped:
static func resolverVisita(idMensagem: String, resposta: String, liberar: Bool, view: UIViewController?) {
//let configuration = URLSessionConfiguration.background(withIdentifier: "url_session_resolve_visita")
//var backgroundSession = URLSession(configuration: configuration)
// Set up the URL request
let todoEndpoint: String = URL_RESPONDE_VISITA
guard let url = URL(string: todoEndpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
let postString = "userUUID=\(SessionManager.getUsrUUID())&devUUID=\(devUUID)&msgID=\(idMensagem)&tarefa=\(liberar ? "L" : "B")&resposta=\(resposta)"
urlRequest.httpBody = postString.data(using: .utf8)
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).responde_visita")
let session = URLSession(configuration: config)
/*
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
let responseString = String(data: data, encoding: .utf8)
print("responseString = \(responseString)")
}*/
let task = session.dataTask(with: urlRequest)
task.resume()
}
The commented out code seem to work sometimes, but it looks like it is not the right way of doing it.
UPDATE END
So i have an application that receives notification and it has 2 actions, and upon user response the app should send a response to the server, preferable on the background.
I am facing an issue where whenever an action is tapped, sometimes the userNotificationCenter method is not called until the app is opened, and sometimes it does run, but the Alamofire server call does not get processed, and then if i open the app some errors are shown in the console regarding the Alamofire call, however if i tap the action then open the app quick enough it works as expected.
I already enabled Background Fetch and Remote notifications under the app capabilities on Xcode.
This is how I am creating the actions for the notification:
let liberar = UNTextInputNotificationAction(identifier:"liberar", title:"Liberar",options:[.authenticationRequired],
textInputButtonTitle: "Liberar",
textInputPlaceholder: "Resposta")
let bloquear = UNTextInputNotificationAction(identifier: "bloquear", title: "Bloquear", options: [.destructive],
textInputButtonTitle: "Bloquear",
textInputPlaceholder: "Resposta")
Here is my UNUserNotificationCenterDelegate protocol implementation:
extension AppDelegate : UNUserNotificationCenterDelegate {
// Receive displayed notifications for iOS 10 devices.
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
// With swizzling disabled you must let Messaging know about the message, for Analytics
// Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
}
// Print full message.
print(userInfo)
// Change this to your preferred presentation option
completionHandler([.alert, .badge, .sound])
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
let acao = response.actionIdentifier
//let request = response.notification.request
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: \(messageID)")
}
print(acao)
if acao == "liberar" {
// Print full message.
let textResponse = response as! UNTextInputNotificationResponse
print("Liberando Visita")
AppConfig.resolverVisita(idMensagem: (userInfo["msgid"] as? String)!, resposta: textResponse.userText, liberar: true, view: nil)
}
else if acao == "bloquear" {
let textResponse = response as! UNTextInputNotificationResponse
print("Bloqueando Visita")
AppConfig.resolverVisita(idMensagem: (userInfo["msgid"] as? String)!, resposta: textResponse.userText, liberar: false, view: nil)
//let newContent = request.content.mutableCopy() as! UNMutableNotificationContent
//print(textResponse.userText)
}
completionHandler()
}}
And here is the resolveVisita method, it basically just gathers some data and send it via a POST request using Alamofire.
static func resolverVisita(idMensagem: String, resposta: String, liberar: Bool, view: UIViewController?) {
if view != nil {
showLoading(mensagem: NSLocalizedString("Processando...", comment: ""), view: view!)
}
//Parameters to be sent
let parameters: Parameters=[
"userUUID":SessionManager.getUsrUUID(),
"devUUID":devUUID,
"msgID": idMensagem,
"resposta": resposta,
"tarefa": liberar ? "L" : "B"
];
//Sending http post request
Alamofire.request(URL_RESPONDE_VISITA, method: .post, parameters: parameters).responseJSON{
response in
//printing response
print(response)
//getting the json value from the server
if let result = response.result.value {
//converting it as NSDictionary
let jsonData = result as! NSDictionary
var mensagem : String = "Solicitacao resolvida com sucesso"
//displaying the message in label
if((jsonData["error"] as! Bool)){
mensagem = jsonData["error_msg"] as! String
if view == nil {
Notificacoes.notificaErroSolicitavao(msgId: idMensagem, errMsg: mensagem)
}
}
if view != nil {
view?.dismiss(animated: false){
showAlert(mensagem: NSLocalizedString(mensagem, comment: ""), view: view!, okMsg: NSLocalizedString("Voltar", comment: ""), segue: "voltarParaLogin")
}
}
else{
print(mensagem)
}
}
}
}
If I just add the .foreground option to the actions making the app open once it is selected, the problem is solved however I really think it would not be necessary for the app to be open for this task.
So after some time I've decided to come back to this issue with a fresh mind, and I discovered what I was doing wrong, I Was trying to create my Instance of the URLSession in the background from the lock screen, and also creating a new instance of the "same Session", to resolve this, all I had to do was create a static instance of the URLSession and use it afterwards whenever I need it.
I created it this way.
static let resolveVisitaSession = URLSession(configuration: URLSessionConfiguration.background(withIdentifier: "br.com.freaccess.resolveVisitaRequest"))
And this is the method that gets called upon the selection on a UNNotificationAction I use my URLSession in there.
static func resolverVisita(idMensagem: String, resposta: String, liberar: Bool) {
print("Entered resolveVisita method")
// Set up the URL request
let todoEndpoint: String = URL_RESPONDE_VISITA
guard let url = URL(string: todoEndpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
let postString = "userUUID=\(SessionManager.getUsrUUID())&devUUID=\(devUUID)&msgID=\(idMensagem)&tarefa=\(liberar ? "L" : "B")&resposta=\(resposta)&skipSocket=true"
urlRequest.httpBody = postString.data(using: .utf8)
resolveVisitaSession.dataTask(with: urlRequest).resume()
}
Using Swift 3 I'm implementing Apple Pay in my app and trying to send PKPaymentToken which I receive in paymentAuthorizationViewController to bank's API to process payment but without success. The data which I send is always rejected.
Bank support suggests me to send the whole payment.token instead of payment.token.PaymentData but I don't how can I do that cause payment.token is the instance of PKPaymentToken and as I know cannot be converted to string or encoded to base64.
What is the correct way to send the token?
func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, completion: #escaping ((PKPaymentAuthorizationStatus) -> Void)) {
self.processPayment(payment: payment, completion: completion)
}
func processPayment(payment: PKPayment, completion: #escaping ((PKPaymentAuthorizationStatus) -> Void)) {
print("Payment token: \(payment.token)")
let paymentData=String(data: payment.token.paymentData.base64EncodedData(), encoding: .utf8)
var request = URLRequest(url: URL(string: "https://bankapi.com/method")!)
request.httpMethod = "POST"
let postString = "orderid=\(orderid)&token=\(String(describing: paymentData))&amount=\(price)"
print("POST: \(postString)")
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(String(describing: error))")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
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))")
//#TODO check response is failure or success
//completion(PKPaymentAuthorizationStatus.failure)
}
task.resume()
}
upd.
bank service provided me an example in Objective C.
I now try to reproduce it in Swift.
Ok there is now clear for me.
let paymentDataDictionary: [AnyHashable: Any]? = try? JSONSerialization.jsonObject(with: payment.token.paymentData, options: .mutableContainers) as! [AnyHashable : Any]
var paymentType: String = "debit"
var paymentMethodDictionary: [AnyHashable: Any] = ["network": "", "type": paymentType, "displayName": ""]
if #available(iOS 9.0, *) {
paymentMethodDictionary = ["network": payment.token.paymentMethod.network ?? "", "type": paymentType, "displayName": payment.token.paymentMethod.displayName ?? ""]
switch payment.token.paymentMethod.type {
case .debit:
paymentType = "debit"
case .credit:
paymentType = "credit"
case .store:
paymentType = "store"
case .prepaid:
paymentType = "prepaid"
default:
paymentType = "unknown"
}
}
let cryptogramDictionary: [AnyHashable: Any] = ["paymentData": paymentDataDictionary ?? "", "transactionIdentifier": payment.token.transactionIdentifier, "paymentMethod": paymentMethodDictionary]
let cardCryptogramPacketDictionary: [AnyHashable: Any] = cryptogramDictionary
let cardCryptogramPacketData: Data? = try? JSONSerialization.data(withJSONObject: cardCryptogramPacketDictionary, options: [])
// in cardCryptogramPacketString we now have all necessary data which demand most of bank gateways to process the payment
let cardCryptogramPacketString = String(describing: cardCryptogramPacketData)
I'm struggling with adding an image to my Push Notification in iOS 10.
I have added a Notification Service Extension, and have used the following code:
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let urlString = request.content.userInfo["image-url"] as? String, let fileUrl = URL(string: urlString) {
URLSession.shared.downloadTask(with: fileUrl) { (location, response, error) in
if let location = location {
let options = [UNNotificationAttachmentOptionsTypeHintKey: kUTTypePNG]
if let attachment = try? UNNotificationAttachment(identifier: "", url: location, options: options) {
self.bestAttemptContent?.attachments = [attachment]
}
}
self.contentHandler!(self.bestAttemptContent!)
}.resume()
}
}
I got this code from the first answer below.
The issue I'm having now is that the notification is received, with a short delay which indicates the download must be happening, but there is no attachment shown.
I am assuming that serviceExtensionTimeWillExpire() is being called and just showing the bestAttempt
Any help is greatly appreciated.
I have my APNs payload configured correctly, I believe:
apns: {
aps: {
alert: {
title: "Title",
subtitle: "Subtitle",
body: "Body"
},
"mutable-content": 1
},
"image-url": "https://helloworld.com/image.png"
}
You have to pull the url out of the notification's user info, then download the image and give a file url to the attachment. Try something like:
if let urlString = request.content.userInfo["image-url"] as? String, let fileUrl = URL(string: urlString) {
URLSession.shared.downloadTask(with: fileUrl) { (location, response, error) in
if let location = location {
let options = [UNNotificationAttachmentOptionsTypeHintKey: kUTTypePNG]
if let attachment = try? UNNotificationAttachment(identifier: "", url: location, options: options) {
self.bestAttemptContent.attachments = [attachment]
}
}
self.contentHandler(self.bestAttemptContent)
}.resume()
}
I've managed to work this out with the following code:
Swift:
if let PusherNotificationData = request.content.userInfo["data"] as? NSDictionary {
if let urlString = PusherNotificationData["image-url"] as? String, let fileUrl = URL(string: urlString) {
URLSession.shared.downloadTask(with: fileUrl) { (location, response, error) in
if let location = location {
let options = [UNNotificationAttachmentOptionsTypeHintKey: kUTTypePNG]
if let attachment = try? UNNotificationAttachment(identifier: "", url: location, options: options) {
self.bestAttemptContent?.attachments = [attachment]
}
}
self.contentHandler!(self.bestAttemptContent!)
}.resume()
}
}
Node:
apns: {
aps: {
alert: {
title: "title",
subtitle: "subtitle",
body: "body"
},
"mutable-content": 1,
category: "test"
},
data: {
"image-url": "www.helloworld.com/image.png"
}
}
Thanks for your help!
Another solution (and a testable one) could be writing the image in a temporally location:
// NotificationRequestHandler
func getImageURL(from userInfo: [AnyHashable: Any]) throws -> URL {
// parse the image URL and return it, otherwise throws and error
}
func temporaryWriteData(from url: URL) throws -> (String, URL) {
let temporaryDirectoryUrl = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let temporaryFileName = UUID().uuidString
let temporaryFileUrl = temporaryDirectoryUrl.appendingPathComponent(temporaryFileName)
let data = try Data(contentsOf: url)
try data.write(to: temporaryFileUrl, options: .atomic)
return (temporaryFileName, temporaryFileUrl)
}
and on didReceive(_:withContentHandler:):
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
defer {
contentHandler(bestAttemptContent ?? request.content)
}
do {
let handler = NotificationRequestHandler()
let imageUrl = try handler.getImageURL(from: request.content.userInfo)
let (imageFileName, imageFileUrl) = try handler.temporaryWriteData(from: imageUrl)
let attachment = try UNNotificationAttachment(identifier: imageFileName, url: imageFileUrl, options: [UNNotificationAttachmentOptionsTypeHintKey: "public.png"])
bestAttemptContent?.attachments = [attachment]
} catch {}
Also something very helpful to debug extensions
and how to test extentions