An alternative to Reachability is to do uploads with a background URLSession
. This way, you do not need to do anything with Reachability. The uploads you initiate will be sent automatically when the connectivity is reestablished (even your app isn't running at the time).
Background sessions involve a few limitations:
Upload tasks must use file-based uploads (not Data
or Stream
). This means that once you build your request, you have to save it to a file before uploading it. (If you build a multipart upload request, Alamofire does this for you.)
The whole idea of background sessions is that they will continue to run even if your app has been suspended (or terminated). So, you can't use the completion handler patterns that we're so familiar with (because those closures may have been discarded by the time time the request is uploaded). So, you must rely upon, for example, taskDidComplete
closure of the SessionDelegate
to determine if the request finished successfully or not.
You must implement handleEventsForBackgroundURLSession
in your app delegate, saving the completion handler. If your app is not running when the uploads finish, the OS will call this method, which must be called when the processing is done. You must supply this completion handler to Alamofire, so that it can do this for you.
If you neglect to save this completion handler (and therefore if Alamofire is unable to call it on your behalf), your app will be summarily terminated. Make sure to save this completion handler so that your app can transparently do what it needs when the uploads are done and then be gracefully suspended again.
Just to warn you: If a user force-quits your app (double tapping on home button and swiping up on the app), that will cancel any pending background uploads. The next time you start the app, it will inform you of any cancelled tasks.
But if the user just leaves your app (e.g. just taps the home button), any pending uploads will successfully survive. Your app could even terminate through its normal lifecycle (e.g. due to memory pressure of other apps the user may subsequently use), and the uploads will not be cancelled. And, when network connectivity is re-established, those uploads will commence and your app will be started in background mode to inform you when they finish.
But I've done something like the following, and the uploads are sent automatically when the connection is reestablished.
import Alamofire
import os.log
import MobileCoreServices
/// The `OSLog` which we'll use for logging
///
/// Note, this class will log via `os_log`, not `print` because it's possible that the
/// app will have terminated (and thus not connected to Xcode debugger) by the time the
/// upload is done and we want to see our log statements.
///
/// By using `os_log`, we can watch what's going on in an app that is running
/// in background on the device through the macOS `Console` app. And I specify the
/// `subsystem` and `category` to simplify the filtering of the `Console` log.
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "BackgroundSession")
/// Background Session Singleton
///
/// This class will facilitate background uploads via `URLSession`.
class BackgroundSession {
/// Singleton for BackgroundSession
static var shared = BackgroundSession()
/// Saved version of `completionHandler` supplied by `handleEventsForBackgroundURLSession`.
var backgroundCompletionHandler: (() -> Void)? {
get { return manager.backgroundCompletionHandler }
set { manager.backgroundCompletionHandler = backgroundCompletionHandler }
}
/// Completion handler that will get called when uploads are done.
var uploadCompletionHandler: ((URLSessionTask, Data?, Error?) -> Void)?
/// Alamofire `SessionManager` for background session
private var manager: SessionManager
/// Dictionary to hold body of the responses. This is keyed by the task identifier.
private var responseData = [Int: Data]()
/// Dictionary to hold the file URL of original request body. This is keyed by the task identifier.
///
/// Note, if the app terminated between when the request was created and when the
/// upload later finished, we will have lost reference to this temp file (and thus
/// the file will not be cleaned up). But the same is true with Alamofire's own temp
/// files. You theoretically could save this to (and restore from) persistent storage
/// if that bothers you.
///
/// This is used only for `uploadJSON` and `uploadURL`. The `uploadMultipart` takes
/// advantage of Alamofire's own temp file process, so we don't have visibility to
/// the temp files it uses.
private var tempFileURLs = [Int: URL]()
private init() {
let configuration = URLSessionConfiguration.background(withIdentifier: Bundle.main.bundleIdentifier!)
manager = SessionManager(configuration: configuration)
// handle end of task
manager.delegate.taskDidComplete = { [unowned self] session, task, error in
self.uploadCompletionHandler?(task, self.responseData[task.taskIdentifier], error)
if let fileURL = self.tempFileURLs[task.taskIdentifier] {
try? FileManager.default.removeItem(at: fileURL)
}
self.responseData[task.taskIdentifier] = nil
self.tempFileURLs[task.taskIdentifier] = nil
}
// capture body of response
manager.delegate.dataTaskDidReceiveData = { [unowned self] session, task, data in
if self.responseData[task.taskIdentifier] == nil {
self.responseData[task.taskIdentifier] = data
} else {
self.responseData[task.taskIdentifier]!.append(data)
}
}
}
let iso8601Formatter = ISO8601DateFormatter()
/// Submit multipart/form-data request for upload.
///
/// Note, Alamofire's multipart uploads automatically save the contents to a file,
/// so this routine doesn't do that part.
///
/// Alamofire's implementation begs a few questions:
///
/// - It would appear that Alamofire uses UUID (so how can it clean up the file
/// if the download finishes after the app has been terminated and restarted ...
/// it doesn't save this filename anywhere in persistent storage, AFAIK); and
///
/// - Alamofire uses "temp" directory (so what protection is there if there was
/// pressure on persistent storage resulting in the temp folder being purged
/// before the download was done; couldn't that temp folder get purged before
/// the file is sent?).
///
/// This will generate the mimetype on the basis of the file extension.
///
/// - Parameters:
/// - url: The `URL` to which the request should be sent.
/// - parameters: The parameters of the request.
/// - fileData: The contents of the file being included.
/// - filename: The filename to be supplied to the web service.
/// - name: The name/key to be used to identify this file on the web service.
func uploadMultipart(url: URL, parameters: [String: Any], fileData: Data, filename: String, name: String) {
manager.upload(multipartFormData: { multipart in
for (key, value) in parameters {
if let string = value as? String {
if let data = string.data(using: .utf8) {
multipart.append(data, withName: key)
}
} else if let date = value as? Date {
let string = self.iso8601Formatter.string(from: date)
if let data = string.data(using: .utf8) {
multipart.append(data, withName: key)
}
} else {
let string = "\(value)"
if let data = string.data(using: .utf8) {
multipart.append(data, withName: key)
}
}
multipart.append(fileData, withName: name, fileName: filename, mimeType: self.mimeType(for: URL(fileURLWithPath: filename)))
}
}, to: url, encodingCompletion: { encodingResult in
switch(encodingResult) {
case .failure(let error):
os_log("encodingError: %{public}@", log: log, type: .error, "\(error)")
case .success:
break
}
})
}
/// Determine mime type on the basis of extension of a file.
///
/// This requires MobileCoreServices framework.
///
/// - parameter url: The file `URL` of the local file for which we are going to determine the mime type.
///
/// - returns: Returns the mime type if successful. Returns application/octet-stream if unable to determine mime type.
private func mimeType(for url: URL) -> String {
let pathExtension = url.pathExtension
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue(),
let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
return mimetype as String
}
return "application/octet-stream";
}
/// Submit JSON request for upload.
///
/// - Parameters:
/// - url: The `URL` to which the request should be sent.
/// - parameters: The parameters of the request.
func uploadJSON(url: URL, parameters: [String: Any]) {
upload(url: url, parameters: parameters, encoding: JSONEncoding.default)
}
/// Submit `x-www-form-urlencoded` request for upload.
///
/// - Parameters:
/// - url: The `URL` to which the request should be sent.
/// - parameters: The parameters of the request.
func uploadURL(url: URL, parameters: [String: Any]) {
upload(url: url, parameters: parameters, encoding: URLEncoding.default)
}
/// Starts a request for the specified `urlRequest` to upload a file.
///
/// - Parameters:
/// - fileURL: The file `URL` of the file on your local file system to be uploaded.
/// - urlRequest: The `URLRequest` of request to be sent to remote service.
func uploadFile(fileURL: URL, with urlRequest: URLRequest) {
manager.upload(fileURL, with: urlRequest)
}
/// Starts a request for the specified `URL` to upload a file.
///
/// - Parameters:
/// - fileURL: The file `URL` of the file on your local file system to be uploaded.
/// - url: The `URL` to be used when preparing the request to be sent to remote service.
func uploadFile(fileURL: URL, to url: URL) {
manager.upload(fileURL, to: url)
}
/// Submit request for upload.
///
/// - Parameters:
/// - url: The `URL` to which the request should be sent.
/// - parameters: The parameters of the request.
/// - encoding: Generally either `JSONEncoding` or `URLEncoding`.
private func upload(url: URL, parameters: [String: Any], encoding: ParameterEncoding) {
let request = try! URLRequest(url: url, method: .post)
var encodedRequest = try! encoding.encode(request, with: parameters)
let fileURL = BackgroundSession.tempFileURL()
guard let data = encodedRequest.httpBody else {
fatalError("encoding failure")
}
try! data.write(to: fileURL)
encodedRequest.httpBody = nil
let actualRequest = manager.upload(fileURL, with: encodedRequest)
if let task = actualRequest.task {
tempFileURLs[task.taskIdentifier] = fileURL
}
}
/// Create URL for temporary file to hold body of request.
///
/// - Returns: The file `URL` for the temporary file.
private class func tempFileURL() -> URL {
let folder = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(Bundle.main.bundleIdentifier! + "/BackgroundSession")
try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true, attributes: nil)
return folder.appendingPathComponent(UUID().uuidString)
}
}
You can then upload a file in the background using multipart/form-data
like so:
let parameters = ["foo": "bar"]
guard let imageData = UIImagePNGRepresentation(image) else { ... }
BackgroundSession.shared.uploadMultipart(url: url, parameters: parameters, fileData: imageData, filename: "test.png", name: "image")
Or you can upload a file in the background using JSON like so:
let parameters = [
"foo": "bar",
"image": imageData.base64EncodedString()
]
BackgroundSession.shared.uploadJSON(url: url, parameters: parameters)
If you want, for example, your view controller to be informed as uploads finish, you can do use uploadCompletionHandler
:
override func viewDidLoad() {
super.viewDidLoad()
BackgroundSession.shared.uploadCompletionHandler = { task, data, error in
if let error = error {
os_log("responseObject: %{public}@", log: log, type: .debug, "\(error)")
return
}
if let data = data {
if let json = try? JSONSerialization.jsonObject(with: data) {
os_log("responseObject: %{public}@", log: log, type: .debug, "\(json)")
} else if let string = String(data: data, encoding: .utf8) {
os_log("responseString: %{public}@", log: log, type: .debug, string)
}
}
}
}
That's only logging the results, but you can feel free to do whatever you want.