1

In my case, I am trying to implement search functionality for my tableview data. Here, I am loading JSON data into my Tableview section and rows. In custom cell I am showing name, I would like to create search by name. I tried below code but nothing display in my result. How to do in a proper way of search functionality.

var sections = [Section]()
var filteredNames = [Section]()
var searchController : UISearchController!

Tableview Delegates

// MARK: UITableview Delegates

    func numberOfSections(in tableView: UITableView) -> Int {
        return sections.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if isFiltering {
            return filteredNames[section].result.count
        } else {
            return sections[section].result.count
        }
    }

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return sections[section].title
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell:MyCustomCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! MyCustomCell
        let item = sections[indexPath.section].result[indexPath.row]
        let filteritem = filteredNames[indexPath.section].result[indexPath.row]
        if isFiltering {
            cell.nameLabel.text = filteritem.name
        } else {
            cell.nameLabel.text = item.name
        }
        return cell
    }

SearchBarAction and Delegates

 // MARK: UISearchBar Delegates

@IBAction func searchAction(_ sender: Any) {

    searchController = UISearchController(searchResultsController: nil)
    searchController.hidesNavigationBarDuringPresentation = false
    searchController.searchBar.keyboardType = UIKeyboardType.asciiCapable
    searchController.searchBar.barTintColor = #colorLiteral(red: 0.317096545, green: 0.5791940689, blue: 0.3803742655, alpha: 1)
    searchController.searchBar.backgroundColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
    searchController.dimsBackgroundDuringPresentation = false

    // Make this class the delegate and present the search
    self.searchController.searchBar.delegate = self
    searchController.searchResultsUpdater = self
    present(searchController, animated: true, completion: nil)
}

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    guard let searchText = searchBar.text else {
        isFiltering = false
        return
    }
    filteredNames = sections[indexPath.section].result[indexPath.row].filter({
        return $0.lowercased().contains(searchText.lowercased())
    })
    isFiltering = filteredNames.count > 0
    self.tableView.reloadData()
}

Error: I am getting error - Use of unresolved identifier 'indexPath'; did you mean 'IndexPath'?

Alexa
  • 53
  • 1
  • 2
  • 7
  • I have a full example of using a UISearchController in another answer [here](https://stackoverflow.com/questions/50409482/searching-dynamic-data-issue-from-table-view-issue-in-swift/50409887#50409887) that might help – Scriptable Aug 15 '19 at 10:29
  • [OT] How many different accounts are you using here on SO? – vadian Aug 15 '19 at 10:30
  • The linked answer in Scriptable's comment will teach you a lot. – vadian Aug 15 '19 at 10:39
  • Thanks @vadian :) alexa read through that answer and give it a go, if your still having problems update your question with your updated code and explain the issue. – Scriptable Aug 15 '19 at 10:46
  • @ Scriptable Thanks for sharing your link. Its very Informative btw I updated my question I got some error on search delegates. Please check it! – Alexa Aug 15 '19 at 10:56
  • @Scriptable I updated my question because I got some error – Alexa Aug 15 '19 at 11:07
  • @vadian I don't know how to assign names value in **search textDidChange** method, so I am getting error. Could you please check once. – Alexa Aug 15 '19 at 11:13
  • In your error `IndexPath` is the TYPE, not the actual object. you need the lower camelCased version, indexPath. – Scriptable Aug 15 '19 at 12:32
  • @Scriptable i tried that even I am getting error - Use of unresolved identifier 'indexPath'; did you mean 'IndexPath'? – Alexa Aug 15 '19 at 12:56
  • @vadian I tried my best didn't get it? help me. – Alexa Aug 15 '19 at 13:00
  • @Scriptable can you please help me on this? – Alexa Aug 15 '19 at 13:11
  • I didnt even read your code properly just the error. you don't have any indexPath in that context. your searching, your not in a section or selected a row? what are your sections for? what is inside them? do you still want to show sections in search results? there isn't enough information here – Scriptable Aug 15 '19 at 13:17
  • @Scriptable this is my JSON and Codable and Here I want to implement search option. search by name i want to do. https://stackoverflow.com/questions/57502487/how-to-load-json-array-data-into-uitableview-section-and-row-using-swift Its very complex for me because I am a new student for this. – Alexa Aug 15 '19 at 13:20
  • @Scriptable is there any idea buddy. Its bit urgent – Alexa Aug 15 '19 at 13:33
  • @vadian help me on this. its very complex for me. didn't get idea buddy. – Alexa Aug 15 '19 at 14:22

2 Answers2

0

I'm taking a bit of a calculated guess here, you may need to make some amends yourself.

In my example (link provided in comments above) I am just filtering an array of names. It is more difficult if you have sections as you want to filter the items in the sections and then remove any empty sections.

So your filter method should probably look something like this:

guard let searchText = searchBar.text else {
    isFiltering = false
    return
}

self.filteredNames = sections.map({ 
    let filteredResult = $0.result.filter { $0.name.contains(searchText) }
    $0.result = filteredResult
    return $0
)).filter { !$0.result.isEmpty } 

So here you are looping through each section, then filtering the result array for matches in the search string, then removing any sections that have 0 results in.

Note you might be able to shorten that

self.filteredNames = sections.map({ 
    $0.result = $0.result.filter { $0.name.contains(searchText) }
)).filter { !$0.result.isEmpty } 

EDIT:

import UIKit
import PlaygroundSupport
import UIKit
import PlaygroundSupport

struct Section {
    let title: String
    var result: [String]
}

class ViewController: UITableViewController {

    let searchController = UISearchController(searchResultsController: nil)

    let names = [
        Section(title: "J", result: [
            "John",
            "Jason"
        ]),
        Section(title: "M", result: [
            "Martin",
            "Michael",
            "Mattew"
        ]),
        Section(title: "T", result: [
            "Terry",
            "Thomas"
        ]),
        Section(title: "S", result: ["Steven"])
    ]
    var filteredNames = [Section]()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.title = "Search Example"

        searchController.searchResultsUpdater = self
        searchController.obscuresBackgroundDuringPresentation = false
        searchController.searchBar.placeholder = "Search"
        navigationItem.searchController = searchController
        definesPresentationContext = true
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        if isFiltering() {
            return filteredNames.count
        } else {
            return names.count
        }
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if isFiltering() {
            return filteredNames[section].result.count
        } else {
            return names[section].result.count
        }
    }

    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if isFiltering() {
            return filteredNames[section].title
        } else {
            return names[section].title
        }
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell() // don't do this, i am for example.

        var name: String
        if isFiltering() {
            name = filteredNames[indexPath.section].result[indexPath.row]
        } else {
            name = names[indexPath.section].result[indexPath.row]
        }

        cell.textLabel?.text = name
        return cell
    }

    func searchBarIsEmpty() -> Bool {
        // Returns true if the text is empty or nil
        return searchController.searchBar.text?.isEmpty ?? true
    }

    func isFiltering() -> Bool {
        return searchController.isActive && !searchBarIsEmpty()
    }

    func filterContentForSearchText(_ searchText: String, scope: String = "All") {

        filteredNames = names.map({
            let result = $0.result.filter {
                $0.localizedLowercase.contains(searchText.lowercased())
            }
            return Section(title: $0.title, result: result)
        }).filter { !$0.result.isEmpty }

        tableView.reloadData()
    }
}

extension ViewController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        filterContentForSearchText(searchController.searchBar.text!)
    }
}

let vc = ViewController()
let nav = UINavigationController()
nav.viewControllers = [vc]

PlaygroundPage.current.liveView = nav
Scriptable
  • 19,402
  • 5
  • 56
  • 72
  • thank you so much I tried that but still I am getting error like Cannot assign to property: '$0' is immutable at the line $0.result = filteredResult. Btw if its difficult is there anything alternative way? – Alexa Aug 15 '19 at 14:04
  • thats because the struct has let result: [Result], try changing to var. There isn't many other alternatives, its difficult because it has sections. I would normally write an example for you but I am also busy with work – Scriptable Aug 15 '19 at 14:05
  • again error Unable to infer closure type in the current context at **sections.map({ ** – Alexa Aug 15 '19 at 14:07
  • @ Scriptable Thank you I will check and try to understand your logic. Thanks a lot – Alexa Aug 15 '19 at 16:58
0

First of all you are filtering sections so name the array

var filteredSections = [Section]()

filteredNames is misleading and implies that the array is [String]

A possible solution is to create new sections with the filtered content

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    if !searchText.isEmpty {
        filteredSections = sections.compactMap { section -> Section? in
            let filteredContent = section.result.filter {$0.name.range(of: searchText, options: .caseInsensitive) != nil }
            return filteredContent.isEmpty ? nil : Section(title: section.title, result: filteredContent)
        }
        isFiltering = true  
    } else {  
        filteredSections.removeAll()
        isFiltering = false
    }
    self.tableView.reloadData()
}

And the table view data source methods must be changed to

func numberOfSections(in tableView: UITableView) -> Int {
    return isFiltering ? filteredSections.count : sections.count
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let currentSection = isFiltering ? filteredSections[section] : sections[section]
    return currentSection.result.count
}

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    return isFiltering ? filteredSections[section].title : sections[section].title
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) as! MyCustomCell
    let section = isFiltering ? filteredSections[indexPath.section] : sections[indexPath.section]
    let item = section.result[indexPath.row]
    cell.nameLabel.text = item.name
    return cell
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • Thank you so much. Its working great. I am trying to understand your logic. I want to become a super power like you. Thanks apple master. great help – Alexa Aug 15 '19 at 16:57
  • The logic is straightforward. In a section based data source you have to filter each section separately. – vadian Aug 15 '19 at 16:59
  • Yeah I understood but need to do more practice. thanks vadian for such a great help. – Alexa Aug 15 '19 at 17:20