1

I am trying to create a service object in my Swift application to handle requests a bit easier. I've got most of it wired up, however I might be misunderstanding completion handlers.

I have this function that simply posts to a local API endpoint I have running.

func createList(name: String, completion: @escaping (Response) -> Void) {
        let parameters = ["name": name, "user_id": session.auth.currentUser!.uid]
        
        AF.request("\(url)/wishlist", method: .post, parameters: parameters, encoding: URLEncoding.default).responseDecodable(of: Response.self) { response in
                switch response.result {
                case .failure(let err):
                    print(err)
                case .success(let res):
                    completion(res)
            }
        }
    }

All that needs to happen is I need to pass that name to the function which I do here

barback.createList(name: name) -> after a button is tapped

However, I am now getting this error.

Missing argument for parameter 'completion' in call

My goal here is to just return the Response object so I can access attributes on that to do certain things in the UI. I was not able to return res here because, from my understanding, this is an async request and it's actually returning from that competition handler? (could be butchering that). The way I saw others doing this was by adding a competition handler to the params and adding an escape to that.

My end goal here is to do something like...

if barback.createList(name: name).status = 200
    (trigger some UI component)
else
    (display error toast)
end

Is my function flawed in it's design? I've tried changing my competition handler to be

completion: (@escaping (Response) -> Void) = nil

but run into some other errors there. Any guidance here?

3 Answers3

2

Completion handlers are similar to function return values, but not the same. For example, compare the following functions:

/// 1. With return value
func createList(name: String) -> Response { }
/// 2. With completion handler
func createList(name: String, completion: @escaping (Response) -> Void) { }

In the first function, you'd get the return value instantly.

let response = barback.createList(name: name)
if response.status = 200 {
    /// trigger some UI component
}

However, if you try the same for the second function, you'll get the Missing argument for parameter 'completion' in call error. That's because, well, you defined a completion: argument label. However, you didn't supply it, as matt commented.

Think of completion handlers as "passing in" a chunk of code into the function, as a parameter. You need to supply that chunk of code. And from within that chunk of code, you can access your Response.

                                           /// pass in chunk of code here
barback.createList(name: name, completion: { response in
    /// access `response` from within block of code
    if response.status = 200 {
        /// trigger some UI component
    }
})

Note how you just say barback.createList, not let result = barback.createList. That's because in the second function, with the completion handler, doesn't have a return value (-> Response).

Swift also has a nice feature called trailing closure syntax, which lets you omit the argument label completion:.

barback.createList(name: name) { response in
    /// access `response` from within block of code
    if response.status = 200 {
        /// trigger some UI component
    }
}

You can also refer to response, the closure's first argument, by using $0 (which was what I did in my comment). But whether you use $0 or supply a custom name like response is up to you, sometimes $0 is just easier to type out.

barback.createList(name: name) {
    /// access $0 (`response`) from within block of code
    if $0.status = 200 {
        /// trigger some UI component
    }
}
aheze
  • 24,434
  • 8
  • 68
  • 125
1

Calling createList would look something more like this:

barback.createList(name: name) { response in
    if response.status == 200 {
        // OK
    } else {
        // Error
    }
}

This fixes the issue because you are now running this completion closure - { response in ... } - where response is the value you pass in. In this case, you pass in res. See this post about using completion handlers.


If you did want an optional completion handler so you don't always need to include it, you could change the definition to the following (adding = { _ in }, meaning it defaults to an empty closure):

func createList(name: String, completion: @escaping (Response) -> Void = { _ in })

Another way is actually making the closure optional:

func createList(name: String, completion: ((Response) -> Void)? = nil)

And then inside the method you need ? when you call completion, since it's optional:

completion?(res)
George
  • 25,988
  • 10
  • 79
  • 133
1

Use the completion handler as mentioned in the comments and answers. In addition you should include a completion when it fails, otherwise you will never get out of that function.

I would restructure your code to cater for any errors that might happens, like this:

func createList(name: String, completion: @escaping (Response?, Error?) -> Void) {
    let parameters = ["name": name, "user_id": session.auth.currentUser!.uid]

    AF.request("\(url)/wishlist", method: .post, parameters: parameters, encoding: URLEncoding.default).responseDecodable(of: Response.self) { response in
        switch response.result {
        case .failure(let err):
            print(err)
            completion(nil, err)   
        case .success(let res):
            completion(res, nil)
        }
    }
}

call it like this:

barback.createList(name: name) { (response, error) in
    if error != nil {
        
    } else {
        
    }
}

If you do not put a completion(...) in your "case .failure" it will never get out of there.