可访问性的替代方法是使用背景 URLSession 进行上传。这样,您无需对 Reachability 做任何事情。当重新建立连接时,您启动的上传将自动发送(即使您的应用当时没有运行)。
后台会话涉及一些限制:
-
上传任务必须使用基于文件的上传(不是Data 或Stream)。这意味着一旦您构建了您的请求,您必须在上传之前将其保存到文件中。 (如果您构建一个分段上传请求,Alamofire 会为您执行此操作。)
-
后台会话的整体理念是,即使您的应用已暂停(或终止),它们仍将继续运行。因此,您不能使用我们非常熟悉的完成处理程序模式(因为这些闭包可能在请求上传时已被丢弃)。因此,您必须依赖,例如,SessionDelegate 的 taskDidComplete 闭包来确定请求是否成功完成。
-
您必须在您的应用委托中实现handleEventsForBackgroundURLSession,保存完成处理程序。如果上传完成时您的应用程序没有运行,操作系统将调用此方法,必须在处理完成时调用该方法。您必须将此完成处理程序提供给 Alamofire,以便它可以为您执行此操作。
如果您忽略保存此完成处理程序(因此如果 Alamofire 无法代表您调用它),您的应用程序将立即终止。确保保存此完成处理程序,以便您的应用可以在上传完成后透明地执行所需的操作,然后再次正常挂起。
-
提醒您:如果用户强行退出您的应用(双击主页按钮并向上滑动应用),这将取消所有待处理的后台上传。下次启动应用程序时,它会通知您任何已取消的任务。
但如果用户只是离开您的应用程序(例如,只是点击主页按钮),任何待处理的上传都将成功保存。您的应用程序甚至可能在其正常生命周期内终止(例如,由于用户随后可能使用的其他应用程序的内存压力),并且上传不会被取消。而且,当网络连接重新建立时,这些上传将开始,您的应用将以后台模式启动,以便在完成时通知您。
但我已经做了类似以下的事情,并且在重新建立连接时会自动发送上传。
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)
}
}
然后您可以使用multipart/form-data 在后台上传文件,如下所示:
let parameters = ["foo": "bar"]
guard let imageData = UIImagePNGRepresentation(image) else { ... }
BackgroundSession.shared.uploadMultipart(url: url, parameters: parameters, fileData: imageData, filename: "test.png", name: "image")
或者您可以像这样使用 JSON 在后台上传文件:
let parameters = [
"foo": "bar",
"image": imageData.base64EncodedString()
]
BackgroundSession.shared.uploadJSON(url: url, parameters: parameters)
例如,如果您希望在上传完成时通知您的视图控制器,您可以使用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)
}
}
}
}
这只是记录结果,但您可以随意做任何您想做的事情。