[TUTORIAL] Using a Textfield in Swift 3 for Search Instead of a Search Controller

Hey, guys! I just figured out a way to use a text field instead of a search controller to achieve instant search results in a table view in Swift 3. The main advantage with this is that you can customize a text field much more easily than the search bar provided in Xcode. My example also goes a step further and shows you how you can use a text field in a parent view controller and have the results instantly update in a child VC. I have a picture blow that demonstrates.

Parent View Code:

let handleTextChangeNotification = "handleTextChangeNotification"
/** MARK: Make sure this variable is outside of the parent view class!**/

class ParentVC: UIViewController, UITextFieldDelegate {

@IBOutlet weak var searchField: UITextField!
  ....

  override func viewDidLoad() {
    searchField.delegate = self
    searchField.addTarget(self, action: #selector(UITextFieldDelegate.textFieldShouldEndEditing(_:)), for: UIControlEvents.editingChanged)
}


func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    searchViewController = storyboard.instantiateViewController(withIdentifier: "searchView")
    let vc = searchViewController!
    addChildViewController(vc)
    vc.view.frame = mainView.bounds
    mainView.addSubview(vc.view)
    vc.didMove(toParentViewController: self)
    
/** MARK: In order for the string to be set, the Notification has to be called AFTER the search view is presented **/
    
  NotificationCenter.default.post(name: NSNotification.Name(rawValue: handleTextChangeNotification), object: nil, userInfo: ["text":searchField.text!])
        
    return true
}

Child View (Search Results):

import UIKit
import Foundation
import AlgoliaSearch
import SwiftyJSON
import AFNetworking

class SearchView: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    @IBOutlet weak var tableView: UITableView!
    var businessSearch = [Business]()
    var businessIndex: AlgoliaSearch.Index!
    let query = Query()
    var searchId = 0
    var displayedSearchId = -1
    var loadedPage: UInt = 0
    var nbPages: UInt = 0
    var searchText = String() /** MARK: this is the variable the textfield.text is stored into **/
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(SearchView.handleTextChange(_:)),
                                               name: NSNotification.Name(rawValue: handleTextChangeNotification), object: nil)
        
        self.tableView.dataSource = self
        self.tableView.delegate = self
        let apiClient = Client(appID: "APPID", apiKey: "APIKEY")
        self.businessIndex = apiClient.index(withName: "businessSearch")
        query.hitsPerPage = 15
        query.attributesToRetrieve = ["businessName", "Address", "Score", "businessType", "hours", "city"]
    }
    /**MARK: Notification Function **/
    func handleTextChange(_ myNot: Notification) {
        if let use = myNot.userInfo {
            if let text = use["text"] {
                searchText = text as! String
                // print(searchText)
            }
        }
        updateSearchResults()
        tableView.reloadData()
    }
    
    func updateSearchResults() {
        query.query = searchText
        let curSearchId = searchId
        
        self.businessIndex.search(self.query, completionHandler: {
            (data, error) in
            
            if curSearchId <= self.displayedSearchId || error != nil {
                print("problem #1")
                print(error?.localizedDescription ?? "Error 1")
                return // Newest query already displayed or error
            }
            
            // Decode JSON
            guard let hits = data!["hits"] as? [[String: AnyObject]] else { return }
            guard let nbPages = data!["nbPages"] as? UInt else { return }
            self.nbPages = nbPages
            
            var tmp = [Business]()
            for hit in hits {
                tmp.append(Business(json: hit))
                
            }
            
            //Reload view with the new data
            self.businessSearch = tmp
            print(tmp)
            self.tableView.reloadData()
        })
        self.searchId += 1
        
    }
    
    func loadMore() {
        if loadedPage + 1 >= nbPages {
            return // All pages already loaded
        }
        let nextQuery = Query(copy: query)
        nextQuery.page = loadedPage + 1
        businessIndex.search(nextQuery, completionHandler: {
            (data, error) in
            
            if nextQuery.query != self.query.query || error != nil {
                print("problem #2")
                return // Query has changed
            }
            
            self.loadedPage = nextQuery.page! as UInt
            let json = JSON(data!)
            let hits: [JSON] = json["hits"].arrayValue
            var tmp = [Business]()
            print("...\(hits.count)")
            for record in hits {
                tmp.append(Business(json: record.dictionaryObject! as [String : AnyObject]))
            }
            // Display the new loaded page
            self.businessSearch.append(contentsOf: tmp)
            self.tableView.reloadData()
        })
    }
    
    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return businessSearch.count
    }
    
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for:indexPath as IndexPath) as! SearchCell
        let cellData = self.businessSearch[indexPath.row]
        
        cell.nameLabel.text = cellData.businessName
        cell.addressLabel.text = "1234 Main St."
        cell.hoursLabel.text = "9:00 AM - 10:00 PM"
        cell.reviewCountLabel.text = "45 Reviews"
        cell.backgroundImage.image = #imageLiteral(resourceName: "samplebg")
        
        
        
        if (indexPath.row + 5) >= businessSearch.count {
            loadMore()
        }
        
        return cell
    }
    
    //:::: Dismiss Keyboard
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
        
    }
    
}

If anything needs explaing just let me know!

11 Likes

Very good tutorial! Thank you for sharing that, it will definitely be helpful to the community using the Swift API client :slight_smile:

Also note that we’re working on new libraries that will facilitate the integration of Algolia in your iOS app, as well as offer you better ways to customise your UI. You can start by checking out InstantSearch Core.

Great job again :smile:

2 Likes

This is fantastic! Thank you!

Would it be ok for us to post this to @algolia on Twitter? If so, do you have a Twitter handle we can attribute credit to?

Thanks!
-Jason

1 Like

Hey, Jason! That would be awesome. My twitter is @ethanjfox

1 Like