0

My app will need to download files from my website from several different places in the app, so it seems to make sense to write the function to accomplish the download once, put it in its own class, and call that function from each ViewController. So far, so good, things work. The download is happening, and the downloaded file will print correctly.

The problem comes when the download function goes to send a "success" or "failed" message back to the ViewController that called it, so that the VC can then react accordingly -- update the display, close out the download dialog, whatever. How to make that happen is where I'm stuck.

What I have:

Each of ViewControllerTwo and ViewControllerThree (which are identical for now, other than requesting different files from my server) calls the download function thus:

Downloader.load(url: urlForFileA!, to: localDestinationFileA, callingViewControllerNumber: 2)

The code for the downloader function (which is currently synchronous, but will eventually become asynchronous) looks like this (in its own Downloader class):

class func load(url: URL, to localUrl: URL, callingViewControllerNumber: Int) {
    let sessionConfig = URLSessionConfiguration.default
    let session = URLSession(configuration: sessionConfig)
    let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData)

    let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
        if let tempLocalUrl = tempLocalUrl, error == nil {
            // Got a file, might be a 404 page...
            if let statusCode = (response as? HTTPURLResponse)?.statusCode {
                print("Success downloading: \(statusCode)")
                if statusCode == 404 {
                    // ERROR -- FILE NOT FOUND ON SERVER
                    returnToCaller(sourceIdent: callingViewControllerNumber, successStatus: .fileNotFound, errorMessage: "File not Found, 404 error")
                }
            }
            do {
                try FileManager.default.copyItem(at: tempLocalUrl, to: localUrl)
                // SUCCESS!
                returnToCaller(sourceIdent: callingViewControllerNumber, successStatus: .success, errorMessage: "")
            } catch (let writeError) {
                returnToCaller(sourceIdent: callingViewControllerNumber, successStatus: .movingError, errorMessage: "\(writeError)")
            }
        } else {
            returnToCaller(sourceIdent: callingViewControllerNumber, successStatus: .downloadFailed, errorMessage: "Grave Unexplained Failure")
        }
    }
    task.resume()
}

This part works.

The returnToCaller function is an admittedly ugly (okay, very, very ugly) way to send something back to the calling ViewController:

class func returnToCaller(sourceIdent : Int, successStatus : downloadSuccessStatusEnum, errorMessage : String) {
    switch sourceIdent {
    case 2:
        ViewControllerTwo().returnFromDownload(successStatus: successStatus, errorMessage: errorMessage)
    case 3:
        ViewControllerThree().returnFromDownload(successStatus: successStatus, errorMessage: errorMessage)
    default:
        fatalError("Unknown sourceIdent of \(sourceIdent) passed to func returnToCaller")
    }
}

The problem is that when that returnFromDownload function in the original ViewController is called, it isn't aware of anything in the VC that's loaded -- I go to change the background color of a label, and get a runtime error that the label is nil. The label exists, but this call into the ViewController code is happening in isolation from the running, instantiated VC itself. (Probably the wrong vocabulary there -- apologies.) The code runs and can print but errors out when interacting with anything in the View itself.

The more I work with this, the less confident I am that I'm on the right track here, and my limited experience with Swift isn't enough to see what needs to be happening so that the download function can do all its work "over here" and then return a success/failure message to the calling VC so that the VC can then work with it.

This question seems to be asking something similar; the one answer there doesn't address my (nor, I think, his) root question of how to get code within the presented VC running again with the results of what happened outside the VC (manager approval in his case, download in mine).

Not asking for my code to be rewritten (unless it's a quick fix), but needing to be pointed in the right direction. Many thanks!

Community
  • 1
  • 1
ConfusionTowers
  • 911
  • 11
  • 34
  • 1
    If I'm reading this correctly, you want - back in the day - was called "loosely coupled" code. In the pre-Swift days another name might be KVC coding. Basically, you want to try to download something *asynchronously*, and send the result back to the VC that called it. At least two ways come to mind. (1) Probably the most correct way is adding an observer to the default NSNotificationCenter (http://stackoverflow.com/questions/24049020/nsnotificationcenter-addobserver-in-swift). (2) Another way - less elegant but it may get the job done - is to use the UIButton's sendAction method. –  Apr 24 '17 at 22:36

1 Answers1

1

What you want can be accomplished pretty easily with a closure.

The first step is to add another parameter to your load method and remove your callingViewController param:

class func load(url: URL, to localUrl: URL, completion: (downloadSuccessStatusEnum, String) -> Void)

This will allow you to call completion instead of returnToCaller when your method completes like so:

DispatchQueue.main.async {

    completion(.success, "Insert your error message here")
}

Lastly, to call this method you simply need to call the method like so in your VCs:

Downloader.load(url: nameOfYourURL, to: destinationName) { statusEnum, errorString in
  // Insert your code you want after the request completes here
}
AdamPro13
  • 7,232
  • 2
  • 30
  • 28
  • Very nice! The compiler seems to suggest that the closure should be escaping, and based on [this answer](http://stackoverflow.com/questions/42214840/swift-3-closure-use-of-non-escaping-parameter-may-allow-it-to-escape) that seems to make sense. Would you agree? – ConfusionTowers Apr 24 '17 at 22:57
  • That works! What is very strange is, in the code that runs after the request completes, for a successful return I change a label's background to green, and then `print` the file contents (a couple pages of plain text), and while the print happens immediately, it's around 5+ seconds before the label changes color (in the simulator, at least). Anything I should be doing so that the View reacts immediately? Many thanks for your help! – ConfusionTowers Apr 24 '17 at 23:16
  • Check to make sure your response is being returned on the main thread. If it's not, you will want to send it to the main thread. – AdamPro13 Apr 24 '17 at 23:17
  • To check the if it's on the main thread, set a breakpoint in your VC where the response returns. When it hits that breakpoint, you can see what thread it's on in the left panel of Xcode. I'll update the answer to show how to dispatch to the main thread. – AdamPro13 Apr 24 '17 at 23:23
  • It ***was*** on a separate thread, and changing to the DispatchQueue.main.async{ ... } as per your update got rid of the lengthy delay before the label color updated. So many thanks for your help -- very much appreciated! – ConfusionTowers Apr 25 '17 at 13:45