The last article mentioned background downloads, so let’s look at how background downloads are handled in Alamofire. First, use native writing method to achieve a background download task, using Alamofire to achieve, through comparison to see the advantages of Alamofire.

Source address: testapi. Onapp. Top/public/vide…

URLSession background download

First you need to create a session and set the session parameters:

//1. Set request parameters
let configuration = URLSessionConfiguration.background(withIdentifier: "com.yahibo.background_id")
let session = URLSession.init(configuration: configuration,delegate: self,delegateQueue: OperationQueue.main)
//2. Set the data source
let videoUrl = "http://onapp.yahibo.top/public/videos/video.mp4"
let url = URL.init(string: videoUrl)!
//3, create a download task and initiate the request
session.downloadTask(with: url).resume()
Copy the code
  • Configure the session asbackgroundMode to enable the background download function
  • Create a download task and execute itresumeStart the task
  • After session initialization, the task callback only goes through the proxy method, and the data callback is not performed through the closure. If the closure is used, an error message will be displayed
session.downloadTask(with: url) { (url, response, error) in
    print(url)
    print(response)
    print(error)
}.resume()
Copy the code
Error Message: Completion handler blocks are not supported in background sessions. Use a delegate instead.Copy the code

Block callbacks to data are not supported in background sessions and proxies are required, so in background downloads we use proxy methods directly to process data. The proxy method is as follows:

extension Alamofire2Controller: URLSessionDownloadDelegate{
    //1. Download progress
    func urlSession(_ session: URLSession.downloadTask: URLSessionDownloadTask.didWriteData bytesWritten: Int64.totalBytesWritten: Int64.totalBytesExpectedToWrite: Int64) {
        print("Download progress:\(Double(totalBytesWritten)/Double(totalBytesExpectedToWrite))")}// The download is complete
    func urlSession(_ session: URLSession.downloadTask: URLSessionDownloadTask.didFinishDownloadingTo location: URL) {
        let locationPath = location.path
        print("Download completed:\(location.path)")
        // Store to user directory
        let documents = NSHomeDirectory(a)+ "/Documents/my.mp4"
        print("Storage location:\(documents)")
        // Copy the video to the destination address
        let fileManager = FileManager.default
        try!fileManager.moveItem(atPath: locationPath, toPath: documents)
    }
}
Copy the code

When the file is downloaded, it will be saved in the TMP file in the sandbox first. This file only stores temporary data and will be automatically cleaned after use. Therefore, you need to copy files downloaded in TMP to the Documents folder for storage.

Check the file download status through the printed path. The above operations do not actually complete the background download. When the application is returned to the background, the download task is stopped.

Download Progress:0.3653140762324527Download Progress:0.4018703091059228
2019-08-19 15:23:14.237923+0800 AlamofireDemo[849:9949] An error occurred on the xpc connection requesting pending callbacks for the background session: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.nsurlsessiond" UserInfo={NSDebugDescription=Connection to service named com.apple.nsurlsessiond}/Users/hibo/Library/Developer/CoreSimulator/Devices/404EDFDD-735E-454B-A576-70268D8A17C0/data/Containers/Data/Application/E3175312-D6B8-4576-9B84-4EBD7751A4C0/Library/Caches/com.apple.nsurlsessiond/Downloads/com.yahibo.background_id/CFNetworkDownload_eo4RMO.tmp Storage location:/Users/hibo/Library/Developer/CoreSimulator/Devices/404EDFDD-735E-454B-A576-70268D8A17C0/data/Containers/Data/Application/E3175312-D6B8-4576-9B84-4EBD7751A4C0/Documents/20190819152314.mp4
Copy the code

As mentioned in the previous article, Apple officially requires that two agent methods be implemented during background task downloads to notify the system of updates to the interface.

1. Implement in an AppDelegate

var backgroundCompletionHandler: (()->Void)? = nil
// Set this to enable background download permission
func application(_ application: UIApplication.handleEventsForBackgroundURLSession identifier: String.completionHandler: @escaping() - >Void) {
    self.backgroundCompletionHandler = completionHandler
}
Copy the code
  • Enable the background download permission, the implementation of proxy method is open

Implement the proxy method in the Alamofire2Controller extension above

// Background task download callback
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    print("Background task download back")
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate.let backgroundHandle = appDelegate.backgroundCompletionHandler else { return }
        backgroundHandle()
    }
}
Copy the code
  • This method is called when the background task completes, and is called inside the methodAppDelegateIn, notifying the system to update the interface or drop frames

Add the above method to run the download again, exit the foreground, and wait a few seconds to see that there is a background download complete callback printed on the console. In this case, we go back to the foreground and our page has actually been updated. At this point we have completed a background download function.

Summary: The background download task requires the implementation of four proxy methods

Controller:

  • URLSessionDownloadTask:Get download progress
  • DidFinishDownloadingTo:Download complete process the downloaded file
  • UrlSessionDidFinishEvents:After the background download is completed, the system is prompted to update the interface in time and execute the closure function in Application

Application:

  • BackgroundCompletionHandler:The background download completes the closure for receiving notification messages

From years of development experience (too loaded 😂), the above implementation is not the ideal result, functional code dispersion. Here’s how Alamofire is implemented.

Second, Alamofire background download

Alamofire.request(url,method: .post,parameters: ["page":"1"."size":"20"]).responseJSON {
    (response) in
    switch response.result{
    case .success(let json):
        print("json:\(json)")
        break
    case .failure(let error):
        print("error:\(error)")
        break}}Copy the code

In the above code, Alamofire can send requests directly through Request, and there is also a download method in the framework to complete the download task. Check the official documentation.

// Download the file
Alamofire.download(url, to: { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
    let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let fileURL = documentsURL.appendingPathComponent("\ [self.currentDateStr()).mp4")
    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
})
.downloadProgress { (progress) in
    print(progress)
}.response(queue: DispatchQueue.global(qos: .utility), completionHandler: { (response) in
    print("Download complete:\(response)")})Copy the code
  • DownloadRequest. DownloadOptions:Set the location where the downloaded files are stored
  • DownloadProgress:Get download progress

Although the above files can be downloaded, they cannot be downloaded in the background. First of all, officials point out:

The Alamofire.download APIs should also be used if you need to download data while your app is in the background. For more information, please see the Session Manager Configurations section.

We need to manually configure the session to be in background mode, but the download used above actually uses the default mode and does not support background download. The following code:

public static let `default`: SessionManager = {
    let configuration = URLSessionConfiguration.default
    configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

    return SessionManager(configuration: configuration)
}()
Copy the code

Looking at the official documentation and source code, we actually only need to reset the session configuration information.

Changing the Session Mode

let configuration = URLSessionConfiguration.background(withIdentifier:"com.yahibo.background_id")
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
sessionManager = SessionManager(configuration: configuration)
Copy the code

The above sessionManager needs to be set up as a singleton to receive Appdelegate’s proxy closure functions in background download mode and use the closure to inform the system to update the interface. The code is as follows:

struct BackgroundManager {
    static let shared = BackgroundManager(a)let manager: SessionManager = {
        let configuration = URLSessionConfiguration.background(withIdentifier:"com.yahibo.background_id")
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
        return SessionManager(configuration: configuration)
    }()
}
Copy the code

Let’s start the download function:

BackgroundManager.shared.manager.download(url) { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
        let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let fileURL = documentsURL.appendingPathComponent("\ [self.currentDateStr()).mp4")
        return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
    }.downloadProgress(queue: DispatchQueue.global(qos: .utility)) { (progress) in
        print(progress)
    }.response(queue: DispatchQueue.global(qos: .utility), completionHandler: { (response) in
        print("Download complete:\(response)")})Copy the code
  • Same as above direct calldownloadMethod to download and store data

We also need to call apple should request handleEventsForBackgroundURLSession block of code in the notification system update interface, how to make the connection in our SessionManager. The code is as follows:

// Set this to enable background download permission
func application(_ application: UIApplication.handleEventsForBackgroundURLSession identifier: String.completionHandler: @escaping() - >Void) {
    BackgroundManager.shared.manager.backgroundCompletionHandler = completionHandler
}
Copy the code
  • SessionManagerWe have everything we needbackgroundCompletionHandlerCode block declaration to receive the closure and invoke the closure

Simple steps to achieve the background we want to download the function, simple coding, clear logic. Here we only realized on the background in Application download authority of agency, but not in the controller to set the delegate and realize urlSessionDidFinishEvents agent method, Here it’s not hard to guess URLSessionDownloadTask, didFinishDownloadingTo, urlSessionDidFinishEvents agent method should be implemented in our SessionManager, Unified management is then passed back to the current interface in the form of closures. Let’s see if SessionManager is implemented this way.

SessionManager source code exploration

First follow the creation of the SessionManager to find the class initialization method:

public init(
    configuration: URLSessionConfiguration = URLSessionConfiguration.default,
    delegate: SessionDelegate = SessionDelegate(),
    serverTrustPolicyManager: ServerTrustPolicyManager? = nil)
{
    self.delegate = delegate
    self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)

    commonInit(serverTrustPolicyManager: serverTrustPolicyManager)
}
Copy the code

Initialization takes three initial parameters with default values and returns a new SessionManager object. In the background download above we only configured the configuration parameter, set to background download mode. As mentioned above, there should be a proxy implementation in the SessionManager. In this function, we see that a SessionDelegate object is initialized and the URLSession proxy implementation points to the SessionDelegate object. It’s not hard to guess that the urlSession-related delegate methods should all be implemented in the SessionDelegate class.

SessionDelegate

In sessionDelegate. swift, the SessionDelegate inherits from NSObject and declares all the closure functions associated with the URLSession proxy to return the results of the proxy event to the interface.

The following proxy methods are implemented in the extension method:

URLSessionDelegate
URLSessionTaskDelegate
URLSessionDataDelegate
URLSessionDownloadDelegate
URLSessionStreamDelegate
Copy the code

Let’s take a look at what is implemented inside the agent methods associated with downloading. The code is as follows:

extension SessionDelegate: URLSessionDownloadDelegate {

    open func urlSession(
        _ session: URLSession.downloadTask: URLSessionDownloadTask.didFinishDownloadingTo location: URL)
    {
        if let downloadTaskDidFinishDownloadingToURL = downloadTaskDidFinishDownloadingToURL {
            downloadTaskDidFinishDownloadingToURL(session, downloadTask, location)
        } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
            delegate.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
        }
    }

    open func urlSession(
        _ session: URLSession.downloadTask: URLSessionDownloadTask.didWriteData bytesWritten: Int64.totalBytesWritten: Int64.totalBytesExpectedToWrite: Int64)
    {
        if let downloadTaskDidWriteData = downloadTaskDidWriteData {
            downloadTaskDidWriteData(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
        } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
            delegate.urlSession(
                session,
                downloadTask: downloadTask,
                didWriteData: bytesWritten,
                totalBytesWritten: totalBytesWritten,
                totalBytesExpectedToWrite: totalBytesExpectedToWrite
            )
        }
    }

    open func urlSession(
        _ session: URLSession.downloadTask: URLSessionDownloadTask.didResumeAtOffset fileOffset: Int64.expectedTotalBytes: Int64)
    {
        if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
            downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
        } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
            delegate.urlSession(
                session,
                downloadTask: downloadTask,
                didResumeAtOffset: fileOffset,
                expectedTotalBytes: expectedTotalBytes
            )
        }
    }
}
Copy the code

The above three methods are used to monitor the download progress and completion of the download, and within the callback the closure callback agent event to the main screen. This file implements all of the methods of the proxy mentioned above, passing values to the outside world through declared closures that are only called externally. The closure function here, bridged with the outside world, returns a self, so it can retrieve the data passed by the agent as a chain. As follows:

open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self {
    downloadDelegate.progressHandler = (closure, queue)
    return self
}
Copy the code
  • Bridge interface and interiorSessionDelegateExtension agent, complete download progress monitoring

Other bridging methods are omitted…

We found an extension from URLSessionDelegate for background downloads:

extension SessionDelegate: URLSessionDelegate {
    open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        sessionDidFinishEventsForBackgroundURLSession?(session)
    }
}
Copy the code

After the background download is complete, the method is executed. In this method, the external implementation closure is called, which is implemented in the SessionManager as follows:

private func commonInit(serverTrustPolicyManager: ServerTrustPolicyManager?). {
    session.serverTrustPolicyManager = serverTrustPolicyManager

    delegate.sessionManager = self

    delegate.sessionDidFinishEventsForBackgroundURLSession ={[weak self] session in
        guard let strongSelf = self else { return }
        DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?()}}}Copy the code
  • toSessionDelegateThe incomingself, circular reference appears here, heredelegate.sessionManageruseweakModification to solve
  • implementationdelegateThe mid-background download completion callback closure is where to receive the background download completion message
  • In the main thread, callbackgroundCompletionHandlerSends the message tobackgroundCompletionHandlerClosure implementation of

Should be clear, here is our SessionManager backgroundCompletionHandler statement of closures, get closure system to realize in the Application, used for communication with the system, tell the system update interface in the background.