Search code examples
iosswiftnsurlsessionuilocalnotification

iOS swift notficiation after download finishes using URLSession


I am downloading a long file using URLSession and after the download is finished i am trying to show a notification to the user to let him know that the download is completed.

Notification works perfectly when the app is running. But not working when the app goes background. When the app comes to foreground again then the notification code starts running.

My codes:

import Foundation
import Zip
import UserNotifications

class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {

    static var shared = DownloadManager()
    var selectedBook: Book!

    typealias ProgressHandler = (Float, Float, Float) -> ()

    var onProgress : ProgressHandler? {
        didSet {
            if onProgress != nil {
                let _ = activate()
            }
        }
    }

    override private init() {
        super.init()
    }

    func activate() -> URLSession {
        let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")

        // Warning: If an URLSession still exists from a previous download, it doesn't create a new URLSession object but returns the existing one with the old delegate object attached!
        return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
    }

    private func calculateProgress(session : URLSession, completionHandler : @escaping (Float, Float, Float) -> ()) {
        session.getTasksWithCompletionHandler { (tasks, uploads, downloads) in
            let progress = downloads.map({ (task) -> Float in
                if task.countOfBytesExpectedToReceive > 0 {
                    return Float(task.countOfBytesReceived) / Float(task.countOfBytesExpectedToReceive)
                } else {
                    return 0.0
                }
            })
            let countOfBytesReceived = downloads.map({ (task) -> Float in
                return Float(task.countOfBytesReceived)
            })
            let countOfBytesExpectedToReceive = downloads.map({ (task) -> Float in
                return Float(task.countOfBytesExpectedToReceive)
            })
            if progress.reduce(0.0, +) == 1.0 {
                self.postNotification()
            }
            completionHandler(progress.reduce(0.0, +), countOfBytesReceived.reduce(0.0, +), countOfBytesExpectedToReceive.reduce(0.0, +))
        }
    }

    func postUnzipProgress(progress: Double) {
        NotificationCenter.default.post(name: .UnzipProgress, object: progress)
    }

    func postNotification() {
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.alert, .sound]) { (granted, error) in
            // Enable or disable features based on authorization.
        }

        let content = UNMutableNotificationContent()
        content.title = NSString.localizedUserNotificationString(forKey: "Download Completed", arguments: nil)
        content.body = NSString.localizedUserNotificationString(forKey: "Quran Touch app is ready to use", arguments: nil)
        content.sound = UNNotificationSound.default()
        content.categoryIdentifier = "com.qurantouch.qurantouch"
        // Deliver the notification in 60 seconds.
        let trigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 2.0, repeats: false)
        let request = UNNotificationRequest.init(identifier: "downloadCompleted", content: content, trigger: trigger)

        // Schedule the notification.
        center.add(request)
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {

        if totalBytesExpectedToWrite > 0 {
            if let onProgress = onProgress {
                calculateProgress(session: session, completionHandler: onProgress)
            }
            let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
            debugPrint("Progress \(downloadTask) \(progress)")

        }
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        debugPrint("Download finished: \(location)")

        let folder = URL.createFolder(folderName: selectedBook.folder)
        let fileURL = folder!.appendingPathComponent("ClipsAndTacksF1ForModeler.zip")

        if let url = URL.getFolderUrl(folderName: selectedBook.folder) {
            do {
                try FileManager.default.moveItem(at: location, to: fileURL)
                try Zip.unzipFile((fileURL), destination: url, overwrite: true, password: nil, progress: { (progress) -> () in
                    self.postUnzipProgress(progress: progress)
                    if progress == 1 {
//                        self.postNotification()
                        UserDefaults.standard.set("selected", forKey: self.selectedBook.fileKey)
                        URL.removeFile(file: fileURL)
                    }
                }, fileOutputHandler: {(outputUrl) -> () in
                })
            } catch {
                print(error)
            }
        }
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        debugPrint("Task completed: \(task), error: \(error)")
    }

}

And started the download here

func downloadBookWithUrl(url: String) {
        DownloadManager.shared.selectedBook = selectedBook
        let url = URL(string: url)!
        let task = DownloadManager.shared.activate().downloadTask(with: url)
        task.resume()        
    }

I got an example from apple that is written in Objective C. But couldn't get through it as i don't know Objective C. Here is the example: https://developer.apple.com/library/archive/samplecode/SimpleBackgroundTransfer/Introduction/Intro.html#//apple_ref/doc/uid/DTS40013416


Solution

  • I followed the instruction from Apple docs suggested by subdan in comments

    Implented the handleEventsForBackgroundURLSession method in appDelegate. And before showing notification, I called the completion handler from appDelegate and it started working.

    In appDelegate:

    var backgroundCompletionHandler: (() -> Void)?
    func application(_ application: UIApplication,
                         handleEventsForBackgroundURLSession identifier: String,
                         completionHandler: @escaping () -> Void) {
            backgroundCompletionHandler = completionHandler
        }
    

    And before calling the notification :

    DispatchQueue.main.async {
                                guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
                                    let backgroundCompletionHandler =
                                    appDelegate.backgroundCompletionHandler else {
                                        return
                                }
                                backgroundCompletionHandler()
                                self.postNotification()
                            }