ios - 使用 ReachabilitySwift 时无法重新加载表

标签 ios swift uitableview

我正在尝试使用 Alamofire 上传图像。另外,我正在使用 ReachabilitySwift 来了解互联网连接的状态。现在,当我尝试上传图像并在中间关闭网络时,我会删除所有 alamofire 请求。下面是代码:

let sessionManager = Alamofire.SessionManager.default
    sessionManager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
        dataTasks.forEach { $0.cancel() }
        uploadTasks.forEach { $0.cancel() }
        downloadTasks.forEach { $0.cancel() }
    }

当互联网再次打开时,我再次开始上传过程。代码如下:

func internetAvailable(){
    DispatchQueue.global(qos: .userInitiated).async {
        DispatchQueue.main.async{                                    
            self.uploadImage()
        }
    }
}

func uploadImagesToServer(){
    DispatchQueue.main.async{                                    
        self.uploadImage()
    }
}

首先在viewDidLoad中,uploadImagesToServer()被调用。在该方法的中间,当它仍在上传图像时,互联网被关闭。当互联网重新打开时,它会转到 internetAvailable(),上传图像,但是当我尝试重新加载表格时,它会转到 numberOfRowsInSection,但不在 cellForRow方法。

以下是我尝试过的方法:

  1. 检查了 numberOfRowsInSection 中的计数,正确。
  2. 尝试在主线程中使用以下方法调用tableview.reloadData():

    DispatchQueue.main.async {
        self.tableView.reloadData()
    }
    

表格 View 代码:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return numberOfImages
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //places image onto imageView
}

如有任何帮助,我们将不胜感激。

最佳答案

可达性的替代方案是使用后台 URLSession 进行上传。这样,您无需对 Reachability 执行任何操作。重新建立连接后,您启动的上传将自动发送(即使您的应用当时未运行)。

后台 session 涉及一些限制:

  1. 上传任务必须使用基于文件的上传(不是DataStream)。这意味着一旦您构建了请求,您必须在上传之前将其保存到文件中。 (如果您构建分段上传请求,Alamofire 会为您执行此操作。)

  2. 后台 session 的整体思想是,即使您的应用程序已暂停(或终止),它们也会继续运行。因此,您不能使用我们非常熟悉的完成处理程序模式(因为在上传请求时这些闭包可能已被丢弃)。因此,您必须依靠 SessionDelegatetaskDidComplete 关闭来确定请求是否成功完成。

  3. 您必须在应用委托(delegate)中实现handleEventsForBackgroundURLSession,以保存完成处理程序。如果上传完成时您的应用程序未运行,操作系统将调用此方法,必须在处理完成后调用该方法。您必须向 Alamofire 提供此完成处理程序,以便它可以为您执行此操作。

    如果您忽略保存此完成处理程序(因此如果 Alamofire 无法代表您调用它),您的应用程序将被立即终止。请务必保存此完成处理程序,以便您的应用程序可以在上传完成后透明地执行所需操作,然后再次正常挂起。

  4. 只是警告您:如果用户强制退出您的应用程序(双击主页按钮并向上滑动应用程序),这将取消任何待处理的后台上传。下次启动应用程序时,它会通知您所有已取消的任务。

    但是,如果用户刚刚离开您的应用程序(例如,只需点击主页按钮),则任何待处理的上传都将成功保留。您的应用程序甚至可能在其正常生命周期中终止(例如,由于用户随后可能使用的其他应用程序的内存压力),并且上传不会被取消。而且,当重新建立网络连接时,这些上传将开始,并且您的应用程序将以后台模式启动,以在上传完成时通知您。

但我做了类似以下的操作,重新建立连接时会自动发送上传内容。

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)

例如,如果您希望在上传完成时通知您的 View Controller ,则可以使用 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)
            }
        }
    }
}

这只是记录结果,但您可以随意做任何您想做的事情。

关于ios - 使用 ReachabilitySwift 时无法重新加载表,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45896877/

相关文章:

ios - iOS:将大区域设置为MKMapView时应用崩溃

swift - didBeginContact 未调用 GameOver 场景。有什么需要补充的吗?

ios - 我在 Xcode 上创建 webView 时遇到问题。模拟器似乎是空白的白页。我应用了应用程序传输安全

swift - 图像无法在表格 View 中显示

xcode - 没有名为 dequeueReusableCellWithIdentifier 的成员

ios - 如何在 iOS 7 中通过游戏中心应用程序邀请玩家?

ios - 为什么某些 iOS Assets 无法以视网膜分辨率显示?

ios - 如何以编程方式更改锚定样式约束?

ios - 包含重复单元格的 Collection View

arrays - 索引 0 超出空数组的范围,有时有效,有时无效