0

I am trying to implement a background download manager for my app. Everything works smoothly. But the problem i am facing is when i return to the downloader view controller download size gets increased each time. So may be each time new download task is getting queued. I tried following way to handle it.

if let task = Downloader.shared.task {
                task.resume()
            } else {
                Downloader.shared.startDownload(url: url)
            }

My download manager class

//
//  Downloader.swift
//  Quran Touch
//
//  Created by Ahsan Aasim on 28/4/19.
//  Copyright © 2019 Ahsan Aasim. All rights reserved.
//


import Foundation
import Zip
import UserNotifications

public struct DownloadProgress {
    public let name: String
    public let progress: Float
    public let completedUnitCount: Float
    public let totalUnitCount: Float
}

class Downloader : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {

    static var shared = Downloader()
    let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).audioDownloader")
    var session: URLSession!
    var task: URLSessionDownloadTask?

    typealias ProgressHandler = (String, Float, Float, Float) -> ()
//
//    var onProgress : ProgressHandler? {
//        didSet {
//            if onProgress != nil {
//                let _ = activate()
//            }
//        }
//    }

    override private init() {
        super.init()
        session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
    }

    func activate() -> URLSession {
        // 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 session
    }

    func startDownload(url: URL) {
        task = Downloader.shared.activate().downloadTask(with: url)
        task?.resume()
    }

    private func calculateProgress(session : URLSession, completionHandler : @escaping ProgressHandler) {
        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 let name = UserDefaults.standard.string(forKey: UserDefaultKeys.downloadingAudio) {
                if name.isEmpty {
                    return self.session.invalidateAndCancel()
                }
                completionHandler(name, 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: "Your reciter is ready to play offline", arguments: nil)
        content.sound = UNNotificationSound.default()
        content.categoryIdentifier = "com.qurantouch.qurantouch.audioDownloader"
        // Deliver the notification in 60 seconds.
        let trigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 2.0, repeats: false)
        let request = UNNotificationRequest.init(identifier: "audioDownloadCompleted", 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 {
            calculateProgress(session: session, completionHandler: { (name, progress, completedUnitCount, totalUnitCount) in
                let progressInfo = DownloadProgress(name: name, progress: progress, completedUnitCount: completedUnitCount, totalUnitCount: totalUnitCount)
                NotificationCenter.default.post(name: .AudioDownloadProgress, object: progressInfo)
            })
        }
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        if let name = UserDefaults.standard.string(forKey: UserDefaultKeys.downloadingAudio) {
            if name.isEmpty {
                return self.session.invalidateAndCancel()
            }
            let folder = URL.createFolder(folderName: "\(Config.audioFolder)\(name)")
            let fileURL = folder!.appendingPathComponent("\(name).zip")

            if let url = URL.getFolderUrl(folderName: "\(Config.audioFolder)\(name)") {
                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 {
                            DispatchQueue.main.async {
                                Reciter().downloadCompleteReciter(success: true).done{_ in}.catch{_ in}

                                guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
                                    let backgroundCompletionHandler =
                                    appDelegate.backgroundCompletionHandler else {
                                        return
                                }
                                backgroundCompletionHandler()
                                self.postNotification()
                            }
                            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)")
        DispatchQueue.main.async {
            Reciter().downloadCompleteReciter(success: false).done{_ in}.catch{_ in}
        }
    }


    func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
        DispatchQueue.main.async {
            Reciter().downloadCompleteReciter(success: false).done{_ in}.catch{_ in}
        }
    }
}
Ahsan Aasim
  • 1,177
  • 3
  • 14
  • 40
  • You should only call `resume` once; you call it when you first create the task and then you call it again each time you return and `task` isn't nil. You effectively only need the `else` clause in your first code block. – Paulw11 Apr 28 '19 at 20:05
  • If your memory is increasing, I’d suggest using the Xcode “debug memory graph” or allocations tool to figure out what is getting allocated and not released, e.g. https://stackoverflow.com/questions/30992338/how-to-debug-memory-leaks-when-leaks-instrument-does-not-show-them/30993476#30993476. – Rob Apr 28 '19 at 22:42
  • Unrelated: 1. You must implement [`urlSessionDidFinishEvents`](https://developer.apple.com/documentation/foundation/urlsessiondelegate/1617185-urlsessiondidfinishevents) and call the handler supplied by [`handleEventsForBackgroundURLSession`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622941-application). 2. Use `nil` rather than `OperationQueue()` when you create your session. The [docs warn](https://developer.apple.com/documentation/foundation/urlsession/1411597-init), "The queue should be a serial queue, in order to ensure the correct ordering of callbacks." – Rob Apr 28 '19 at 22:49

0 Answers0