iosswifturlsession

If you type too fast in search-bar API call errors out


Where the error is happening

I have two questions here:

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

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

    }