iOS
iOS

Reputation: 3626

Cancel a completion block

Quick question.

Usually, we make web call and on obtaining response, we return the data in a completion block as below

func someAPIcall(input: input, completion: (result: data) -> Void) {
    ...
    completion(data)
}

and I make use of the function as below

someAPIcall(input) {
    (result: data) in
    print(result)
    // Update UI, etc
}

Is it possible to cancel the completion block somehow, at any later point of time? Say, if I make the web call and popViewController immediately, though if I cancel the request, if the data is returned to the completion block, the completion task is executed.

Is there any mechanism by which I could assign a var to the closure and could cancel that later?

How could I cancel the block from executing when I require, say in viewWillDisappear?

Upvotes: 5

Views: 5050

Answers (4)

gnasher729
gnasher729

Reputation: 52602

There is nothing that lets you directly cancel any block. So the block will execute when it is called. However, the block can of course execute code that figures out if whatever action it was supposed to perform is still needed.

Often you would just have a weak reference to some object in your block, and not perform an action if that weak reference is nil. In other cases you would check some property of an object. A straightforward method that always works: Create a trivial class with a single instance property "cancelled". Create and get hold of an instance, let the block refer to it, when you want to cancel the block set the "cancelled" property to true, and the callback checks that setting. (Again, you could make it a weak reference, and if a caller isn't interested anymore, they can just let go of that instance).

Upvotes: 1

user498982
user498982

Reputation:

There are many different ways to do this. The right answer depends on your particular use case, and the way you've designed you API interaction. NSOperations have a great cancel / dependency management / completion workflow, so if you're able to place your API interactions in an NSOperationQueue that may be the best way forward. Another possibility I use for some more simple interactions is to simply keep a reference to the NSURLSessionTasks that correspond to a particular view interaction of view controller, and cancel them as needed. For example:

//: Playground - noun: a place where people can play

import UIKit

class MyViewController: UIViewController, UISearchBarDelegate {

    var tasks = [NSURLSessionTask]()
    let client = MyAPIClient()

    deinit {
        cancelAllTasks()
    }

    func cancelAllTasks() {
        tasks.forEach { $0.cancel() }
    }

    func cancelAllSearchTasks() {
        tasks.filter({ $0.taskDescription == MyAPIClient.TaskDecription.search.rawValue }).forEach { $0.cancel() }
    }

    func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
        // Cancel previous search as user types a new search
        cancelAllSearchTasks()

        guard let text = searchBar.text else {
            return
        }

        tasks.append(client.search(text) { [weak self] results in
            // ...
        })
    }

}

class MyAPIClient {

    enum TaskDecription: String {
        case search
    }

    let session = NSURLSession()

    func search(text: String, completion: (result: [String]) -> Void) -> NSURLSessionTask {
        let components = NSURLComponents()
        components.scheme = "http"
        components.host = "myapi.com"
        components.queryItems = [NSURLQueryItem(name: "q", value: text)]
        guard let url = components.URL else {
            preconditionFailure("invalid search url")
        }

        let task = session.dataTaskWithURL(url) { (data, response, error) in
            // ...
            completion(result: ["results", "from", "api", "go", "here!"])
        }
        task.resume()
        task.taskDescription = TaskDecription.search.rawValue

        return task
    }
}

Here, when the view controller is deallocated, we cancel all the NSURLSessionTasks that are related to this controller. We also cancel any existing "search" api tasks as the user types in the search bar so that we aren't making "stale" api calls.

Of course this is a fairly simple example but you get the idea- it's good to be smart about the amount of network calls your application is making and cancel them if they are no longer needed!

Upvotes: -1

matt
matt

Reputation: 535576

You can't necessarily erase the completion block from existence, but since it's your completion block, you can easily just not do anything when it is called:

func someAPIcall(input: input, completion: (result: data) -> Void) {
    guard somethingOrOther else {return}
    // ...
    completion(data)
}

somethingOrOther might be a property of self, or (as you've already been told) you might check whether self even still exists.

This is not very different from the mechanism used by NSOperation, which can check its own cancelled property before actually doing anything.

Upvotes: 5

Mark
Mark

Reputation: 7419

My guess is you're strongly retaining self in the completion block. If you pass a weak reference to self in the capture list, your actions will not be performed if the view controller is released.

someAPIcall(input) { [weak self] result in
    guard let strongSelf = self else { return }

    strongSelf.label.text = ...
}

Note this will only work if the tasks you are executing in the block are performed on self. someAPIcall still maintains a reference to the completion block, but the completion block has a weak reference to your view controller. (Technically you could use the value of weak self to check whether to perform your other tasks).

If this is not sufficient, and you have access to the implementation of someAPIcall, then you can add a cancel() method (as others have mentioned) than will stop the call, and release the block.

Upvotes: 2

Related Questions