Updating Swift InstantSearchClient Objects to AlgoliaSearchClient Objects

Hi,

I’ve been successfully using the InstantSearchClient library in my iOS project for years now. Yesterday I finally updated my pods and now AlgoilaSearchClient is the updated library. The issue is there have been several Algolia changes such as Query initializers, data types, callbacks, etc. and several errors have appeared. I placed ERROR next to them in the newer code.

I was wondering if anyone can help update my older InstantSearchClient code to the newer AlgoilaSearchClient code. I actually got the code from the functions from an Algolia tutorial sample post years ago. It worked great!

Older Code:

import InstantSearchClient

var datasource = [DataModel]()

var algoliaIndex: InstantSearchClient.Index!
let query = Query()
var searchId = 0
let displayedSearchId = -1
var loadedPage: UInt = 0
var nbPages: UInt = 0

func setAlgoliaAPI() {
        
    let apiClient = Client(appID: "...", apiKey: "...")
    algoliaIndex = apiClient.index(withName: "home")
    query.hitsPerPage = 15 
    query.attributesToRetrieve = ["...", "..."]
    query.restrictSearchableAttributes = ["...", "..."]
}

func updateSearchResults(for searchController: UISearchController) {
        
    query.query = searchController.searchBar.text
    let curSearchId = searchId
        
    algoliaIndex.search(query: query) { [weak self] (data, error) in
        guard let weakSelf = self else { return }
            
        if let error = error {
            print(error.localizedDescription)
            return
        }
            
        if curSearchId <= weakSelf.displayedSearchId { return }

        // Decode JSON
        guard let hits = data!["hits"] as? [[String: Any]] else { return }
        guard let nbPages = data!["nbPages"] as? UInt else { return }
        self?.nbPages = nbPages

        var tmp = [DataModel]()
        for hit in hits {
                
            if let entry = hit.keys.first(where: { $0.lowercased().contains("account") }) {
                    
                if let dict = hit[entry] as? [String: AnyObject] {
                    tmp.append(DataModel(json: dict))
                }
            }
            print(hit)
        }
            
        guard let safeText = searchController.searchBar.text else { return }
            
        if safeText.trimmingCharacters(in: safeText.whitespace) == "" {

            self?.datasource.removeAll()

        } else {
            //Reload view with the new data
              
            self?.datasource = tmp
        }
        self?.tableView.reloadData()
    })
        
    searchId += 1
}
    
func loadMore() {
        
    if loadedPage + 1 >= nbPages {
        return // All pages already loaded
    }
    let nextQuery = Query(copy: query)
    nextQuery.page = loadedPage + 1
        
    algoliaIndex.search(query: nextQuery) { [weak self] (data , error) in
            
        if let error = error {
            print(error.localizedDescription)
            return
        }
            
        if nextQuery.query != self?.query.query {
            return // Query has changed
        }
            
        guard let safeNextQueryPage = nextQuery.page else { return }
        self?.loadedPage = safeNextQueryPage as UInt
            
        if let data = data {
            let json = JSON(data)
            let hits: [JSON] = json["hits"].arrayValue
            var tmp = [DataModel]()
            print("\nhits_count...\(hits.count)\n")

            for record in hits {
                    
                guard let hit = record.dictionaryObject else { continue }
                    
                if let entry = hit.keys.first(where: { $0.lowercased().contains("account") }) {
                        
                    if let dict = hit[entry] as? [String: AnyObject] {
                        tmp.append(DataModel(json: dict))
                    }
                }
            }
            // Display the new loaded page
            
            self?.datasource.append(contentsOf: tmp)
            self?.tableView.reloadData()
        }
    })
}

Newer Code:

import AlgoliaSearchClient

var algoliaIndex: AlgoliaSearchClient.Index!
var query = Query()
var searchId = 0
let displayedSearchId = -1
var loadedPage: Int = 0
var nbPages: UInt = 0

func setAlgoliaAPI() {
        
    let apiClient = SearchClient(appID: "...", apiKey: "...")

    algoliaIndex = apiClient.index(withName: "home")
    // ...
}

func updateSearchResults(for searchController: UISearchController) {
        
    query.query = searchController.searchBar.text
    let curSearchId = searchId
        
    algoliaIndex.search(query: query) { [weak self](result) in    
        guard let weakSelf = self else { return }
            
        switch result {
                
        case .failure(let error):
            print(error.localizedDescription)
                
        case .success(let searchResponse):
                
            if curSearchId <= weakSelf.displayedSearchId {    
                return // Newest query already displayed or error
            }

            // Decode JSON
            guard let hits = searchResponse["hits"] as? [[String: Any]] else { return } // ERROR - Value of type 'SearchResponse' has no subscripts
            guard let nbPages = searchResponse["nbPages"] as? UInt else { return } // ERROR - Value of type 'SearchResponse' has no subscripts
            self?.nbPages = nbPages

            var tmp = [DataModel]()
            for hit in hits {
                    
                if let entry = hit.keys.first(where: { $0.lowercased().contains("account") }) {
                        
                    if let dict = hit[entry] as? [String: AnyObject] {
                         tmp.append(DataModel(json: dict))
                    }
                }
            }
                
            guard let safeText = searchController.searchBar.text else { return }
                
            if safeText.trimmingCharacters(in: safeSelf.whitespace) == "" {

                    self?.datasource.removeAll()

            } else {
                //Reload view with the new data
                
                self?.datasource = tmp
            }
            self?.tableView.reloadData()
        }
    }
        
    searchId += 1
}

func loadMore() {
        
    if loadedPage + 1 >= nbPages {
        return // All pages already loaded
    }
    let nextQuery = Query(copy: query) // ERROR - No exact matches in call to initializer 
    nextQuery.page = loadedPage + 1
        
    algoliaIndex.search(query: nextQuery) { [weak self](result) in
            
        switch result {
            
        case .failure(let error):
            print(error.localizedDescription)
                
        case .success(let data):
                
            if nextQuery.query != self?.query.query {
                return // Query has changed
            }
                
            guard let safeNextQueryPage = nextQuery.page else { return }
            self?.loadedPage = safeNextQueryPage as Int
                
            if let data = data { // ERROR - Initializer for conditional binding must have Optional type, not 'SearchResponse'
                let json = JSON(data) // ERROR - Call can throw, but it is not marked with 'try' and the error is not handled
                let hits: [JSON] = json["hits"].arrayValue // ERROR - JSON' is ambiguous for type lookup in this context
                var tmp = [DataModel]()
                print("\nhits_count...\(hits.count)\n")

                for record in hits {
                        
                    guard let hit = record.dictionaryObject else { continue }
                        
                    if let entry = hit.keys.first(where: { $0.lowercased().contains("account") }) {
                            
                        if let dict = hit[entry] as? [String: AnyObject] {
                            tmp.append(DataModel(json: dict))
                        }
                    }
                }
                // Display the new loaded page
                
                self?.datasource.append(contentsOf: tmp)
                self?.tableView.reloadData()
            }
        }
    }
}

DataModel:

class DataModel {
    // ...
    init(json: [String: Any]) {
       // ...
   }
}

Hi @flowtinc

I’ve passed this issue on to the iOS/Swift engineers internally to see if they can provide any insights into this upgrade process.

@chuck.meyer Thanks, I really appreciate it. There’s a big problem with iOS 14 where compile errors don’t appear as detailed here. I was having a ton of problems trying to debug it without the compile errors showing.

Again I really appreciate the help! :slightly_smiling_face:

1 Like

Sure thing – I’m sorry I’m not more help myself, but I don’t have insights into the changes made to the libraries. Whatever comes of this, I’ll try to get it into an upgrade guide.

1 Like

Still waiting to hear back internally. Hang tight!

1 Like

Hi @flowtinc

After chatting with the frontend team, this is what they recommend for updating your code:

func updateSearchResults(for searchController: UISearchController) {
    
    query.query = searchController.searchBar.text
    let curSearchId = searchId
    
    algoliaIndex.search(query: query) { [weak self] (result) in
      guard let weakSelf = self else { return }
      
      switch result {
        
      case .failure(let error):
        print(error.localizedDescription)
        
      case .success(let searchResponse):
        
        if curSearchId <= weakSelf.displayedSearchId {
          return // Newest query already displayed or error
        }
        
        // Decode JSON
        let hits = searchResponse.hits.map(\.object).compactMap { Dictionary($0) }
        guard let nbPages = searchResponse.nbPages else { return } // nbPages is available as a field of `SearchResponse`
        self?.nbPages = UInt(nbPages)
        
        var tmp = [DataModel]()
        for hit in hits {
          
          if let entry = hit.keys.first(where: { $0.lowercased().contains("account") }) {
            
            if let dict = hit[entry] as? [String: AnyObject] {
              tmp.append(DataModel(json: dict))
            }
          }
        }
        
        guard let safeText = searchController.searchBar.text else { return }
        
        if safeText.trimmingCharacters(in: .whitespaces) == "" {
          
          self?.datasource.removeAll()
          
        } else {
          //Reload view with the new data
          
          self?.datasource = tmp
        }
        self?.tableView.reloadData()
      }
    }
    
    searchId += 1
}
  
func loadMore() {
    
    if loadedPage + 1 >= nbPages {
      return // All pages already loaded
    }
    var nextQuery = query // Query is now a value type (struct), not reference type (class) so to copy it you only need to instantiate another variable
    nextQuery.page = loadedPage + 1
    algoliaIndex.search(query: nextQuery) { [weak self] result in
      switch result {

      case .failure(let error):
        print(error.localizedDescription)

      case .success(let response):

        if nextQuery.query != self?.query.query {
          return // Query has changed
        }

        guard let safeNextQueryPage = nextQuery.page else { return }
        self?.loadedPage = safeNextQueryPage as Int


        let hits = response.hits.map(\.object).compactMap { Dictionary($0) }
        var tmp = [DataModel]()
        print("\nhits_count...\(hits.count)\n")

        for hit in hits {
          if let entry = hit.keys.first(where: { $0.lowercased().contains("account") }) {

            if let dict = hit[entry] as? [String: AnyObject] {
              tmp.append(DataModel(json: dict))
            }
          }
        }
        // Display the new loaded page

        self?.datasource.append(contentsOf: tmp)
        self?.tableView.reloadData()
      }
}

I’d also recommend to make the DataModel conform to Decodable protocol, so the hits can be extracted as follows:

case .success(let response):
  do {
    let hits: [DataModel] = try response.extractHits()
  } catch let error {
    print("hits parsing error: \(error)")
  }