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