Home > Software engineering >  TableView SearchBar doesn't work: Index out of range
TableView SearchBar doesn't work: Index out of range

Time:02-18

I know there are some similar questions, but it's doesn't work to me. I'm new to this, so I followed some tutorial trying make a search bar in my table view screen. I got a problem: there are index out of range and I cannot realise why. Here is my code:

import UIKit

final class AllGroupsViewController: UITableViewController {

    var groups = [
        "cats",
        "birds",
        "dogs",
        "books",
        "music",
        "movies",
        "art",
        "science",
        "tech",
        "beauty",
    ]
    
    @IBOutlet var searchBar: UISearchBar!
    
    var isSearching = false
    var filteredData = [String]()
    
    var userGroups: [String] = []
    var groupSectionTitles = [String]()
    var groupsDictionary = [String: [String]]()

    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UINib(
            nibName: "GroupCell",
            bundle: nil),
                           forCellReuseIdentifier: "groupCell")
        
        for group in groups {
            let groupKey = String(group.prefix(1))
            if var groupValues = groupsDictionary[groupKey] {
                groupValues.append(group)
                groupsDictionary[groupKey] = groupValues
            } else {
                groupsDictionary[groupKey] = [group]
            }
        }
        
        
        groupSectionTitles = [String](groupsDictionary.keys)
        groupSectionTitles = groupSectionTitles.sorted(by: { $0 < $1 })
        
    }
    
    // MARK: - Table view data source
    override func numberOfSections(in tableView: UITableView) -> Int {
        return groupSectionTitles.count
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        if isSearching {
            return filteredData.count
        } else {
            return groups.count
        }
        
        let groupKey = groupSectionTitles[section]
        if let groupValues = groupsDictionary[groupKey] {
            return groupValues.count
        }
        
        return 0
    }
    
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return groupSectionTitles[section]
    }
    
    override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
        return groupSectionTitles
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard
            let cell = tableView.dequeueReusableCell(withIdentifier: "groupCell", for: indexPath) as? GroupCell
        else { return UITableViewCell() }
        
        var currentGroup = groups[indexPath.row]
        
        let groupKey = groupSectionTitles[indexPath.section]
        if let groupValues = groupsDictionary[groupKey] {
            currentGroup = groupValues[indexPath.row]
        }
        
        if isSearching {
            currentGroup = filteredData[indexPath.row]
        } else {
            currentGroup = groups[indexPath.row]
        }
        return cell
        
        cell.configure(
            photo: UIImage(systemName: "person.3.fill") ?? UIImage(),
            name: currentGroup)

        return cell
    }

    override func tableView(_ tableView: UITableView,
                            didSelectRowAt indexPath: IndexPath) {
        defer {
            tableView.deselectRow(at: indexPath, animated: true)
        }
        
        let groupKey = groupSectionTitles[indexPath.section]
        var currentGroup = ""
        if let groupValues = groupsDictionary[groupKey] {
            currentGroup = groupValues[indexPath.row] // here is index out of range
        }
        
        if userGroups.firstIndex(of: currentGroup) == nil {
            userGroups.append(currentGroup)
        }
        
        self.performSegue(withIdentifier: "addGroup", sender: nil)
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if segue.identifier == "addGroup",
               let myGroupsViewController = segue.destination as? MyGroupsViewController {
                myGroupsViewController.groups = userGroups
        }
    }
}

extension AllGroupsViewController {
     
     func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
         filteredData = groups.filter({$0.lowercased().prefix(searchText.count) == searchText.lowercased()})
         isSearching = true
         tableView.reloadData()
     }
     
     func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
         isSearching = false
         searchBar.text = ""
         tableView.reloadData()
     }
 }

I'll be so glad if somebody will help me. And, please, can you recommend me some good tutorial to achieve my aim?

CodePudding user response:

Actually issue is more to do with logic of accessing groups than crash because of adding search bar.

For example:

override func tableView(_ tableView: UITableView,
                        numberOfRowsInSection section: Int) -> Int {
    
    if isSearching {
        return filteredData.count
    } else {
        return groups.count
    }
    
    let groupKey = groupSectionTitles[section]
    if let groupValues = groupsDictionary[groupKey] {
        return groupValues.count
    }
    
    return 0
}

Here because you use if-else you will either return filteredData.count when searching or groups.count - you will not go beyond this code

So when you are not searching, you will return groups.count which is 10 and that is wrong because you want to return the count for which section we are in, for example a should return 1, b should return 3.

The logic after if-else block should replace logic in else section

Now looking at next two functions:

override func tableView(_ tableView: UITableView,
                        cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
    guard let cell
            = tableView.dequeueReusableCell(withIdentifier: "groupCell",
                                            for: indexPath) as? GroupCell
    else { return UITableViewCell() }
    
    var currentGroup = groups[indexPath.row]
    
    let groupKey = groupSectionTitles[indexPath.section]
    if let groupValues = groupsDictionary[groupKey] {
        currentGroup = groupValues[indexPath.row]
    }
    
    if isSearching {
        currentGroup = filteredData[indexPath.row]
    } else {
        currentGroup = groups[indexPath.row]
    }
    return cell
    
    cell.configure(
        photo: UIImage(systemName: "person.3.fill") ?? UIImage(),
        name: currentGroup)
    
    return cell
}

override func tableView(_ tableView: UITableView,
                        didSelectRowAt indexPath: IndexPath) {
    defer {
        tableView.deselectRow(at: indexPath, animated: true)
    }
    
    let groupKey = groupSectionTitles[indexPath.section]
    var currentGroup = ""
    if let groupValues = groupsDictionary[groupKey] {
        currentGroup = groupValues[indexPath.row] // here is index out of range
    }
    
    if userGroups.firstIndex(of: currentGroup) == nil {
        userGroups.append(currentGroup)
    }
    
    self.performSegue(withIdentifier: "addGroup", sender: nil)
}

First because numberOfRowsInSection returns wrong values, we will have issues in these functions.

Then I think the logic of accessing the right data source of groups, group sections is not done right.

For example: currentGroup = groups[indexPath.row] in cellForRowAt indexPath is not right because this gets group from group array of 10 when we only want to group for the specific section.

And also I see return cell twice so code after the first will not be run.

So what I did is just refactored these functions to make it more clear and added some comments.

First, we need to keep in mind the different data sources:

// All the groups
var groups = [
    "cats",
    "birds",
    "dogs",
    "books",
    "music",
    "movies",
    "art",
    "science",
    "tech",
    "beauty",
]

// Checks if search is active or not
var isSearching = false

// This will hold the filtered array when searching
var filteredData = [String]()

// This will hold groups of the user
var userGroups: [String] = []

// This will hold section prefixes [a, b, c, etc]
var groupSectionTitles = [String]()

// This will hold mapping of prefixes to groups
// [a: [art], b: [beauty, books], etc]
var groupsDictionary = [String: [String]]()

There is nothing different above from your code, only comments, however we have to keep a visual image of this because this is important to how we need to access the data

Next, I created this function to get the correct groups in a section since we need to do this many times

private func getGroups(in section: Int) -> [String]
{
    // The current section should be got from groupSectionTitles
    let groupKey = groupSectionTitles[section]
    
    var groupsInSection: [String] = []
    
    // Get groups for current section
    if let groupValues = groupsDictionary[groupKey] {
        groupsInSection = groupValues
    }
    
    // Change groups in section if searching
    if isSearching {
        groupsInSection = filteredData
    }
    
    return groupsInSection
}

Then I refactored these functions slightly:

override func tableView(_ tableView: UITableView,
                        numberOfRowsInSection section: Int) -> Int
{
    if isSearching {
        return filteredData.count
    } else {
        let groupsInSection = getGroups(in: section)
        return groupsInSection.count
    }
}

override func tableView(_ tableView: UITableView,
                        cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
    guard let cell
            = tableView.dequeueReusableCell(withIdentifier: "groupCell",
                                            for: indexPath) as? GroupCell
    else { return UITableViewCell() }
    
    let groupsInSection = getGroups(in: indexPath.section)
    
    cell.configure(
        photo: UIImage(systemName: "person.3.fill") ?? UIImage(),
        name: groupsInSection[indexPath.row])
    
    return cell
}

override func tableView(_ tableView: UITableView,
                        didSelectRowAt indexPath: IndexPath)
{
    let groupsInSection = getGroups(in: indexPath.section)
    let currentGroup = groupsInSection[indexPath.row]
    
    if userGroups.firstIndex(of: currentGroup) == nil {
        userGroups.append(currentGroup)
    }
    
    defer {
        tableView.deselectRow(at: indexPath, animated: true)
    }
    
    self.performSegue(withIdentifier: "addGroup", sender: nil)
}

I think now your crash will be resolved and things work as expected.

However, since you did not connect and implement search delegate yet, maybe there can be some issues when isSearching becomes true but I think that can be for another question on filtering with search delegate.

  • Related