Bernhard Engl
Bernhard Engl

Reputation: 271

Cancel a URLSession based on user input in Swift

I've centralized API calls for my App in a class called APIService. Calls look like the one below:

// GET: Attempts getconversations API call. Returns Array of Conversation objects or Error
    func getConversations(searchString: String = "", completion: @escaping(Result<[Conversation], APIError>) -> Void) {

        {...} //setting up URLRequest

        let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let _ = data
                else {
                    print("ERROR: ", error ?? "unknown error")
                    completion(.failure(.responseError))
                    return
            }
            do {

                {...} //define custom decoding strategy

                }
                let result = try decoder.decode(ResponseMultipleElements<[Conversation]>.self, from: data!)
                completion(.success(result.detailresponse.element))
            }catch {
                completion(.failure(.decodingError))
            }
        }
        dataTask.resume()
    }

I'm executing API calls from anywhere in the Application like so:

func searchConversations(searchString: String) {
        self.apiService.getConversations(searchString: searchString, completion: {result in
            switch result {
            case .success(let conversations):
                DispatchQueue.main.async {
                    {...} // do stuff 
                }
            case .failure(let error):
                print("An error occured \(error.localizedDescription)")
            }
        })
    }

What I would like to achieve now is to execute func searchConversations for each character tapped by the user when entering searchString. This would be easy enough by just calling func searchConversations based on a UIPressesEvent being fired. Like so:

override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        guard let key = presses.first?.key else { return }

        switch key.keyCode {
        {...} // handle special cases
        default:
            super.pressesEnded(presses, with: event)
            searchConversations(searchString: SearchText.text)
        }
    }

My problem is this now: Whenever a new character is entered, I'd like to cancel the previous URLSession and kick-off a new one. How can I do that from inside the UIPressesEvent handler?

Upvotes: 1

Views: 1392

Answers (2)

Rob
Rob

Reputation: 437542

The basic idea is to make sure the API returns an object that can later be canceled, if needed, and then modifying the search routine to make sure to cancel any pending request, if any:

  1. First, make your API call return the URLSessionTask object:

    @discardableResult
    func getConversations(searchString: String = "", completion: @escaping(Result<[Conversation], APIError>) -> Void) -> URLSessionTask {
        ...
    
        let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
            ...
        }
        dataTask.resume()
        return dataTask
    }
    
  2. Have your search routine keep track of the last task, canceling it if needed:

    private weak var previousTask: URLSessionTask?
    
    func searchConversations(searchString: String) {
        previousTask?.cancel()
        previousTask = apiService.getConversations(searchString: searchString) { result in
            ...
        }
    }
    
  3. We frequently add a tiny delay so that if the user is typing quickly we avoid lots of unnecessary network requests:

    private weak var previousTask: URLSessionTask?
    private weak var delayTimer: Timer?
    
    func searchConversations(searchString: String) {
        previousTask?.cancel()
        delayTimer?.invalidate()
    
        delayTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { [weak self] _ in
            guard let self = self else { return }
            self.previousTask = self.apiService.getConversations(searchString: searchString) {result in
                ...
            }
        }
    }
    
  4. The only other thing is that you probably want to change your network request error handler so that the “cancel” of a request isn’t handled like an error. From the URLSession perspective, cancelation is an error, but from our app’s perspective, cancelation is not an error condition, but rather an expected flow.

Upvotes: 2

Thilina Chamath Hewagama
Thilina Chamath Hewagama

Reputation: 9040

You can achieve this by using a timer,

1) Define a timer variable

var requestTimer: Timer?

2) Update searchConversations function

@objc func searchConversations() {
    self.apiService.getConversations(searchString: SearchText.text, completion: {result in
        switch result {
        case .success(let conversations):
            DispatchQueue.main.async {
                {...} // do stuff
            }
        case .failure(let error):
            print("An error occured \(error.localizedDescription)")
        }
    })
}

3) Update pressesEnded

override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {

    guard let key = presses.first?.key else { return }

    switch key.keyCode {
    {...} // handle special cases
    default:
        super.pressesEnded(presses, with: event)
        self.requestTimer?.invalidate()
        self.requestTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(searchConversations), userInfo: nil, repeats: false)
    }
}

Upvotes: 0

Related Questions