Swift UITableView Index Out of Range Error - ios

I am running a Firebase app in Swift. Here I am fetching Followers and Following Users. In cases where the user has just signed up, the app crashes as there may be no follower/following users yet. I have tried everything I can think of to handle the error, and all are not helping. My question is :
1. How can I avoid this crash?
2. Is there any way I can put an image or a button instead of blank tables that will redirect them to follow other users?
Below is the code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "InProfileCell", for: indexPath) as! ConnectionsCell
if currentIndex == 1 {
let user = followerusers[indexPath.row]
cell.showFollowersUsers(val: user)
print("follower")
} else {
let user = followingUsers[indexPath.row]
cell.showFollowingUsers(val: user)
print("following")
}
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if currentIndex == 0 {
return followingUsers.count
} else {
return followerusers.count
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}

The logic in your numberOfRows and cellForRow is inconsistent. Your numberOfRows states that
if currentIndex == 0 {
followingUsers
} else {
followerUsers
}
However, your cellForRow is inconsistent with this because it states that
if currentIndex == 1 { //NOTE THAT YOU ARE USING 1 HERE INSTEAD OF 0
followerUsers
} else {
followingUsers
}
Now when the currentIndex is 2, the numberOfRows will return count of the followerUsers but the cellForRow would try to access the values of followingUsers (instead of followerUsers). That is the reason you are getting index out of range.

Related

Expanding tableview sections on clicking each section

I have a tableview which has 2 sections and some cells(which can be dynamic)below each section showing associated data.
This is the code I have written to show the data...
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return recentUsers?.count
} else {
return groupNameArray?.count
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section == 0 {
return " CHAT LIST"
} else {
return " GROUPS"
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! RecentMessageCell
cell.tag = indexPath.row
if indexPath.section == 0 {
if let user = recentChatUsers?[indexPath.row] {
cell.idLabel?.text = user.id
}
} else {
if groupNameArray.isEmpty == false {
let grpArr = groupNameArray[indexPath.row]
cell.userNameLabel?.text = grpArr.grpname
}
}
return cell
}
Now what I want to achieve is if I click on the first section, it should expand and show the cells it contains and the same should happen with the second cell also. Clicking on each of those sections again should hide the cells that were expanded.
I did search the internet for solutions. But though there were resources available, I couldn't find much help for my problem...
Add an array to keep track of section expend/collapse
let sectionStats = [Bool](repeating: true, count: 2)
Add a, IBAction to track section tap, update value of sectionStats for the corresponding section and reload section
and update your numberOfRowsInSection as show below
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard sectionStats[section] else {
return 0
}
if section == 0 {
return 1
} else {
return list.count
}
}
Tappable Header:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return headerView(section: section, title: section == 0 ? "CHAT LIST" : "GROUPS")
}
private func headerView(section: Int, title: String) -> UIView {
let button = UIButton(frame: CGRect.zero)
button.tag = section
button.setTitle(title, for: .normal)
button.setTitleColor(UIColor.red, for: .normal)
button.addTarget(self, action: #selector(sectionHeaderTapped), for: .touchUpInside)
return button
}
#objc private func sectionHeaderTapped(sender: UIButton) {
let section = sender.tag
sectionStats[section] = !sectionStats[section]
tableView.beginUpdates()
tableView.reloadSections([section], with: .automatic)
tableView.endUpdates()
}
Good tutorial on How to build a Table View with Collapsible Sections:
https://medium.com/ios-os-x-development/ios-how-to-build-a-table-view-with-collapsible-sections-96badf3387d0
This kind of feature requires a bit more code and I cannot write the whole code here but I can explain you the concepts that will be used to achieve this and will attach a few good tutorials which I used to ultimately create a feature like this
First you need to create a custom HeaderView for your sections
Next you need a UITapGestureRecognizer on your section and need to write your login inside the function provided in action part of UITapGestureRecognizer's constructor
You need to create a separate Protocol inside your HeaderView file and your ViewController that contains your TableView will adopt to that protocol and will handle whether to expand or collapse your rows
Also, you will need to create a separate Struct instance for each section which will contain a boolean variable that will indicate whether that section is expanded or collapsed
That is the basic concept that will be needed while creating Expandable List in iOS.
Below I have attached links to some of the tutorials :
Tutorial 1
Tutorial 2
Tutorial 3
Tutorial 4

Swift using guard and fatal error specified in an function

I use enum for building my UITableView cells:
enum VAXSections: Int
{
case Segmented = 0
case Scrollable = 1
case ScheduledMode = 2
case ScheduledFooter = 3
case SilentHours = 4
case SilentFooter = 5
}
here how I use it:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
guard let unwrappedSection = VAXSections(rawValue: indexPath.section) else {
showFatalError()
return nil
}
Few problem here I want to guard my section value if it's out of max case in my enum. For example if indexPath.section will be bigger then 5 then app should fall back. But if you see we can't return nil here, as cellForRowAtIndexPath has to return cell in any case.
I can solve problem by providing more readability with replacing showFatalError() fiction with:
guard let unwrappedSection = VAXSections(rawValue: indexPath.section) else {
fatalError("Check \(String(VAXUnitDashboardViewController.self)) UITableView datasource or check \(String(VAXSections.self)) enum.")
}
then I don't need to return any value. But then I turned in another problem. As I need to specify at least 3 datasource functions of my UITableView I need to duplicate fatal error which I wish to replace with one function that do the same all the time:
fatalError("Check \(String(VAXUnitDashboardViewController.self)) UITableView datasource or check \(String(VAXSections.self)) enum.")
enum VAXItems: String {
case Item1
case Item2
case Item3
}
enum VAXSections: String {
case Segmented
case Scrollable
case ScheduledMode
case ScheduledFooter
case SilentHours
case SilentFooter
}
struct VAXModel {
var type: VAXSections
var items: [VAXItems]
}
Then on your UIViewController you can have:
class ViewController: UIViewController {
private var model: [VAXModel] = []
override func viewDidLoad() {
super.viewDidLoad()
model = [
VAXModel(type: .ScheduledMode, items: [.Item1, .Item2, .Item3]),
VAXModel(type: .SilentFooter, items: [.Item1])
]
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return model.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model[section].items.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(String(UITableViewCell), forIndexPath: indexPath)
let item = model[indexPath.section].items[indexPath.row]
switch item {
case .Item1: cell.textLabel?.text = item.rawValue
case .Item2: // Config
case .Item3: // Config
}
return cell
}
}
I don't think you need to have it in 3 places actually. Assuming that the 3 data source methods you are talking about are :
func numberOfSectionsInTableView(_ tableView: UITableView) -> Int
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
You actually need to have your guard in only one of them.
numberOfSectionsInTableView is the first method that will be called, so if you fail here, the other two methods won't be called. If the number of sections is based on some calculations, you can also cut off the value, something like this : if calculatedNumberOfSections > 6 { return 6 } else { return calculatedNumberOfSections } (remember that section numbering is 0 based)
numberOfRowsInSection - if you guard here you have two options - either fail with fatalError or (better in my opinion) - return 0 if a section number higher than 5 gets passed. Returning 0 will result in cellForRowAtIndexPath not being called with that section.
cellForRowAtIndexPath - you already got this :)

Two custom tableViewCells in UITableView

I am trying to create a contacts page where you can see all your contacts with a friend request cell showing up when you receive a friend request, but not there when you do not have any. At the moment, both custom cells work fine. The issue I have is that the contactRequestTableViewCell overlaps the first cell of the contactListTableViewCell.
I have researched other questions about two custom tableviewcells and none are quite having the same issues that I am facing.
Here is my executing code at the moment, I am returning 2 sections in the table view.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! ContactListTableViewCell
let requestCell = tableView.dequeueReusableCellWithIdentifier("requestCell", forIndexPath: indexPath) as! ContactRequestsTableViewCell
let user = OneRoster.userFromRosterAtIndexPath(indexPath: indexPath)
if (amountOfBuddyRequests > 0) {
if (indexPath.section == 0) {
requestCell.hidden = false
cell.hidden = false
requestCell.friendRequestLabel.text = "test"
} else if (indexPath.section >= 1) {
cell.contactNameLabel!.text = user.displayName;
cell.contactHandleLabel!.text = "# " + beautifyJID(user.jidStr)
cell.contactHandleLabel!.textColor = UIColor.grayColor()
OneChat.sharedInstance.configurePhotoForImageView(cell.imageView!, user: user)
}
return cell;
}
else { // if buddy requests == 0
requestCell.hidden = true
cell.contactNameLabel!.text = user.displayName;
cell.contactHandleLabel!.text = "# " + beautifyJID(user.jidStr)
cell.contactHandleLabel!.textColor = UIColor.grayColor()
print ("This is how many unreadMessages it has \(user.unreadMessages)")
// If there is unread messages for a person highlight it blue
// However this feature isn't working right now due to unreadMessages bug
if user.unreadMessages.intValue > 0 {
cell.backgroundColor = .blueColor()
} else {
cell.backgroundColor = .whiteColor()
}
OneChat.sharedInstance.configurePhotoForCell(cell, user: user)
return cell;
}
}
This is the current output that I have right now, my cells that have "test" are covering up other contactListTableViewCells.
The function tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell should always return one and the only one TableViewCell you want at indexPath, so you don't want to always return cell of type ContactListTableViewCell.
According to documentation, the cellForRowAtIndexPath tableView method asks for the cell at the indexPath, which means literally there can only be one cell at certain row of a certain section, so returning two cells is not an option.
I suggest you use two arrays to store the requests and contacts information. For example, you have arrays requests and contacts. Then you can tell the tableView how many rows you want:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return requests.count + contacts.count
}
and then in cellForRowAtIndexpath you do something like:
override func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row < requests.count {
// return a request cell
}
else {
// return a contact cell
}
}
I'm only using one tableView section here. If you still want two sections you can simply return 2 in numberOfSections function and add if statements in cellForRowAtIndexPath for indexPath.section.
Hope this helps.
It turns out that the issue was dealing with the data sources. My data sources were not pointing to the correct tableviewcell. This resulted in them pointing to an incorrect cell. This issue was fixed by remaking the data sources system that was in place. This issue will not affect most as the data sources will point to the correct tableviewcell by default.
Contrary to what another poster said, you can in fact display two or more custom cells in a single table. This is how I fixed the tableView display issues:
var friendRequests = ["FriendRequest1", "FriendRequest2"]
var contacts = ["User1","User2","User3","User4"]
var amountOfBuddyRequests = 1
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if (amountOfBuddyRequests > 0) {
return 2
}
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (amountOfBuddyRequests > 0) {
if (section == 0) {
return friendRequests.count
}
}
return contacts.count
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if (amountOfBuddyRequests > 0) {
if (indexPath.section == 0) {
let requestCell = tableView.dequeueReusableCellWithIdentifier("requestCell") as! ContactRequestsTableViewCell
requestCell.friendRequestLabel.text = friendRequests[indexPath.row]
requestCell.onButtonTapped = {
self.friendRequests.removeAtIndex(indexPath.row)
self.tableView.reloadData()
}
requestCell.addButtonTapped = {
self.addUser(self.friendRequests[indexPath.row])
self.friendRequests.removeAtIndex(indexPath.row)
self.tableView.reloadData()
}
return requestCell
}
}
let friendCell = tableView.dequeueReusableCellWithIdentifier("FriendCell") as! ContactListTableViewCell
friendCell.contactNameLabel.text = contacts[indexPath.row]
return friendCell
}

fatal error: Index out of range

I want to show 25 of the songs I have in my library. This is my code:
var allSongsArray: [MPMediaItem] = []
let songsQuery = MPMediaQuery.songsQuery()
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 25 //allSongsArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell")
let items = allSongsArray[indexPath.row]
cell?.textLabel?.text = items.title
cell?.detailTextLabel?.text = items.artist
cell?.imageView?.image = items.artwork?.imageWithSize(imageSize)
return cell!
}
When I run this, it crashes, because:
fatal error: Index out of range
I tried to change the numberOfRowsInSection to allSongsArray.count, but it ends up with the same error.
You should return allSongsArray.count and to avoid being returned empty cells use yourTableView.reloadData after you fill your allSongsArray.
When you first create the array it is empty. Hence, it will give you out of bound error.
Try to return the count of the songs array instead.
1st you need to get the data into the array and then update the table view.
Here is a sample code:
#IBAction private func refresh(sender: UIRefreshControl?) {
if myArray.count > 0 {
self.tableView.reloadData()
sender?.endRefreshing()
} else {
sender?.endRefreshing()
}
}
In case your app crashes in
let items = allSongsArray[indexPath.row]
as you check if the allSongsArray.count, u can safe guard it by making sure that the [indexPath.row] doesn't exceed your array count. so u can write a simple if condition as;
if allSongsArray.count > 0 && indexPath.row < allSongsArray.count {
//Do the needful
}
Please return the actual array count instead of static
return allsongsarray.count
For folks looking for a solution to swiping on a filtered list, you need to provide the section as well as the row or xcode throws this error.
func swipe(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let myRow = filteredList[indexPath.section].items[indexPath.row]
}
not
func swipe(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let myRow = filteredList[indexPath.row]
}

Return no cell based on nil variable

This is a bit of a continuation of this question, but basically I am trying to figure out how I can return no cell if the result of a function is nil.
This is my code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("rideCell", forIndexPath: indexPath) as! RideCell
var ride = DataManager.sharedInstance.getRideByName(favouritesArray[indexPath.row] as! String)
println(ride)
if ride != nil {
cell.rideNameLabel.text = ride!.name
var dateFormat = NSDateFormatter()
dateFormat.dateFormat = "h:mm a"
cell.updatedLabel.text = dateFormat.stringFromDate(ride!.updated!)
if ride!.waitTime! == "Closed" {
cell.waitTimeLabel.text = ride!.waitTime!
} else {
cell.waitTimeLabel.text = "\(ride!.waitTime!)m"
}
}
return cell
}
So at the moment everything works, however wherever ride does equal nil, I just get the prototype cell returned, whereas I would like it to return nothing.
I have tried hiding or changing the height of these cells to nil, but it just seems like messy solution. Anyone know a better one?
Thanks.
determine the number of rows by implementing tableView(_ tableView: UITableView, numberOfRowsInSection section: Int)
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) {
if section == 0 {
return arrayName.count
}
return 0
}
I figured out a solution. As a few people mentioned, I had to figure out what to display before dealing with cellForRowAtIndexPath.
Basically I added some code to figure out which favourites could be found in the array and put it in viewWillAppear.
for index in 0...favouritesArray.count - 1 {
var ride = DataManager.sharedInstance.getRideByName(favouritesArray[index] as! String)
if ride != nil {
favouritesFound.append(ride!.name!)
println(favouritesFound)
}
}
Works perfectly now! Thanks everyone.
It is assumed that you always know how many valid rows do you have and you publish this number by implementing UITableViewDataSource's tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int method. So if at some moment number of rows changes you should call UITableView.reloadData not to try to hide excess rows.
To get an array of valid rides try something like:
var newArray:[String] = []
for str in favouritesArray where DataManager.sharedInstance.getRideByName(str!) != nil
{
newArray += [str]
}

Resources