Search code examples
swifttableviewuisearchbar

how to add a searchbar in a tableview with a parsed JSON file


I have successfully parsed a JSON file with the following data model into my project and my tableview.

import Foundation

struct ActionResult: Codable {
    let data: [Datum]
}
struct Datum: Codable {
    let goalTitle, goalDescription, goalImage: String
    let action: [Action]
}
struct Action: Codable {
    let actionID: Int
    let actionTit: String
}

Now I am trying to create a searchbar to search on the "actionTitle". My tableview has section headers and rows.

Relevant code:

var filteredData: [Action]?
    
    let searchController = UISearchController()
  
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Search"
        searchController.searchBar.delegate = self
        filteredData = ????
        navigationItem.searchController = searchController
        parseJSON()
        
        
                func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            filteredData = []
            if searchText == ""{
                filteredData = ????
            }
            else {
                for actions in ???? {
                if actions.lowercased().contains(searchText.lowercased()) {
                    filteredData.append(actions)
        }

                self.tableView.reloadData()
 }

I do not know what code to use where I have ????.

Thanks.


Solution

  • You want to keep an instance of all of the available data (allActions), and always show your filter data (filteredData) in the tableView. So when there is nothing to filter, filteredData is equal to allActions (unless you intend to hide all data when the search is empty).

    When searchBar(_:,textDidChange:) is called, you can use filter(_:) to evaluate if the item should be included. Apple's description on the filter closure:

    A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be included in the returned array.

    I don't know if there is a specific reason for declaring filteredData: [Action]?, is it because the data is not populated until parseJSON() is called? If so--I suggest initializing an empty arrays and populating them when the data is available.

    Also, does parseData() produce an instance of Datum? I believe this piece of your code is not included, so I am adding datum: Datum?. If I am wrong, please provide more info what parseJSON() populates and I will update my answer.

    var result: ActionResult? {
        didSet {
            guard let result = result else { return }
            allSectionDataActionMap = Dictionary(uniqueKeysWithValues: result.data.enumerated().map { ($0.0, ($0.1, $0.1.actions)) })
            updateFilteredData()
        }
    }
    
    
    var allSectionDataActionMap = [Int: (datum: Datum, actions: [Action])]()
    
    // Maps the section index to the Datum & filtered [Action]
    var filteredSectionDataActions = [Int: (datum: Datum, actions: [Action])]()
    
    let searchController = UISearchController()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        title = "Search"
        searchController.searchBar.delegate = self
        navigationItem.searchController = searchController
    
        // ...
        parseJSON()
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return filteredSectionDataActions.count
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return filteredSectionDataActions[section]?.actions.count ?? 0
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell(style: .default, reuseIdentifier: "cell")
        if let action = filteredSectionDataActions[indexPath.section]?.actions[indexPath.row] {
            // setup cell for action
            cell.textLabel?.text = action.actionTitle
        }
        return cell
    }
    
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        updateFilteredData(for: searchText.lowercased())
        tableView.reloadData()
    }
    
    func updateFilteredData(for searchText: String = String()) {
    
        if searchText.isEmpty {
    
            filteredSectionDataActions = allSectionDataActionMap
    
        } else {
    
            for (index, (datum, actions)) in allSectionDataActionMap {
    
                let filteredActions = actions.filter { $0.actionTitle.lowercased().contains(searchText) }
    
                if filteredActions.isEmpty {
    
                    filteredSectionDataActions[index] = (datum, actions)
    
                } else {
    
                    filteredSectionDataActions[index] = (datum, filteredActions)
    
                }
            }
    
        }
    
    }