Отменить URLSession на основе ввода пользователя в Swift

Я централизованно вызываю API для своего приложения в классе под названием APIService. Вызовы выглядят так, как показано ниже:

// 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()
    }

Я выполняю вызовы API из любого места в приложении, например:

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)")
            }
        })
    }

Чего я хотел бы добиться сейчас, так это выполнить func searchConversations для каждого символа, нажатого пользователем при вводе searchString. Это было бы достаточно просто, просто вызвав func searchConversations на основе запуска UIPressesEvent. Вот так:

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

Моя проблема сейчас заключается в следующем: всякий раз, когда вводится новый символ, я хотел бы отменить предыдущую сессию URL и запустить новую. Как я могу сделать это изнутри обработчика UIPressesEvent?


person Bernhard Engl    schedule 10.04.2020    source источник
comment
Взгляните на следующий ответ: Ссылка   -  person Alan S    schedule 10.04.2020
comment
Небольшое несвязанное наблюдение, но обычно мы не проверяем 200 (например, httpResponse.statusCode == 200, а скорее любой код 2xx (например, 200..<300 ~= httpResponse.statusCode). Успехом считается любой код 2xx, а не только 200. Возможно, учитывая ваш текущий веб-сервис, 200 достаточно, но 2xx разумно.   -  person Rob    schedule 10.04.2020


Ответы (2)


Основная идея состоит в том, чтобы убедиться, что API возвращает объект, который позже может быть отменен, если это необходимо, а затем изменить процедуру поиска, чтобы убедиться, что любой ожидающий запрос отменяется, если таковой имеется:

  1. Во-первых, сделайте так, чтобы ваш вызов API возвращал объект URLSessionTask:

    @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. Пусть ваша процедура поиска отслеживает последнюю задачу, отменяя ее при необходимости:

    private weak var previousTask: URLSessionTask?
    
    func searchConversations(searchString: String) {
        previousTask?.cancel()
        previousTask = apiService.getConversations(searchString: searchString) { result in
            ...
        }
    }
    
  3. Мы часто добавляем небольшую задержку, чтобы, если пользователь быстро печатает, мы избегали множества ненужных сетевых запросов:

    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. Другое дело, что вы, вероятно, захотите изменить обработчик ошибок сетевого запроса, чтобы «отмена» запроса не обрабатывалась как ошибка. С точки зрения URLSession отмена — это ошибка, но с точки зрения нашего приложения отмена — это не состояние ошибки, а скорее ожидаемый поток.

person Rob    schedule 10.04.2020
comment
Большое спасибо за вашу помощь и структурированный формат, в котором вы предоставляете свой ответ !!! Это звучит именно то, что мне нужно, чтобы решить мою проблему. Я попробую как можно скорее. - person Bernhard Engl; 11.04.2020

Вы можете добиться этого, используя таймер,

1) Определите переменную таймера

var requestTimer: Timer?

2) Обновите функцию searchConversations.

@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) Обновить 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)
    }
}
person Thilina Hewagama    schedule 10.04.2020
comment
Спасибо за ваш вклад. Может быть, я неправильно читаю, но я не понимаю, как это действительно отменяет какие-либо старые запросы. Похоже, все, что он делает, это немного ждет, прежде чем запустить новый, вместо того, чтобы отменить старый и запустить новый. - person Bernhard Engl; 10.04.2020