JasonP JasonP - 1 year ago 57
Swift Question

Empty closure for asynchronous calls

Recently Ive been working on something that requires a UI to be updated once a function gets desired information from a web request.

I found out that passing in an empty closure in this function and then calling the closure in the same function allows me to update the UI after the data is downloaded (before I just tried to update the UI without a closure and the program crashed because the data was still being downloaded).

first I created a type alias:

typealias DLComplete = () -> ()

Here is what the function looked like:

func DLDetails(completed: DLComplete) {

let url = "string"
Alamofire.request(GET, url).responseJSON { response in

//Getting all the data I need and putting them in variables

} completed()

Then in my view controller:

ViewDidLoad() {
DLDetails() {
//call function that updates UI

So basically, Im wondering, why does creating an empty closure like this allow the program to first download the data, and once the data is downloaded, then update the UI. How is everything working?

How is calling the empty closure in my
function allowing me to call this function in my VC that opens another closure that lets me call the update UI function?

Im new to closures so I'm not even sure how
() -> ()
allows me to call the update UI function in my view controller after the data is downloaded.

Rob Rob
Answer Source

You say:

I am having trouble understanding what () -> () is doing or what (String?, NSError?) -> () is doing.

Those constructs, themselves, aren't doing anything. It's just defining a closure, a piece of code that can be passed from one method to another. In this case the idea is that viewDidLoad can say "start some asynchronous network request, go ahead and return immediately so the app doesn't block the main thread (i.e. doesn't freeze the user interface), but here is a a piece of code you can call when you're done so I can update my UI when the asynchronous request is done."

So, the () -> () is saying that this variable will hold a closure. In () -> () the closure provided by viewDidLoad is defined to be one that takes no parameters and returns no values. In (String?, NSError?) -> (), it's saying the closure will be passed two parameters, optional string and error references, but return no value. In short, it's giving the download method an opportunity to pass back the string value if the request was successful, or an error object if the request failed. So, viewDidLoad provides the closure, and the download method is responsible for calling the closure when the asynchronous request is done.

You ask:

I'm wondering, why does creating an empty closure like this allow the program to first download the data, and once the data is downloaded, then update the UI. How is everything working?

It's all a matter of timing involved in asynchronous methods. Alamofire's responseJSON method returns immediately, but its trailing closure is called asynchronously (i.e. later, after the request finishes). So, if you want to trigger a UI update in your view controller, you adopt your own completion handler pattern in your DLDetails method, and you call its completion handler only when the responseJSON completion handler is called.

BTW, in your Alamofire example, make sure to put completed() inside the responseJSON closure, not after it like shown in your code snippet. The idea is to call your closure when the request is done, and if you don't put it inside the responseJSON closure, it will get called prematurely, before the request finishes.

You might consider not updating your model directly inside DLDetails, but rather define completed to pass back the retrieved data. For example if returning a string, DLComplete would be (String?) -> () (e.g. pass String if request succeeds, and return nil if not). You might also pass back an ErrorType or NSError reference, too, so if there was an error, the view controller has an opportunity to present the UI appropriate for the particular type of error (e.g. an authentication error might trigger a re-authentication flow, whereas network connectivity errors might trigger a different UI).

typealias DownloadCompletion = (String?, NSError?) -> ()

func downloadDetails(completionHandler: DownloadCompletion) {
    let url = "string"
    Alamofire.request(.GET, url)
        .responseJSON { response in
            switch response.result {
            case .Failure(let error):
                // handle errors (including `validate` errors) here
                completionHandler(nil, error)
            case .Success(let value):
                // handle success here, retrieving/parsing the necessary value(s)
                let string = ...
                completionHandler(string, nil)

And then in your view controller:

override func viewDidLoad() {
    downloadDetails() { responseString, error in
        guard let string = responseString else {
            // handle failure here

        // do something with `string` here, e.g update model and/or UI

    // But don't try to use `responseString` or `error` here, because the above
    // closure runs asynchronously (i.e. later), and will not have been called by
    // the time we get here.

Clearly, the parsing in downloadDetails is likely to be more complicated than parsing out a simple String, so just change the first parameter(s) of your closure to be whatever data is appropriate in your case.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download