0

I am trying to upload an image using Alamofire. Also, I am using ReachabilitySwift to know the status of internet connection. Now when I try to upload an image and in the middle turn off the network, I remove all alamofire requests. Below is the code for that :

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

When the internet is switched on again, I start the uploading process again. Here's the code:

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

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

At first in viewDidLoad, uploadImagesToServer() get called. In the middle of that method while it's still uploading image, internet is turned off. When internet is turned back on, it goes to internetAvailable(), uploads the image but when I try to reload the table, it goes to the numberOfRowsInSection, but not in cellForRow method.

Below are the things I have tried:

  1. checked the count in numberOfRowsInSection, it's correct.
  2. tried calling tableview.reloadData() in main thread using :

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

TableView code:

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

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

Any help would be appreciated.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
apurv
  • 21
  • 5
  • show table view code as well. – Tushar Sharma Aug 26 '17 at 15:19
  • added..there's not much there. I am just placing image on imageView. – apurv Aug 26 '17 at 15:28
  • Can you upload whole viewController class code to GitHub gist or anywhere else? It is hard to determine where is error by the chunks you gave – Ilya V. Aug 26 '17 at 17:28
  • It's a little complicated, but an alternative to Reachability is to do background uploads. This way, not only do you not to do anything with Reachability, but any uploads you initiated will be sent automatically when the connectivity is reestablished (even your app isn't running at the time). [This](https://stackoverflow.com/a/26542755/1271826) describes how to do background downloads with Alamofire; the idea is largely the same with background uploads. The only trick is that you have to build the body of the request, save it to a local file, and then upload that payload as a file. – Rob Aug 26 '17 at 17:36
  • @Rob thanks for the approach. it's a bit complicated but I'll try it out. – apurv Aug 26 '17 at 17:59

1 Answers1

0

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:

  1. 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.)

  2. 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.

  3. 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.

  4. 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.

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044