I'm trying to create a new Notification Content Extension in iOS 10 for local notifications where the notification view controller responsible for the content extension downloads an image from the network and renders it in a UIImageView. I have the Notification Content Extension target set up with an appropriate Info.plist and the content extension works beautifully for simple things like rendering a label with some content, for example the sample code that comes in the template:
func didReceive(_ notification: UNNotification) {
self.label.text = notification.request.content.body
}
However, when I try to introduce NSURLSession (or URLSession in Swift 3) into the mix, the notification content totally fails to load - not even the label gets set anymore:
func didReceive(_ notification: UNNotification) {
self.label.text = notification.request.content.body
let session = URLSession.shared()
let url = URL(string: "https://chart.googleapis.com/chart?cht=p3&chs=250x100&chd=t:60,40&chl=Hello|World")!
let task = session.downloadTask(with: url) { (fileURL, response, error) in
if let path = fileURL?.path {
DispatchQueue.main.async {
self.imageView.image = UIImage(contentsOfFile:path)
}
}
}
task.resume()
}
Is the usage of NSURLSession in the notification content extension not allowed? Is my extension possibly being killed before the download completes? If so, how could I ensure that it's not killed so I can download and render the image?
At the point when func didReceive(_ notification: UNNotification) is called in the Content extension, any modifications to the content such as downloading images should have already occurred.
It seems that you use a Notification Service Extension to do the downloading of any additional content. The Notification Content extension is only responsible for providing a custom user interface if you need one.
In your Service extension you download the image using the url in the notification payload, and set it as an attachment on the UNNotification object. If you don't need any custom UI, the system will automatically display visual media attachments like a video or an image. If that suits your needs, you don't actually need a Notification Content Extension at all.
Pusher provide a great tutorial on setting up a Notification Service Extension for handling media attachments in push notifications on iOS 10 right here.
It's actually possible to download images in a Notification Content Extension. However, your code contains two problems:
The URL is not valid.
The fileURL returned by the downloadTask methode is deleted as soon as the function runs out of scope, which is already the case when you try to access the fileURLfrom another thread. Instead it's better to catch the fileURL content in a data variable and use that to produce an image in the main thread
Slightly corrected code:
guard let url = URL(string: "https://betamagic.nl/images/coredatalab_hero_01.jpg") else {
return
}
let task = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
if let fileURL = fileURL,
let data = try? Data(contentsOf: fileURL) {
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
}
}
task.resume()
Disable app transport security in Info.plist for you extension.
Tip: move file from tmp folder to cache for save
Related
i have a problem described in title. you may see source code in my repository (https://github.com/Hudayberdyyev/custom_download_manager) . i will try to briefly explain the problem. I am trying to write a download manager based on this repo (https://github.com/r-plus/HLSion). and basically it consists of 3 parts:
SessionManager (Which managed all of sessions)
HLSData (HLSData model which initialized same as the code below. it is like an intermediary between the session manager )
public convenience init(url: URL, options: [String: Any]? = nil, name: String) {
let urlAsset = AVURLAsset(url: url, options: options)
self.init(asset: urlAsset, description: name)
}
AssetStore (It's managed HLSData.plist file. Which contain name and path of each download session).
this is how the start of downloads is implemented:
var sources = [HLSData]()
#objc func startDownloadButtonTapped() {
print(#function)
let hlsData = sources[0]
switch hlsData.state {
case .notDownloaded:
hlsData.download { (percent) in
DispatchQueue.main.async {
print("percent = \(percent)")
self.percentLabel.text = "\(percent)"
}
}.finish { (relativePath) in
DispatchQueue.main.async {
print("download completed relative path = \(relativePath)")
}
}.onError { (error) in
print("Error finish. \(error)")
}
case .downloading:
print("State is downloading")
break
case .downloaded:
print(hlsData.localUrl ?? "localURL is nil")
}
}
Before tapping state is notDownloaded. respectively app is start download when the button tapped and state is changed to downloading.
Everything is works fine and progress tracked well. But when i go to the background and return back to app, state is still keep of downloading, but progress closure doesn't work anymore. How can i restore or reset this closures for tracking progress. Thanks in advance.
On doing some tests, I feel there is a bug in iOS 12 and below with the AVAssetDownloadDelegate
When doing some tests, I noticed the following when trying to download media over HLS using AVAssetDownloadTask:
iOS 13 and above
When going into the background, the download continues
When coming into the foreground from the background, the AVAssetDownloadDelegate still triggers assetDownloadTask didLoad totalTimeRangesLoaded and the progress can be updated
After suspending or quitting an app, reinitializing an AVAssetDownloadURLSession with the same URLSessionConfiguration identifier, the download resumes automatically from where it last left off
iOS 12 and below
Everything still almost holds true except point 2, for some reason the assetDownloadTask didLoad totalTimeRangesLoaded no longer gets triggered when coming into the foreground from the background and so the progress no longer gets updated.
One workaround I got was from this answer https://stackoverflow.com/a/55847387/1619193 was that in the past, downloads had to be resumed manually after the app was suspended for AVAssetDownloadTask by providing it a location to the partially downloaded file on disk.
As per the documentation:
AVAssetDownloadTask provides the ability to resume previously stopped
downloads under certain circumstances. To do so, simply instantiate a
new AVAssetDownloadTask with an AVURLAsset instantiated with a file
NSURL pointing to the partially downloaded bundle with the desired
download options, and the download will continue restoring any
previously downloaded data.
Interestingly, you cannot find this on the official documentation anymore and also it seems like setting the destinationURL has been deprecated so it seems like there has been some refactoring in how things work.
My solution:
Subscribe to the UIApplication.willEnterForegroundNotification notification
In the call back for the UIApplication.willEnterForegroundNotification, check if the device is running iOS 12 and below
If it does, cancel the current AVAssetDownloadTask
This should trigger the AVAssetDownloadDelegate callback assetDownloadTask didFinishDownloadingTo which will give you the location of the partially downloaded file
Reconfigure the AVAssetDownloadTask but do not configure it with the HLS url, instead configure it with the URL to the partially downloaded asset
Resume the download and the progress AVAssetDownloadDelegate will seem to start firing again
You can download an example of this here
Here are some small snippets of the above steps:
private let downloadButton = UIButton(type: .system)
private let downloadTaskIdentifier = "com.mindhyve.HLSDOWNLOADER"
private var backgroundConfiguration: URLSessionConfiguration?
private var assetDownloadURLSession: AVAssetDownloadURLSession!
private var downloadTask: AVAssetDownloadTask!
override func viewDidLoad()
{
super.viewDidLoad()
// UI configuration left out intentionally
subscribeToNotifications()
initializeDownloadSession()
}
private func initializeDownloadSession()
{
// This will create a new configuration if the identifier does not exist
// Otherwise, it will reuse the existing identifier which is how a download
// task resumes
backgroundConfiguration
= URLSessionConfiguration.background(withIdentifier: downloadTaskIdentifier)
// Resume will happen automatically when this configuration is made
assetDownloadURLSession
= AVAssetDownloadURLSession(configuration: backgroundConfiguration!,
assetDownloadDelegate: self,
delegateQueue: OperationQueue.main)
}
private func resumeDownloadTask()
{
var sourceURL = getHLSSourceURL(.large)
// Now Check if we have any previous download tasks to resume
if let destinationURL = destinationURL
{
sourceURL = destinationURL
}
if let sourceURL = sourceURL
{
let urlAsset = AVURLAsset(url: sourceURL)
downloadTask = assetDownloadURLSession.makeAssetDownloadTask(asset: urlAsset,
assetTitle: "Movie",
assetArtworkData: nil,
options: nil)
downloadTask.resume()
}
}
func cancelDownloadTask()
{
downloadTask.cancel()
}
private func getHLSSourceURL(_ size: HLSSampleSize) -> URL?
{
if size == .large
{
return URL(string: "https://video.film.belet.me/45505/480/ff27c84a-6a13-4429-b830-02385592698b.m3u8")
}
return URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8")
}
// MARK: INTENTS
#objc
private func downloadButtonTapped()
{
print("\(downloadButton.titleLabel!.text!) tapped")
if downloadTask != nil,
downloadTask.state == .running
{
cancelDownloadTask()
}
else
{
resumeDownloadTask()
}
}
#objc
private func didEnterForeground()
{
if #available(iOS 13.0, *) { return }
// In iOS 12 and below, there seems to be a bug with AVAssetDownloadDelegate.
// It will not give you progress when coming from the background so we cancel
// the task and resume it and you should see the progress in maybe 5-8 seconds
if let downloadTask = downloadTask
{
downloadTask.cancel()
initializeDownloadSession()
resumeDownloadTask()
}
}
private func subscribeToNotifications()
{
NotificationCenter.default.addObserver(self,
selector: #selector(didEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
// MARK: AVAssetDownloadDelegate
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?)
{
guard error != nil else
{
// download complete, do what you want
return
}
// something went wrong, handle errors
}
func urlSession(_ session: URLSession,
assetDownloadTask: AVAssetDownloadTask,
didFinishDownloadingTo location: URL)
{
// Save the download path of the task to resume downloads
destinationURL = location
}
If something seems out of place, I recommend checking out the full working example here
I am working on an application that requires to download a certain number of files to be able to work offline. Obviously, download tasks are preferred to be done with the app in the background. I implemented an URLSession with a background configuration following Apple's documentation available here : https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background. I also followed a tutorial on raywenderlich: https://www.raywenderlich.com/3244963-urlsession-tutorial-getting-started.
Basically, what I've done looks like this (I've made my class a Singleton but I have the same problem either way):
public final class DownloadService: NSObject {
static let shared = DownloadService()
static let identifier = "downloadService"
private var urlSession: URLSession!
var backgroundCompletionHandler: (() -> Void)? // This is attributed in the handleEventsForBackgroundURLSession delegate method in the AppDelegate
private override init() {
super.init()
let config = URLSessionConfiguration.background(withIdentifier: DownloadService.identifier)
config.isDiscretionary = true
urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
}
}
extension DownloadService: URLSessionDelegate {
// Delegate method called when the background session is finished.
public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
guard let completionHandler = self.backgroundCompletionHandler else {
Logger.fault("No completion for bg session", category: .network)
return
}
Logger.log("Complete background session", category: .network)
// This must be executed on the main thread
// Executes things such as updating the app preview in recent apps view
completionHandler()
}
}
}
extension DownloadService: URLSessionDownloadDelegate {
// Delegate method called when a download task is finished
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// Perform
guard let sourceUrl = downloadTask.originalRequest?.url else {
return
}
Logger.log("Received file: %#", sourceUrl.lastPathComponent, category:.network)
// Check and save file
saveFile(originalFileURL: sourceUrl, downloadedTo: location)
}
}
And I start the download using:
/// Download file using a previously created URLSession.
/// - parameter filename: Name of the file.
/// - parameter baseURL: URL where the files are located.
/// - parameter size: Expected filesize in Bytes.
private func download(file filename: String, from baseURL: String, size: Int64) {
guard let url = URL(string: baseURL)?.appendingPathComponent(filename) else { return }
let task = urlSession.downloadTask(with: url)
task.countOfBytesClientExpectsToSend = 0
task.countOfBytesClientExpectsToReceive = size
task.resume()
}
My problem is that everything works fine when the app is in foreground, but whenever I put the app in the background or lock the screen, I have an error saying:
Task <46648342-7D13-4D1F-96A1-FDAE4C1F8475>.<362> finished with error [22] Error Domain=NSPOSIXErrorDomain Code=22 "Invalid argument"
I have tried playing a bit with the URLSessionConfiguration, specifically the isDiscretionary parameter which is set to false by default, and it seems that setting it to true, as advised by Apple's documentation, even blocks the download from proceeding with the app in the foreground, resulting to the same error 'Invalid argument'.
I wonder if this parameter has anything to do with my problem, or if there's something I've misunderstood?
The exemple on raywenderlich provided above also works the same way, using isDiscretionary seems to make the download fail everytime.
I am using Xcode 11.3.1 with Swift 5 and targeting iOS13.
Let me know if any other information is needed and thank you for your help!
So, I was trying to do it with a simulator. Either by running from Xcode with the debugger, or by installing the app into the simulator (without the debugger since it affects the application lifecycle).
I tried to run it on a real device (iPad), and there's no sign of this error whatsoever! Setting isDiscretionary seems to work as intended so I'm not sure that this parameter was causing the issue on a simulator.
I am currently working in a project where we already have the setup for push notifications in our application's main target and now we want to add the rich push functionality. I made a lot of research on the internet and couldn't find a way to implement rich push notifications without adding a new target(Notification Service Extension) to the project.
As I said in title, is it possible to add an image to a push notification without using this new target?
Technically yes, but in reality no. As long as your image is small enough that doing something like a base64 encoding of the image still allows it to fit in the payload, you could do it.
However, considering how simple it is to add a notification service extension, why are you trying to avoid it? Your extension would just look like so:
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void) {
guard let urlPath = request.content.userInfo["media-url"] as? String,
let url = URL(string: urlPath) else {
return
}
self.contentHandler = contentHandler
bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
defer { contentHandler(bestAttemptContent!) }
let destination = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(url.lastPathComponent)
do {
let data = try Data(contentsOf: url)
try data.write(to: destination)
let attachment = try UNNotificationAttachment(identifier: "",
url: destination)
bestAttemptContent!.attachments = [attachment]
} catch {
}
}
Notice explicitly that you need to do a synchronous download in this method.
I've written an extension to share images to my web-service. The server is receiving the items without problem but something is happening on the server that execution is terminated before the expected state change.
I am trying to figure out how I can display a response from the web-server (like an error) after the post button has been hit in the share extension.
Is this possible? How can it be done?
my didSelectPost:
override func didSelectPost() {
let identifier = NSBundle.mainBundle().bundleIdentifier! + "." + NSUUID().UUIDString
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(identifier)
configuration.sharedContainerIdentifier = "group.aa.com"
let session = NSURLSession(configuration: configuration)
let request = urlRequestWithImage(attachedImage, text: contentText)!
let task = NSURLSession.sharedSession().dataTaskWithRequest(request)
task.resume()
extensionContext?.completeRequestReturningItems([], completionHandler: nil)
}
In Xcode select the app running which is right between the STOP icon and the device selector. From there, choose the extension and run that in the simulator or on your attached device, now you will see the prints from your code in Xcode
I am using Alamofire to download data
How to make alamofire run download in background with swift?
Thanks
The basic idea is as follows:
The key problem is that with background downloads, your app may actually be terminated while downloads are in progress (e.g. jettisoned due to memory pressure). Fortunately, your app is fired up again when background downloads are done, but any task-level closures you originally supplied are long gone. To get around this, when using background sessions, one should rely upon session-level closures used by the delegate methods.
import UIKit
import Alamofire
import UserNotifications
fileprivate let backgroundIdentifier = ...
fileprivate let notificationIdentifier = ...
final class BackgroundSession {
/// Shared singleton instance of BackgroundSession
static let shared = BackgroundSession()
/// AlamoFire `SessionManager`
///
/// This is `private` to keep this app loosely coupled with Alamofire.
private let manager: SessionManager
/// Save background completion handler, supplied by app delegate
func saveBackgroundCompletionHandler(_ backgroundCompletionHandler: #escaping () -> Void) {
manager.backgroundCompletionHandler = backgroundCompletionHandler
}
/// Initialize background session
///
/// This is `private` to avoid accidentally instantiating separate instance of this singleton object.
private init() {
let configuration = URLSessionConfiguration.background(withIdentifier: backgroundIdentifier)
manager = SessionManager(configuration: configuration)
// specify what to do when download is done
manager.delegate.downloadTaskDidFinishDownloadingToURL = { _, task, location in
do {
let destination = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent(task.originalRequest!.url!.lastPathComponent)
try FileManager.default.moveItem(at: location, to: destination)
} catch {
print("\(error)")
}
}
// specify what to do when background session finishes; i.e. make sure to call saved completion handler
// if you don't implement this, it will call the saved `backgroundCompletionHandler` for you
manager.delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] _ in
self?.manager.backgroundCompletionHandler?()
self?.manager.backgroundCompletionHandler = nil
// if you want, tell the user that the downloads are done
let content = UNMutableNotificationContent()
content.title = "All downloads done"
content.body = "Whoo, hoo!"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let notification = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(notification)
}
// specify what to do upon error
manager.delegate.taskDidComplete = { _, task, error in
let filename = task.originalRequest!.url!.lastPathComponent
if let error = error {
print("\(filename) error: \(error)")
} else {
print("\(filename) done!")
}
// I might want to post some event to `NotificationCenter`
// so app UI can be updated, if it's in foreground
}
}
func download(_ url: URL) {
manager.download(url)
}
}
Then I can just initiate those downloads. Note, I do not specify any task-specific closure when I initiate the download, but rather merely use the above session-level closures that use the details of the URLSessionTask to identify what to do:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// request permission to post notification if download finishes while this is running in background
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
if let error = error, !granted {
print("\(error)")
}
}
}
#IBAction func didTapButton(_ sender: Any) {
let urlStrings = [
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg"
]
let urls = urlStrings.flatMap { URL(string: $0) }
for url in urls {
BackgroundSession.shared.download(url)
}
}
}
If your app isn't running when the downloads finish, iOS needs to know that, after it restarted your app, when you're all done and that it can safely suspend your app. So, in handleEventsForBackgroundURLSession you capture that closure:
class AppDelegate: UIResponder, UIApplicationDelegate {
...
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
BackgroundSession.shared.saveBackgroundCompletionHandler(completionHandler)
}
}
That is used by sessionDidFinishEventsForBackgroundURLSession, in step 1.
Two observations:
This is only called if your app was not running when the downloads finish.
If doing background sessions, though, you must capture this closure and call it when you're all done processing the background session delegate methods.
So, to recap, the basic limitations of background sessions are:
You can only use download and upload tasks while the app is in background;
You can only rely upon session-level delegates because the app may have been terminated since the requests were initiated; and
In iOS, you must implement handleEventsForBackgroundURLSession, capture that completion handler, and call it when your background process is done.
I must also point out that while Alamofire is a wonderful library, it's not actually adding a lot value (above and beyond what is provided by URLSession to this background download process). If you're doing simple uploads/downloads only, then you might consider just using URLSession directly. But if you are using Alamofire in your project already or if your requests consist of more complicated application/x-www-form-urlencoded requests (or whatever) which merit the advantages of Alamofire, then the above outlines the key moving parts involved in the process.