'If you type too fast in search-bar API call errors out
One) as the user types in the search bar an api call fired updating the tableview. If the user types too fast it results in an error. I would like to see how to prevent this.
Two) I only get 500 free api calls and as a user types a new API being fired can really add up requests fairly quickly. Is there a way to possibly save the result and store is briefly to prevent multiple duplicate API calls? Is this not the correct approach?
This is the AddressResult file
// MARK: - AddressResult
struct AddressResult: Codable {
let meta: Meta
let autocomplete: [Autocomplete]
}
// MARK: - Autocomplete
struct Autocomplete: Codable {
let areaType, id: String
let score: Double
let mprID: String?
let fullAddress: [String]?
let line: String?
let city: String
let postalCode: String?
let stateCode, country: String
let centroid: Centroid?
let propStatus, validationCode: [String]?
let counties: [County]?
let slugID, geoID: String?
let countyNeededForUniq: Bool?
enum CodingKeys: String, CodingKey {
case areaType = "area_type"
case id = "_id"
case score = "_score"
case mprID = "mpr_id"
case fullAddress = "full_address"
case line, city
case postalCode = "postal_code"
case stateCode = "state_code"
case country, centroid
case propStatus = "prop_status"
case validationCode = "validation_code"
case counties
case slugID = "slug_id"
case geoID = "geo_id"
case countyNeededForUniq = "county_needed_for_uniq"
}
}
// MARK: - Centroid
struct Centroid: Codable {
let lon, lat: Double
}
// MARK: - County
struct County: Codable {
let name, fips, stateCode: String
enum CodingKeys: String, CodingKey {
case name, fips
case stateCode = "state_code"
}
}
// MARK: - Meta
struct Meta: Codable {
let build: String
}
This is the AddressTableViewCell
import UIKit
class AddressTableViewCell: UITableViewCell {
@IBOutlet weak var addressLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Here is the ViewController:
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
// MARK: - Variable Declarations
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!
var tempAddressData: [String] = []
var searchString = ""
// MARK: - ViewController LifeCycle Methods
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
initSearchController()
}
// MARK: - SearchBar Methods
func initSearchController() {
searchBar.delegate = self
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText == "" {
tempAddressData = []
} else {
searchString = searchText
fetchAddresses()
tempAddressData = []
}
tableView.reloadData()
}
// MARK: - TableView Methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tempAddressData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "address", for: indexPath) as? AddressTableViewCell else {
return UITableViewCell()
}
cell.addressLabel.text = tempAddressData[indexPath.row]
return cell
}
// MARK: - API Method
// TODO: - This will be moved to "AddressFetcher" when it is compleated.
func fetchAddresses() {
let escapedString = searchString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
//Create URL:
guard let url = URL(string: "https://realty-in-us.p.rapidapi.com/locations/auto-complete?input=\(escapedString ?? "")") else {
fatalError("Invalid url string.")
}
//create request to add headers:
var request = URLRequest.init(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = ["Content-Type" : "application/json", "X-RapidAPI-Host" : "realty-in-us.p.rapidapi.com", "X-RapidAPI-Key":"API_KEY"]
let session = URLSession.init(configuration: config)
//Create URL session data task
let task = session.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
fatalError("Unable to unwrap date from api call.")
}
do {
//Parse the JSON data
let autoCompleteResult = try JSONDecoder().decode(AddressResult.self, from: data)
//print("Successfully received the data \(autoCompleteResult.autocomplete)")
DispatchQueue.main.async {
for address in autoCompleteResult.autocomplete {
self.tempAddressData.append("\(address.line ?? "") \(address.city), \(address.stateCode) \(address.postalCode ?? "")")
self.tableView.reloadData()
}
}
} catch {
fatalError(error.localizedDescription)
}
}
task.resume()
}
}
Solution 1:[1]
As other guys mentioned, I think it's a bad practice to query the api on every stroke, use a Debouncer Use it as blue print adjust for your own needs.
Btw like libs like reactiveKit have Debouncer build in. easy to use.
class Debouncer {
var handler: (() -> Void)? {
didSet {
worker?.cancel()
if let handler = handler {
let worker = DispatchWorkItem(block: handler)
queue.asyncAfter(deadline: .now() + timeInterval, execute: worker)
self.worker = worker
}
}
}
private let timeInterval: TimeInterval
private var worker: DispatchWorkItem?
private let queue: DispatchQueue
init(timeInterval: TimeInterval, queue: DispatchQueue = .main) {
self.timeInterval = timeInterval
self.queue = queue
}
func cancel() {
worker?.cancel()
worker = nil
}
}
class Throttler {
var handler: (() -> Void)? {
didSet {
if worker == nil {
let worker = DispatchWorkItem { [weak self] in
self?.handler?()
self?.worker = nil
}
self.worker = worker
queue.asyncAfter(deadline: .now() + timeInterval, execute: worker)
}
}
}
private let timeInterval: TimeInterval
private var worker: DispatchWorkItem?
private let queue: DispatchQueue
init(timeInterval: TimeInterval, queue: DispatchQueue = .main) {
self.timeInterval = timeInterval
self.queue = queue
}
func cancel() {
worker?.cancel()
worker = nil
}
}
Solution 2:[2]
Example Solution with combine framework
import Combine
class ViewModelSearch: ObservableObject {
@Published var searchText = ""
}
class YourVC:UIViewController {
var viewModel = ViewModelSearch()
private var subscriptions = Set<AnyCancellable>()
override func viewDidLoad() {
viewModel.$searchText
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.sink { value in
//CALL YOUR API
}.store(in: &subscriptions)
}
func searchTextUpdated(newText:String) {
viewModel.searchText = newText
}
}
Note: This is not tested
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Bas9990 |
| Solution 2 | Prashant Tukadiya |

