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.