Cancel a completion block
Asked Answered
D

4

5

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?

Dov answered 29/4, 2016 at 9:24 Comment(7)
how are you retaining the completion block and cancelling the activity?Chicoine
I am asking is it possible to do so.. To cancel the completion closure's activity..Dov
not the cancel the completion block once it's running, you need to not call the completion block...Chicoine
you can cancel whole task or request to apicall.Ostrowski
You can wrap your completion block into NSBlockOperation. It has support for cancellation.Scathing
there is a dumb way to simulate it, add a var shouldIBeDead : Bool, and set it true once you hit cancel, and on the completion block call use guard shouldIBeDead else{//execute code}Agouti
Since somebody up-voted this comment: @AlexanderDoloz NSBlockOperation is not appropriate since the OPs "task" is asynchronous. A NSBlockOperation executes one or more synchronous tasks. Additionally, there's no built-in mechanism for NSOperations which transfer the result of one operation to the dependent operation. It's possible to workaround this, but it's unduly elaborate and error prone.Fishback
Z
1

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

Zendejas answered 1/5, 2016 at 16:55 Comment(1)
I have a use case where I am searching, just like an auto complete text field. Now if I type four characters at once, there will be 4 API calls. I only want to handle the response of the fourth API call and cancel out every other. Is it possible?Convince
M
5

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.

Mealymouthed answered 29/4, 2016 at 13:40 Comment(0)
C
2

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.

Crosseye answered 29/4, 2016 at 13:29 Comment(0)
Z
1

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

Zendejas answered 1/5, 2016 at 16:55 Comment(1)
I have a use case where I am searching, just like an auto complete text field. Now if I type four characters at once, there will be 4 API calls. I only want to handle the response of the fourth API call and cancel out every other. Is it possible?Convince
C
-1

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!

Custer answered 1/5, 2016 at 16:35 Comment(1)
no NSURLSessionTask is mentioned. Could you please write a universal solution?Miltonmilty

© 2022 - 2024 — McMap. All rights reserved.