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?
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.
For tutorials, you can have a look at:
UISearchResultsController tutorial - watch from minute 8 onwards
UISearchBar tutorial - watch from minute 10 onwards
StackOverflow discussion
Related
I'm new to this, I've just made an indexed table view "groups search" with all groups in my app and got a problem: when I'm trying to add some group to the "my groups" view, there are should appear a selected group, but actually I got the first one from all groups array instead. Also I can't add several items started with a similar letter in the "my groups". It might be stupid, but I have no idea how to fix that. Thank you!
import UIKit
final class AllGroupsViewController: UITableViewController {
var groups = [
"cats",
"birds",
"dogs",
"books",
"music",
"movies",
"art",
"science",
"tech",
"beauty",
]
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 {
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]
}
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) }
performSegue(
withIdentifier: "addGroup",
sender: nil)
}
}
import UIKit
final class MyGroupsViewController: UITableViewController {
var groups = [String]() {
didSet {
//
}
}
#IBAction func addGroup(segue: UIStoryboardSegue) {
guard
segue.identifier == "addGroup",
let allGroupsController = segue.source as? AllGroupsViewController,
let groupIndexPath = allGroupsController.tableView.indexPathForSelectedRow,
!self.groups.contains(allGroupsController.groups[groupIndexPath.section])
else { return }
self.groups.append(allGroupsController.groups[groupIndexPath.section])
tableView.reloadData()
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(
nibName: "GroupCell",
bundle: nil),
forCellReuseIdentifier: "groupCell")
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
groups.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 currentGroup = groups[indexPath.row]
cell.configure(
photo: UIImage(systemName: "person.3.fill") ?? UIImage(),
name: currentGroup)
return cell
}
override func tableView(
_ tableView: UITableView,
commit editingStyle: UITableViewCell.EditingStyle,
forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
groups.remove(at: indexPath.row)
tableView.deleteRows(
at: [indexPath],
with: .fade)
}
}
}
First here is what I understand the goal is:
There is a UITableViewController called MyGroupsViewController which should show all the groups the user has selected
On tapping on + from the MyGroupsViewController, the user is taken to another UITableViewController called AllGroupsViewController which shows all the groups the user can join
On selecting a UITableViewCell from AllGroupsViewController, you will unwind back to MyGroupsViewController showing the groups added for the user
I will say you were quite close and I have just added some minor things which I hope will bring you close to your goal.
One way to pass data between UIViewControllers when using segue transitions is to override a function called prepare for segue (Read more about it in the Apple docs)
1
First change I will make is to add the override prepareForSegue to the end of the MyGroupsViewController class so that I can pass the groups array to the AllGroupsViewController
You can put it anywhere, I added it below your tableview editing function
override func tableView(_ tableView: UITableView,
commit editingStyle: UITableViewCell.EditingStyle,
forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
groups.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
// This was added by me
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Check the correct segue identifier and retrieve the destination
if segue.identifier == "addGroup",
let allGroupsViewController = segue.destination as? AllGroupsViewController
{
// Set the groups variable with data we stored in the
// user groups array
allGroupsViewController.userGroups = groups
}
}
2
In AllGroupsViewController, I added an array user groups to store all the groups the user taps on and the existing user's groups will be sent to it by MyGroupsViewController in step 1
class AllGroupsViewController: UITableViewController {
var groups = [
"cats",
"birds",
"dogs",
"books",
"music",
"movies",
"art",
"science",
"tech",
"beauty",
]
// I added this. This will save groups the user wants
var userGroups: [String] = []
3.
Then I made some small changes to your tableView didSelectRowAtIndexPath function to store what the user has tapped
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
defer {
tableView.deselectRow(at: indexPath, animated: true)
}
// Added by me: Retrieve the group tapped by the user
let groupKey = groupSectionTitles[indexPath.section]
var currentGroup = ""
if let groupValues = groupsDictionary[groupKey] {
currentGroup = groupValues[indexPath.row]
}
// Added by me: Check that this group was not added for the user
// before to avoid duplicates
if userGroups.firstIndex(of: currentGroup) == nil {
userGroups.append(currentGroup)
}
self.performSegue(withIdentifier: "addGroup", sender: nil)
}
4
Finally, back in MyGroupsViewController, I updated your addGroup unwind function slightly as follows:
#IBAction func addGroup(segue: UIStoryboardSegue) {
guard segue.identifier == "addGroup",
let allGroupsViewController = segue.source as? AllGroupsViewController
else { return }
// Added by me. Removed old code and added by me. Just update
// the groups array with what was selected in AllGroupsViewController
groups = allGroupsViewController.userGroups
tableView.reloadData()
}
You will get this end result with groups added to the user, without duplicates and also you are able to add groups with the same starting letter.
You can watch the experience here on Youtube
If I missed something or did not answer something from your question, please add some more info / questions in the comments.
I have a table which has a name and a picture in each cell. I have a search bar which searches through the names which does successfully happen however the images are blank. When you erase your search from the search bar, the images in the cell also do disappear! Would anyone know what I have done wrong and if so can someone please help me out!
Thank you
Had an issue with when search is deleted images are not shown but now it is fixed thanks to Raja
Only issue left is that it does not filter images when searched. Images are still blank when the cells are searched
import UIKit
class TestTableViewController: UITableViewController {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var userWorkoutName: UILabel!
var valueToPass: String!
var workoutName = ["Apple","Orange","Banana"]
var workoutImage = ["A","O","B"]
var searchingWorkouts = [String()]
var searching = false
override func viewDidLoad() {
super.viewDidLoad()
searchBar.delegate = self
searchingWorkouts = workoutName
}
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searching {
return searchingWorkouts.count
} else {
return workoutName.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIentifier = "Cell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIentifier, for: indexPath) as! WorkoutTableViewCell
if searching {
cell.workoutName.text = searchingWorkouts[indexPath.row]
cell.workoutImage.image = UIImage(named: searchingWorkouts[indexPath.row])
} else {
cell.workoutName.text = workoutName[indexPath.row]
cell.workoutImage.image = UIImage(named: workoutImage[indexPath.row])
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let moreDetail = storyboard.instantiateViewController(identifier: "UploadWorkoutViewController") as! UploadWorkoutViewController
if searching {
moreDetail.getWorkoutTitle = searchingWorkouts[indexPath.row]
} else {
moreDetail.getWorkoutTitle = workoutName[indexPath.row]
}
self.navigationController?.pushViewController(moreDetail, animated: true)
}
}
extension TestTableViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
searchingWorkouts = workoutName.filter({$0.prefix(searchText.count) == searchText } )
searching = true
if searchText.isEmpty {
searching = false
} else {
searching = true
}
tableView.reloadData()
}
}
Images are blank because you're not filtering the images, you're only filtering the workout names. And while searching you're assigning searchingWorkouts to image, which is totally wrong.
cell.workoutImage.image = UIImage(named: searchingWorkouts[indexPath.row])
Just like maintaining the searchingWorkouts, you need to maintain the searchingWorkoutImage as well. And then change the above line to this
cell.workoutImage.image = UIImage(named: searchingWorkoutImage[indexPath.row])
But the question is how will you filter the image names? Because workout names and image names are different.
So a better solution is to create a Workout class with name and image properties and change your code to the following
class Workout {
var name: String = ""
var image: String = ""
init(name: String, image: String) {
self.name = name
self.image = image
}
}
class TestTableViewController: UITableViewController {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var userWorkoutName: UILabel!
var valueToPass: String!
var workouts = [Workout(name: "Apple", image: "A"), Workout(name: "Orange", image: "O")]
var searchingWorkouts = [Workout]()
var searching = false
override func viewDidLoad() {
super.viewDidLoad()
searchBar.delegate = self
searchingWorkouts = workouts
}
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searching {
return searchingWorkouts.count
} else {
return workouts.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIentifier = "Cell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIentifier, for: indexPath) as! WorkoutTableViewCell
if searching {
cell.workoutName.text = searchingWorkouts[indexPath.row].name
cell.workoutImage.image = UIImage(named: searchingWorkouts[indexPath.row].image)
} else {
cell.workoutName.text = workouts[indexPath.row].name
cell.workoutImage.image = UIImage(named: workouts[indexPath.row].image)
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let moreDetail = storyboard.instantiateViewController(identifier: "UploadWorkoutViewController") as! UploadWorkoutViewController
if searching {
moreDetail.getWorkoutTitle = searchingWorkouts[indexPath.row].name
} else {
moreDetail.getWorkoutTitle = workouts[indexPath.row].name
}
self.navigationController?.pushViewController(moreDetail, animated: true)
}
}
extension TestTableViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
searchingWorkouts = workouts.filter({$0.name.prefix(searchText.count) == searchText } )
searching = true
if searchText.isEmpty {
searching = false
} else {
searching = true
}
tableView.reloadData()
}
}
I'm trying to accomplish searching for other users only when the user has typed at least 2 words, and only then begin the search in the database (because I don't want to scan the entire database for the users). I had some problems with 2 letter search, but I think I got the code (thanks to user Jay).
However when I run it in the simulator, the console prints name, but nothing shows up in the tableview? (its empty).
Do you know what I have done wrong?
This is my code:
class FollowUsersTableViewController: UIViewController {
#IBOutlet var tableView: UITableView!
private var viewIsHiddenObserver: NSKeyValueObservation?
let searchController = UISearchController(searchResultsController: nil)
var usersArray = [UserModel]()
var filteredUsers = [UserModel]()
var loggedInUser: User?
//
var databaseRef = Database.database().reference()
//usikker på den koden over
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchBar.delegate = self
//large title
self.title = "Discover"
if #available(iOS 11.0, *) {
self.navigationController?.navigationBar.prefersLargeTitles = true
} else {
// Fallback on earlier versions
}
self.tableView?.delegate = self
self.tableView?.dataSource = self
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
self.searchController.delegate = self;
definesPresentationContext = true
tableView.tableHeaderView = searchController.searchBar
}
func searchUsers(text: String) {
if text.count >= 2 {
self.usersArray = [] //clear the array each time
let endingText = text + "\u{f8ff}"
databaseRef.child("profile").queryOrdered(byChild: "username")
.queryStarting(atValue: text)
.queryEnding(atValue: endingText)
.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let childSnap = child as! DataSnapshot
print(childSnap)
let userObj = Mapper<UserModel>().map(JSONObject: childSnap.value!)
userObj?.uid = childSnap.key
if childSnap.key != self.loggedInUser?.uid { //ignore this user
self.usersArray.append(userObj!)
}
}
self.tableView.reloadData()
})
}
} //may need an else statement here to clear the array when there is no text
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let dest = segue.destination as! UserProfileViewController
let obj = sender as! UserModel
let dict = ["uid": obj.uid!, "username": obj.username!, "photoURL": obj.photoURL, "bio": obj.bio]
dest.selectedUser = dict as [String : Any]
}
}
// MARK: - tableview methods
extension FollowUsersTableViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchController.searchBar.text!.count >= 2 ? filteredUsers.count : 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! FollowTableViewCell
let user = filteredUsers[indexPath.row]
cell.title?.text = user.username
if let url = URL(string: user.photoURL ?? "") {
cell.userImage?.sd_setImage(with: url, placeholderImage: #imageLiteral(resourceName: "user_male"), options: .progressiveDownload, completed: nil)
cell.userImage.sd_setIndicatorStyle(.gray)
cell.userImage.sd_showActivityIndicatorView()
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.performSegue(withIdentifier: "user", sender: self.filteredUsers[indexPath.row])
}
}
// MARK: - search methods
extension FollowUsersTableViewController:UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate {
func updateSearchResults(for searchController: UISearchController) {
searchController.searchResultsController?.view.isHidden = false
self.searchUsers(text: self.searchController.searchBar.text!)
filterContent(searchText: self.searchController.searchBar.text!)
self.tableView.reloadData()
}
func filterContent(searchText:String){
if searchText.count >= 2{
self.filteredUsers = self.usersArray.filter{ user in
return(user.username!.lowercased().contains(searchText.lowercased()))
}
}
}
}
This code is very close, and as the other answer points out you've got two arrays.
But let's really simplify the issue: You only need one array since you are filtering Firebase, not the array.
In other words when a user types into the searchField, you're querying Firebase for results and putting them into an array. That's the same array you should be using as the dataSource for your tableView.
As your code sits, you're filtering Firebase and then filtering those results again which isn't needed.
So at a high level supposed we have four users in our database
Larry
Moe
Monroe
Curly
the user types 'Mo' into the search field which causes your Fire query to execute. It will return two results:
Mo
Monroe
which then gets populated into an array - we'll call it userResultsArray.
then
tableView.reloadData()
which then calls the tableView delegate methods
func tableView(_ tableView: UITableView, numberOfRowsInSection
return userResultsArray.count
and
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
get the user from userResultsArray
return the user name
As shown, you only need the single array to store the queried (filtered) results from Firebase.
You have 2 arrays usersArray and filteredUsers
if childSnap.key != self.loggedInUser?.uid { //ignore this user
self.usersArray.append(userObj!)
}
...
self.tableView.reloadData()
so the the above reload has no effect as you always use
let user = filteredUsers[indexPath.row]
in cellForRowAt , in such cases you should have a var like
var isSearching = false
and alter it when you search then in all delegate & dataSource methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return isSearching ? filteredUsers.count : usersArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! FollowTableViewCell
let user = isSearching ? filteredUsers[indexPath.row] : usersArray[indexPath.row]
.....
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
isSearching = true
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
isSearching = false
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
isSearching = false
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
isSearching = false
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredUsers = usersArray.filter { /* do filter */ }
if(filteredUsers.count == 0){
isSearching = false
} else {
isSearching = true
}
self.tableView.reloadData()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.performSegue(withIdentifier: "user", sender: isSearching ? self.filteredUsers[indexPath.row] : self.usersArray[indexPath.row])
}
I am trying to put Ads totally randomly between cells inside a UITableView. I am gonna show my main file to you understand what I am doing and how I want:
Table View Controller:
class Page1: UITableViewController, UISearchBarDelegate {
#IBOutlet weak var searchBar: UISearchBar!
var employeesSearching = [Employee]()
var isSearching : Bool = false
#IBOutlet weak var GoogleBannerView: GADBannerView!
let collation = UILocalizedIndexedCollation.current()
var sections: [[Any]] = []
var objects: [Any] = [] {
didSet {
let selector: Selector = #selector(getter: UIApplicationShortcutItem.localizedTitle)
sections = Array(repeating: [], count: collation.sectionTitles.count)
let sortedObjects = collation.sortedArray(from: objects, collationStringSelector: selector)
for object in sortedObjects {
let sectionNumber = collation.section(for: object, collationStringSelector: selector)
sections[sectionNumber].append(object as AnyObject)
}
self.tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.searchBar.delegate = self
self.tableView.contentOffset = CGPoint(x: 0, y: searchBar.frame.height) //hide searchBar
Shared.instance.employees.sort {
(first, second) in
first.name.compare(second.name, options: .diacriticInsensitive) == .orderedAscending
}
}
func getMatches(letter: String, withArray array: [Employee]) -> [Employee] {
return array.filter({ ($0.name.compare(letter, options: .diacriticInsensitive, range: $0.name.startIndex..<$0.name.index($0.name.startIndex, offsetBy: 1), locale: nil) == .orderedSame)})
}
override func numberOfSections(in tableView: UITableView) -> Int {
if isSearching { return 1 }
return collation.sectionTitles.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let letter = collation.sectionTitles[section]
if isSearching {
return employeesSearching.count
} else {
let matches = getMatches(letter: letter, withArray: Shared.instance.employees)
if !matches.isEmpty { return matches.count }
}
return 0
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if isSearching { return nil }
let letter = collation.sectionTitles[section]
let matches = getMatches(letter: letter, withArray: Shared.instance.employees)
if matches.count == 0 { return nil }
return collation.sectionTitles[section] }
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
if isSearching { return nil }
return collation.sectionIndexTitles }
override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return collation.section(forSectionIndexTitle: index) }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 3 || indexPath.row == 9 || indexPath.row == 14 {
let cellAd = tableView.dequeueReusableCell(withIdentifier: "cellAd", for: indexPath)
GoogleBannerView?.adUnitID = "ca-app-pub-6043248661561548/4628935113"
GoogleBannerView?.rootViewController = self
GoogleBannerView?.load(GADRequest())
return cellAd
}
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell1
if isSearching {
cell.nameLabel.text = employeesSearching[indexPath.row].name
cell.positionLabel.text = employeesSearching[indexPath.row].position
} else {
let letter = collation.sectionTitles[indexPath.section]
let matches = getMatches(letter: letter, withArray: Shared.instance.employees)
cell.nameLabel.text = matches[indexPath.row].name
cell.positionLabel.text = matches[indexPath.row].position
}
return cell
}
...
}
How do I smuggle a UITableViewCell as! AdCell randomly into the UITableView?
I mean, what should I do in cellForRowAt? I am a bit confused between all these indexed sections.
Firstly you need to generate a random number between 0 and your tableView Datasource array size
let lower : UInt32 = 0
let upper : UInt32 = array.count
let randomIndex = arc4random_uniform(upper - lower) + lower
then you need to add the Ad object in the array at the randomIndex
array.insert(AdObject, atIndex:randomIndex)
then just reload your tableView and handle the different types in cellForRow function
One approach would be to insert some sort of "ad" object at desired locations within your data model.
Then update cellForRowAt to look at the object in the data model for the given index path. If it's an "ad" object, create and setup an "ad" cell. Otherwise create and setup an appropriate data cell as you do now.
I'm trying to make my app iOS 7 compatible with a search bar. So that means I can't use UISearchController (like I would far prefer to be doing). So I'm trying to implement the search bar with searchDisplayController. But whenever I run the app and enter text into the search bar, the app crashes.
I plugged in a few break points to see when exactly this is happening, and it seems that it happens at the numberOfRowsInSection function. Although I have no idea why that is because when I print to the console in this function, right before crash the filtered data array is what I would expect it to be given the input text in the status bar.
So here is my code. Maybe I'm missing something. A second set of eyes on this is greatly appreciated. Thanks!
class RegData2: UITableViewController, UISearchBarDelegate, UISearchDisplayDelegate {
// Beginning to ios7 compatibility
let model = Model()
var prevArray = [String]()
var selectionPrev = String()
var selection : String = ""
var filteredTableData = [String]()
var int = 0
override func viewDidLoad() {
super.viewDidLoad()
// Reload the table
self.tableView.reloadData()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (tableView == self.searchDisplayController?.searchResultsTableView) {
return self.filteredTableData.count
}
else {
return prevArray.count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell!
cell.textLabel?.font = UIFont.boldSystemFontOfSize(18)
if (tableView == self.searchDisplayController?.searchResultsTableView) {
cell.textLabel?.text = filteredTableData[indexPath.row]
return cell
}
else {
cell.textLabel?.text = prevArray[indexPath.row]
return cell
}
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
tableView.deselectRowAtIndexPath(indexPath, animated: true)
// controller.searchBar.resignFirstResponder()
if (tableView == self.searchDisplayController?.searchResultsTableView) {
selection = filteredTableData[indexPath.row]
}
else {
selection = prevArray[indexPath.row]
}
performSegueWithIdentifier("regData2ToRegView", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "regData2ToRegView" {
let regView = segue.destinationViewController as! RegView
regView.prevSelection = selection
regView.prevSelectionType = selectionPrev
}
}
func filterContentForSearchText(searchText: String, scope: String = "Title") {
filteredTableData = prevArray.filter({( item : String) -> Bool in
var match = (scope == "Title")
var stringMatch = item.rangeOfString(searchText)
return match && (stringMatch != nil)
})
}
func searchDisplayController(controller: UISearchDisplayController, shouldReloadTableForSearchString searchString: String?) -> Bool {
self.filterContentForSearchText(searchString!, scope: "Title")
return true
}
func searchDisplayController(controller: UISearchDisplayController, shouldReloadTableForSearchScope searchOption: Int) -> Bool {
self.filterContentForSearchText(self.searchDisplayController!.searchBar.text!, scope: "Title")
return true
}
}