I'm used to making a custom class for URLSession using singleton. So I'm using it with URLSession delegate first time and I'm getting confused. Because delegate method is not called!
Can I know any solution? Did I miss something?
And I just want to check whether making a custom class for URLSession is a good thing?
Here is my code. This is my custom API for URLSession.
import UIKit
class CustomNetworkAPI {
static let shared = CustomNetworkAPI()
var session: URLSession?
private var sessionDataTask: URLSessionDataTask?
private var sessionDownloadTask: URLSessionDownloadTask?
var cache: URLCache?
private init() {}
func downloadTaskForImage(_ url: URL, _ completionHandler: #escaping (Result<URL, Error>) -> ()) {
// print(session.delegate)
sessionDownloadTask = session?.downloadTask(with: url, completionHandler: { (url, response, error) in
if let error = error {
completionHandler(.failure(error))
return
}
guard let url = url else {
completionHandler(.failure(URLRequestError.dataError))
return
}
completionHandler(.success(url))
})
sessionDownloadTask?.resume()
}
}
And this is sample code(not whole thing) of VC using the api and delegate.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let api = CustomNetworkAPI.shared
let config = URLSessionConfiguration.default
api.session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
guard let url = URL(string: "...") else { return }
api.downloadTaskForImage(url) { (result) in
switch result {
case .success(let url):
print(url)
case .failure(let error):
print(error)
}
}
}
}
extension ViewController: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("down load complete")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
print("ing")
}
}
I was missing something, which is this quote below that I found in apple docs.
Your URLSession object doesn’t need to have a delegate. If no delegate is assigned, a system-provided delegate is used, and you must provide a completion callback to obtain the data.
So I tried to remove the completion handler block, and the delegate method was called.
Related
I am trying to create a download task that will continue to download a video even when the application is in the background.
So I followed the apple documentation to create a url session as background.
When I started the download process by tapped a button, I will print the progress of the download.
However, the application stops printing the progress when the application goes into background.
I wonder what did I miss, or misunderstand.
class ViewController: UIViewController {
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "MySession")
config.isDiscretionary = true
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue:
OperationQueue())
}()
var downloadUrl = "https://archive.rthk.hk/mp4/tv/2021/THKCCT2021M06000036.mp4"
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func downloadButtonTapped(_ sender: UIButton) {
print("DownloadButton Tapped")
let url = URL(string: downloadUrl)
guard let url = url else {
print("Invalid URL")
return
}
let downloadTask = urlSession.downloadTask(with: url)
downloadTask.resume()
}
}
extension ViewController: URLSessionDownloadDelegate, URLSessionDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
print("Downloaded, location: \(location.path)")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
print("Progress: \(progress)")
}
}
You need to configure the background tasks to make that work properly.
For more details to configure background tasks please check this official apple developer documentation
https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background
Background: I have a page where users can go to download various map files. I currently have a solution setup but it has some issues. Here is what I have:
public class FileDownloader: NSObject, URLSessionDownloadDelegate {
public let identifier: String
public let progress = CurrentValueSubject<Float, Never>(0)
public let handleFinish: (URL) -> Void
public let handleError: (Error?) -> Void
public init(identifier: String, handleFinish: #escaping (URL) -> Void, handleError: #escaping (Error?) -> Void) {
self.identifier = identifier
self.handleFinish = handleFinish
self.handleError = handleError
}
lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: self.identifier)
config.sessionSendsLaunchEvents = true
let queue = OperationQueue()
return URLSession(configuration: config, delegate: self, delegateQueue: queue)
}()
func startDownload(url: URL) {
let backgroundTask = urlSession.downloadTask(with: url)
backgroundTask.resume()
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
self.handleError(error)
}
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
self.handleFinish(location)
}
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let percentage = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
self.progress.send(percentage)
}
}
Basically I create a new FileDownloader for each row file. The problems are the following:
On the simulator, I start a download and let it run for a bit, then I stop the app. When I start it again and try to download the file again, it starts downloading 2 of the same file and updates me with the progress of both.
I'm not sure how to get the current progress when I go away from the download page and then come back.
Seems like if I could get the currently running task then I could use that. Is it possible to query the system for a running download and start getting the progress updates? Am I way off on my implementation and need to make changes to do what I want to do?
I have a file for a BackgroundSession class
class BackgroundSession: NSObject {
static let shared = BackgroundSession()
static let identifier = "com.***.bg"
private var session: URLSession!
var savedCompletionHandler: (() -> Void)?
private override init() {
super.init()
let configuration = URLSessionConfiguration.background(withIdentifier: BackgroundSession.identifier)
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
func start(_ request: URLRequest) {
session.downloadTask(with: request).resume()
}
}
extension BackgroundSession: URLSessionDelegate {
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
self.savedCompletionHandler?()
self.savedCompletionHandler = nil
}
}
}
extension BackgroundSession: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
// handle failure here
print("\(error.localizedDescription)")
}
}
}
extension BackgroundSession: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let data = try Data(contentsOf: location)
let json = try JSONSerialization.jsonObject(with: data)
print("\(json)")
// do something with json
} catch {
print("\(error.localizedDescription)")
}
}
}
I am listening for background location updates to come in later on
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("didUpdateLocations")
if locations.first != nil {
let lastLocation = locations.last
self.lastLocation = lastLocation
print("doing background work")
self.getUserData()
if PubnubController.pubnubChannel != nil {
PubnubController.sharedClient.publish(["action": "newCoordinates", "data": ["coordinates": ["latitude": lastLocation?.coordinate.latitude, "longitude": lastLocation?.coordinate.longitude]]], toChannel: PubnubController.pubnubChannel!, compressed: false)
}
}
}
self.getUserData() looks like this
func getUserData() {
print("getUserData")
if (self.userId != -1 && self.userAuthToken != nil) {
let httpUrl: String = "https://api.***.com/dev/users/\(self.userId)"
guard let url = URL(string: httpUrl) else {
return
}
var request = URLRequest(url: url)
request.setValue(self.userAuthToken, forHTTPHeaderField: "Authorization")
let session = BackgroundSession.shared
session.start(request)
}
}
In my ExtensionDelegate.swift I have the typical func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>)
with a for loop and switch set with a case for WKURLSessionRefreshBackgroundTask that looks like this
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
print("WKURLSessionRefreshBackgroundTask")
// Be sure to complete the URL session task once you’re done.
urlSessionTask.setTaskCompletedWithSnapshot(false)
In my controller, I also have pasted the function the class is supposed to call
func application(_ application: WKExtension, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
print("handleEventsForBackgroundURLSession")
BackgroundSession.shared.savedCompletionHandler = parseUserData
}
It seems that both the delegate function and this pasted function are not being called with my data. I'm having a really hard time trying to understand this background URLSession flow
Note the BackgroundSession class came from this Stackoverflow question
URLSession.datatask with request block not called in background
This handleEventsForBackgroundURLSession is an iOS pattern. This is a method of the UIApplicationDelegate protocol. You can’t just add that to some random controller. It’s only applicable for your iOS app's UIApplicationDelegate.
For watchOS, I suspect the idea is the same, except rather than calling the completion handler that iOS provides, supply your own completion handler to the BackgroundSession that calls the setTaskCompletedWithSnapshot of the WKURLSessionRefreshBackgroundTask:
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once you’re done.
BackgroundSession.shared.savedCompletionHandler = {
urlSessionTask.setTaskCompletedWithSnapshot(false)
}
...
}
}
}
But, effectively, the idea is the same. We’re deferring the setTaskCompletedWithSnapshot until urlSessionDidFinishEvents(forBackgroundURLSession:) is called.
If you want the BackgroundSession to call your controller’s parser, you can specify a protocol for that interface:
protocol Parser: class {
func parse(_ data: Data)
}
You can then give your BackgroundSession a property to keep track of the parser:
weak var parser: Parser?
You can have the didFinishDownloadingTo call the parser:
extension BackgroundSession: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let data = try Data(contentsOf: location)
parser?.parse(data)
} catch {
os_log(.error, log: log, "Error retrieving data for %{public}#: %{public}#", downloadTask.originalRequest?.url?.absoluteString ?? "Unknown request", error.localizedDescription)
}
}
}
You can then have your controller (or whatever) (a) conform to this protocol; (b) implement the parse(_:) method of that protocol; and (c) specify itself as the parser:
BackgroundSession.shared.parser = self
Use case : Download file from a server via one API which will give the download server url(download server url vality is for 10 secs only).
I have enabled the background capabilities.
Then created one Download Manager which holds the OperationQueue for download data task.
In Operation class when main() function calls, I have makes the API call which will return the download server URL and started a download.
Works fine in debug mode but does not work in release mode.
Download Manager :
final class SCDownloadManager: NSObject {
#objc static var shared = SCDownloadManager()
private override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(SCDownloadManager.networkDidChange(_:)), name: NSNotification.Name.init(NetworkConnectionChanged), object: nil)
}
/// Dictionary of operations, keyed by the `Download URL` of the `URLSessionTask`
fileprivate var operations = [URL: SCDownloadOperation]()
fileprivate var serverURLRequestMap = [URL: SCDownloadOperation]()
/// Serial NSOperationQueue for downloads
private let queue: OperationQueue = {
let _queue = OperationQueue()
_queue.name = "SCDownloadManagerQueue"
_queue.maxConcurrentOperationCount = 3
return _queue
}()
/// Delegate-based NSURLSession for DownloadManager
lazy var downloadSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.xyz.background.download")
let sessionQueue = OperationQueue()
return URLSession(configuration: configuration, delegate: self, delegateQueue: sessionQueue)
}()
lazy var dataSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.xyz.background.url")
let sessionQueue = OperationQueue()
return URLSession(configuration: configuration, delegate: self, delegateQueue: sessionQueue)
}()
#discardableResult
#objc func addDownload(_ downloadRecord: DownloadRecord) -> SCDownloadOperation {
let operation = SCDownloadOperation(withDownloadRecord: downloadRecord)
operation.delegate = self
operations[downloadRecord.downloadUrl] = operation
queue.addOperation(operation)
return operation
}
/// Cancel all queued operations
#objc func cancelAll() {
queue.cancelAllOperations()
}
#objc func networkDidChange(_ notification: Notification) {
if let reachability = notification.object as? Reachability {
if reachability.currentReachabilityStatus() == NotReachable {
cancelAll()
}
}
}
}
//MARK:- Download operation delegate
extension SCDownloadManager: SCDownloadOperationDelegate {
func getServerDownloadUrl(forRequest request: URLRequest, ofOperation operation: SCDownloadOperation) {
if let url = request.url {
serverURLRequestMap[url] = operation
}
self.dataSession.downloadTask(with: request).resume()
}
}
// MARK: URLSessionDownloadDelegate methods
extension SCDownloadManager: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
guard let sourceURL = downloadTask.originalRequest?.url else { return }
if session == dataSession {
if FileManager.default.isReadableFile(atPath: location.path) {
if let data = FileManager.default.contents(atPath: location.path) {
if let downloadOperation = serverURLRequestMap[sourceURL] {
if let serverUrl = String(data: data, encoding: .utf8), let downloadServerUrl = URL(string: serverUrl) {
self.operations.changeKey(from: downloadOperation.downloadRecord.downloadUrl, to: downloadServerUrl)
downloadOperation.downloadRecord.downloadUrl = downloadServerUrl
if let url = downloadOperation.downloadRecord.downloadUrl {
downloadOperation.downloadRecord.downloadTask = downloadSession.downloadTask(with: url)
downloadOperation.downloadRecord.downloadTask.resume()
}
}
}
do {
try FileManager.default.removeItem(at: location)
} catch {
print("error while removing file from default location")
}
}
}
} else {
operations[sourceURL]?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if session == self.downloadSession {
guard let sourceURL = downloadTask.originalRequest?.url else { return }
operations[sourceURL]?.urlSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
}
}
// MARK: URLSessionTaskDelegate methods
extension SCDownloadManager: URLSessionTaskDelegate {
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
if downloadSession == session {
DispatchQueue.main.async {
if let completionHandler = Constants.appDelegate.backgroundTransferCompletionHandler {
Constants.appDelegate.backgroundTransferCompletionHandler = nil
completionHandler()
}
}
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let sourceURL = task.originalRequest?.url else { return }
if session == dataSession {
if error == nil {
} else {
print("got error while doing API on download task")
}
} else {
operations[sourceURL]?.urlSession(session, task: task, didCompleteWithError: error)
operations.removeValue(forKey: sourceURL)
}
}
}
Operation Class :
class SCDownloadOperation: SCAsynchronousOperation {
let downloadRecord: DownloadRecord
var getServerDownloadUrl: ((URLRequest)->Void)?
weak var delegate: SCDownloadOperationDelegate?
init(withDownloadRecord dr: DownloadRecord) {
downloadRecord = dr
super.init()
}
override func cancel() {
if let task = downloadRecord.downloadTask{
task.cancel()
}
super.cancel()
}
override func main() {
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
let configuration = RequestConfiguration(withMethodType: .GET, urlString: downloadRecord.downloadUrl.absoluteString)
if let request = NetworkManager.sharedInstance.urlRequest(forRequestConfiguration: configuration) {
delegate?.getServerDownloadUrl(forRequest: request, ofOperation: self)
}
default: break
}
}
}
// MARK: NSURLSessionDownloadDelegate methods
extension SCDownloadOperation: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
downloadRecord.progress = 1
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
processDownloadFinish(atLocation: location)
default: break
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
downloadRecord.isExecuting = true
downloadRecord.isDownloadProgressAvailable = true
let downloadProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
downloadRecord.progress = downloadProgress
downloadRecord.percentComplete = ((Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))*100)
downloadRecord.downloadProgress = downloadProgress
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
updateWrittenData()
default: break
}
}
}
// MARK: NSURLSessionTaskDelegate methods
extension SCDownloadOperation: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
completeOperation()
if error != nil {
print("\(String(describing: error))")
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
contentDownloadFail()
default: break
}
}
}
}
In the following code, the file downloads just fine. However none of the delegate methods seem to be called as I receive no output whatsoever. the progressView is not updated either. Any idea why?
import Foundation
import UIKit
class Podcast: PFQueryTableViewController, UINavigationControllerDelegate, MWFeedParserDelegate, UITableViewDataSource, NSURLSessionDelegate, NSURLSessionDownloadDelegate {
func downloadEpisodeWithFeedItem(episodeURL: NSURL) {
var request: NSURLRequest = NSURLRequest(URL: episodeURL)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
var downloadTask = session.downloadTaskWithURL(episodeURL, completionHandler: { (url, response, error) -> Void in
println("task completed")
if (error != nil) {
println(error.localizedDescription)
} else {
println("no error")
println(response)
}
})
downloadTask.resume()
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
println("didResumeAtOffset")
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
var downloadProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
println(Float(downloadProgress))
println("sup")
epCell.progressView.progress = Float(downloadProgress)
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
println(location)
}
}
From my testing, you have to choose whether you want to use a delegate or a completion handler - if you specify both, only the completion handler gets called. This code gave me running progress updates and the didFinishDownloadingToURL event:
func downloadEpisodeWithFeedItem(episodeURL: NSURL) {
let request: NSURLRequest = NSURLRequest(URL: episodeURL)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
let downloadTask = session.downloadTaskWithURL(episodeURL)
downloadTask.resume()
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
println("didResumeAtOffset: \(fileOffset)")
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
var downloadProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
println("downloadProgress: \(downloadProgress)")
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
println("didFinishDownloadingToURL: \(location)")
println(downloadTask)
}
From the NSURLSession documentation, here's the relevant section:
Like most networking APIs, the NSURLSession API is highly asynchronous. It returns data in one of two ways, depending on the methods you call:
To a completion handler block that returns data to your app when a transfer finishes successfully or with an error.
By calling methods on your custom delegate as the data is received.
By calling methods on your custom delegate when download to a file is complete.
So by design it returns data to either a completion handler block or a delegate. But as evinced here, not both.
Interestingly, Apple specifically explains this behavior in their NSURLSessionDataDelegate (but neither in the base delegate NSURLSessionTaskDelegate nor in NSURLSessionDownloadDelegate)
NOTE
An NSURLSession object need not have a delegate. If no delegate is assigned, when you create tasks in that session, you must provide a completion handler block to obtain the data.
Completion handler block are primarily intended as an alternative to using a custom delegate. If you create a task using a method that takes a completion handler block, the delegate methods for response and data delivery are not called.
Swift 3
class ViewController: UIViewController {
var urlLink: URL!
var defaultSession: URLSession!
var downloadTask: URLSessionDownloadTask!
}
// MARK: Button Pressed
#IBAction func btnDownloadPressed(_ sender: UIButton) {
let urlLink1 = URL.init(string: "https://github.com/VivekVithlani/QRCodeReader/archive/master.zip")
startDownloading(url: urlLink!)
}
#IBAction func btnResumePressed(_ sender: UIButton) {
downloadTask.resume()
}
#IBAction func btnStopPressed(_ sender: UIButton) {
downloadTask.cancel()
}
#IBAction func btnPausePressed(_ sender: UIButton) {
downloadTask.suspend()
}
func startDownloading (url:URL) {
let backgroundSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "backgroundSession")
defaultSession = Foundation.URLSession(configuration: backgroundSessionConfiguration, delegate: self, delegateQueue: OperationQueue.main)
downloadProgress.setProgress(0.0, animated: false)
downloadTask = defaultSession.downloadTask(with: urlLink)
downloadTask.resume()
}
// MARK:- URLSessionDownloadDelegate
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("File download succesfully")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
downloadProgress.setProgress(Float(totalBytesWritten)/Float(totalBytesExpectedToWrite), animated: true)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
downloadTask = nil
downloadProgress.setProgress(0.0, animated: true)
if (error != nil) {
print("didCompleteWithError \(error?.localizedDescription)")
}
else {
print("The task finished successfully")
}
}