reloadSections UITableView Swift - ios

I am trying to reload section and update the number of cells in a section, but I'm getting an error and crash whenever I try. Here is the code I am using:
import UIKit
import CalendarView
import SwiftMoment
class TimeAwayRequestTableViewController: UITableViewController {
#IBOutlet var calendarView: CalendarView!
var selectedDates : [Moment] = []
override func viewDidLoad() {
super.viewDidLoad()
calendarView.delegate = self
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var returnInt = 0
if section == 0 {
returnInt = 2
}
if section == 1 {
returnInt = 1
}
if section == 2 {
print(selectedDates.count)
returnInt = selectedDates.count
}
return returnInt
}
}
extension TimeAwayRequestTableViewController : CalendarViewDelegate {
func calendarDidSelectDate(date: Moment) {
selectedDates.append(date)
print(selectedDates)
let section = NSIndexSet(index: 2)
self.tableView.beginUpdates()
self.tableView.reloadSections(section, withRowAnimation: .Automatic)
self.tableView.endUpdates()
}
func calendarDidPageToDate(date: Moment) {
print(date)
}
}
So basically when the date is tapped in the calendar, I add it to the Array and then update the number of cells. I haven't gotten to the point where I configure the cell content, right now I'm just trying to make sure this is working like i want. The error I get is:
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]'
But i don't understand why because it counts and shows 2. So I'm confused.

I bet you used some arrays(including selectedDates) in func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell and it was beyond of bounds.Please check your data.

Related

UITableView reloadData() causing reload to UISearchBar inside every sections

I have a tableview which has 2 sections. Both of the sections have UISearchBar in the indexPath.row 0 and the rest of the rows in each section populate the list of array.
Whenever I type some text in search bar every time the searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) delegate method gets called and inside the delegate method I call tableView.reloadData() to reload the search results in tableview.
Now the problem is each time the tableView reloads the UISearchBar reloads too (as UISearchbar is in row number 1) and every time the SearchBar keypad Resigns.
Instead of doing tableView.reloadData() I even tried to reload every row except the first one using bellow code
let allButFirst = (self.tableView.indexPathsForVisibleRows ?? []).filter { $0.section != selectedSection || $0.row != 0 }
self.tableView.reloadRows(at: allButFirst, with: .automatic)
But no luck. App gets crashed saying
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert row 2 into section 0, but there are only 2 rows in section 0 after the update'
You are probably changing the data source and then you are reloading rows at index paths what doesn't exist yet.
It is not so easy, but let's have an example: Before you start typing, the search result will contain something like this:
["aa", "ab", "ba", "bb"]
Then you will type "a" to the search bar and data source changes into:
["aa", "ab"]
tableView.deleteRows(at: [IndexPath(row:3, section: 0), IndexPath(row:4, section: 0)], with: .automatic)
then you delete everything in this searchbar and your data source will change to the default: ["aa", "ab", "ba", "bb"]
so in this case you need to call:
tableView.insertRows(at: [IndexPath(row:3, section: 0), IndexPath(row:4, section: 0)], with: .automatic)
I created some working example - without storyboard source, I believe it is pretty simple to recreated it according this class.
class SearchCell: UITableViewCell {
#IBOutlet weak var textField:UITextField?
}
class TextCell: UITableViewCell {
#IBOutlet weak var label:UILabel?
}
class ViewController: UIViewController, UITableViewDataSource, UITextFieldDelegate {
#IBOutlet weak var tableView: UITableView?
weak var firstSectionTextField: UITextField?
var originalDataSource:[[String]] = [["aa","ab","ba","bb"], ["aa","ab","ba","bb"]]
var dataSource:[[String]] = []
let skipRowWithSearchInput = 1
override func viewDidLoad() {
super.viewDidLoad()
dataSource = originalDataSource
tableView?.tableFooterView = UIView()
tableView?.tableHeaderView = UIView()
}
func numberOfSections(in tableView: UITableView) -> Int {
return dataSource.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource[section].count + skipRowWithSearchInput
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0, let cell = tableView.dequeueReusableCell(withIdentifier: "search", for: indexPath) as? SearchCell {
cell.textField?.removeTarget(self, action: #selector(textFieldDidChangeText(sender:)), for: .editingChanged)
cell.textField?.addTarget(self, action: #selector(textFieldDidChangeText(sender:)), for: .editingChanged)
if indexPath.section == 0 {
firstSectionTextField = cell.textField
}
return cell
} else if let cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? TextCell {
cell.label?.text = dataSource[indexPath.section][indexPath.row - skipRowWithSearchInput]
return cell
} else {
return UITableViewCell()
}
}
#objc func textFieldDidChangeText(sender: UITextField) {
let section = sender == firstSectionTextField ? 0 : 1
let text = sender.text ?? ""
let oldDataSource:[String] = dataSource[section]
//if the search bar is empty then use the original data source to display all results, or initial one
let newDataSource:[String] = text.count == 0 ? originalDataSource[section] : originalDataSource[section].filter({$0.contains(text)})
var insertedRows:[IndexPath] = []
var deletedRows:[IndexPath] = []
var movedRows:[(from:IndexPath,to:IndexPath)] = []
//resolve inserted rows
newDataSource.enumerated().forEach { (tuple) in let (toIndex, element) = tuple
if oldDataSource.contains(element) == false {
insertedRows.append(IndexPath(row: toIndex + skipRowWithSearchInput, section: section))
}
}
//resolve deleted rows
oldDataSource.enumerated().forEach { (tuple) in let (fromIndex, element) = tuple
if newDataSource.contains(element) == false {
deletedRows.append(IndexPath(row: fromIndex + skipRowWithSearchInput, section: section))
}
}
//resolve moved rows
oldDataSource.enumerated().forEach { (tuple) in let (index, element) = tuple
if newDataSource.count > index, let offset = newDataSource.firstIndex(where: {element == $0}), index != offset {
movedRows.append((from: IndexPath(row: index + skipRowWithSearchInput, section: section), to: IndexPath(row: offset + skipRowWithSearchInput, section: section)))
}
}
//now set dataSource for uitableview, right before you are doing the changes
dataSource[section] = newDataSource
tableView?.beginUpdates()
if insertedRows.count > 0 {
tableView?.insertRows(at: insertedRows, with: .automatic)
}
if deletedRows.count > 0 {
tableView?.deleteRows(at: deletedRows, with: .automatic)
}
movedRows.forEach({
tableView?.moveRow(at: $0.from, to: $0.to)
})
tableView?.endUpdates()
}
}
the result:
If do you need to clarify something, feel free to ask in comment.
Try this-
tableView.beginUpdates()
//Do the update thing
tableView.endUpdates()
It worked.
I took two sections one for search field and another for reloading data (rows populating data).
I took separate custom cell for search and took outlet in that class itself.
and in viewForHeaderInSection I used tableView.dequeueReusableCell(withIdentifier:) and returned customCell.contentView
Then I called tableview.ReloadData() in searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
It worked without problem.

IOS swift 4 expand and collapse tableview not working and causing crash

Following is my code to add section header view cell
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "menuHeaderTableViewCellID") as! MenuHeaderTableViewCell
cell.foodMenuItems = menuResult?.foodMenuItems?[section]
cell.setParentUI()
cell.expandCollapseClicked = {
[weak self]
(postiton) in
let isCollapsed = self?.menuResult?.foodMenuItems?[postiton].isCollapsed ?? false
self?.menuResult?.foodMenuItems?[postiton].isCollapsed = !isCollapsed
self?.tableViewMenu?.beginUpdates()
self?.tableViewMenu?.reloadSections([section], with: .fade)
self?.tableViewMenu?.endUpdates()
}
return cell
}
Following is code for count in each row and section
func numberOfSections(in tableView: UITableView) -> Int {
return (menuResult?.foodMenuItems?.count) ?? 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let rowCount = menuResult?.foodMenuItems?[section].items?.count ?? 0
let isCollpased = menuResult?.foodMenuItems?[section].isCollapsed ?? false
return isCollpased ? 0 : rowCount
}
Following is code inside header view cell
#IBAction func expandCollapseClicked(_ sender: UIButton) {
guard let superView = self.superview as? UITableView else {
return
}
expandCollapseClicked?(superView.indexPath(for: self)?.section ?? 0)
}
I am facing issue on collapsing first section my header disappears and when I try to collapase other section getting following exception how to fix this?
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (25), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
*** First throw call stack:
Problem may be in :
Avoid dequeueReusableCell. Try to use UIView with label and button.
expandCollapseClicked = {...}
should be called by UIButton. as it's sender is UIButton. Try to use:
view.button.expandCollapseClicked = {...}
Hope this will work.

I mixed static and dynamic UITableViewCells in my UITableView and got wrong values in numberOfRowsInSection

In my swift ios app I have a UITableView - in story board I added there 3 cells. The first two are static ones and the 3rd one is dynamic, basically first two shows some information and the 3rd one (and the rest generated based on 3rd) show comments. I fetch comments as a json from my webserver.
When json is empty I see cell no. 1 and no. 2 - that's fine. But when json has one value, I see only cell no. 1, I don't see either 2 or 3. When json has 2 comments - I see two static cells.
When json has 3 comments, I see two static cells + 3rd comment. Basically I never see two first comments.
The problem might be here - this is how I'm fetching comments from webservice:
#IBOutlet weak var myTableView: UITableView!
var items = NSMutableArray()
....
Alamofire.request(.GET, "\(serverURL)/comments/\(case_id)/comments/")
.validate()
.responseJSON { response in
switch response.result {
case .Success:
self.items.removeAllObjects()
if let jsonData = response.result.value {
let data = JSON(jsonData)
if let responseDictionary = data.dictionary {
if let commentsArray = responseDictionary["comments"]?.array {
for commentObject in commentsArray {
if let singleComment = SingleComment.fromJSON(commentObject){
self.items.addObject(singleComment)
}
}
self.myTableView.reloadData()
}
}
}
self.myTableView.reloadData()
case .Failure(let error):
print("SWITCH ERROR comments")
print(error)
}
}
and my method numberOfRowsInSection looks as follows:
func tableView(tview: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.items.count == 0{
return 2
} else {
return self.items.count;
}
}
I thought the solution could be to just modify the return statement in else block above, so that it is return self.items.count+2, but that throws an error:
* Terminating app due to uncaught exception 'NSRangeException', reason: '* -[__NSArrayM objectAtIndex:]: index 2 beyond bounds [0 ..
0]'
what could I do in such situation?
====== EDIT
my cellForRowAtIndexPath is a longer method, but I trimmed it down for the most important stuff:
func tableView(myComments: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = myComments.dequeueReusableCellWithIdentifier("firstCell") as! FirstCell
...
return cell
}
else if indexPath.row == 1 {
let cell = myComments.dequeueReusableCellWithIdentifier("cellStatic") as! SecondCell
...
return cell
} else {
let cell = myComments.dequeueReusableCellWithIdentifier("cell") as! SingleCommentCell
let comment:SingleComment = self.items[indexPath.row] as! SingleComment
...
return cell
}
}
You need to allow for the 2 static rows in both your numberOfRowsInSection and cellForRowAtIndexPath functions. The number of rows is 2 plus the count of your items
func tableView(tview: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count + 2;
}
When retrieving the item to display, the row value will be two more than then the required items array index, so row 2 will require item[0]:
func tableView(myComments: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = myComments.dequeueReusableCellWithIdentifier("firstCell") as! FirstCell
...
return cell
}
else if indexPath.row == 1 {
let cell = myComments.dequeueReusableCellWithIdentifier("cellStatic") as! SecondCell
...
return cell
} else {
let cell = myComments.dequeueReusableCellWithIdentifier("cell") as! SingleCommentCell
let comment = self.items[indexPath.row - 2] as! SingleComment
...
return cell
}
}
Also, I would suggest you modify your fetch code and array definition so that that items is var items = [SingleComment]() rather than an NSMutableArray. This will remove the need to downcast the objects you retrieve from the array.

dispatch_async() block didn't finish before UITableViewDataSource methods

There is a UITableView which its cells will be filled by data got with HTTP post request. But UITableView functions executed before the data comes. When the application starts, all three tableView methods executed and then application throws a runtime error. I guess it's because in cellForRowAtIndexPath, messageList is still empty.
Here is the code:
class messageViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
var authData : NSDictionary = [:]
var funcLib = functionLibrary()
var messagesList : NSArray = []
var messageCount: Int = 0
#IBOutlet weak var messageTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
var authCode = self.authData["auth"] as! String
var userID = self.authData["user_id"] as! String
var messageRequsetBodyData: AnyObject = ["op":"users","op2":"getThisWeekMessages","id":"\(userID)","id2":"","id3":"","authCode":"\(authCode)"] as AnyObject
funcLib.HTTPPostRequest("http://asdasd.asdasdasd.com/services/index.php", bodyData: messageRequsetBodyData){data in
dispatch_async(dispatch_get_main_queue()){
if let data = data{
var messaggesListDic = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as! NSDictionary
println("------MESSAGGES---------")
self.messageCount = messaggesListDic["count"] as! Int
//self.messages = messaggesListDic["messages"] as! NSDictionary
self.messagesList = messaggesListDic["messages"] as! NSArray
println("\(self.messagesList)")
self.messageTableView.reloadData()
}
}
}
self.messageTableView.delegate = self
self.messageTableView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func dismissMessageVC(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
println("asdasd")
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
println("asdasd")
println("\(self.messageCount)")
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
println("bdbsdbsdb")
var cell = self.messageTableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as? UITableViewCell
let row = indexPath.row
cell!.textLabel!.text = self.messagesList[0]["content"] as? String
return cell!
}
Runtime error description:
Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 0 beyond bounds for empty array'
I tried assign the cell label with using cell.textLabel?.text = "asdasd" command and it works. So I think there is no problem with outlets or methods.
How can I assign the data to messageList before cellForRowAtIndexPath executed with using different way?
Yes, if you have a table view that needs to make an asynchronous call to retrieve the data, you should expect the table view data source methods to be called before the asynchronous request is done. But, when the asynchronous request is done, when you simply call tableView.reloadData(), the table view data sources methods will be called a second time. This is a very common pattern.
The issue here, though, is that this code is not gracefully handling the situation that there is no data to display when the table view data sources methods are called the first time. If numberOfRowsForSection returned 0 until data was retrieved (as described by the others, notably by returning messagesList.count(), as suggested by John and Yedidya, rather than returning a fixed number), all would be good.
You return constant value for number of rows even if your message count is zero. Better to return message count.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
println("asdasd")
println("\(self.messageCount)")
return self.messageCount;
}
Replace the numberOfRows function return value with messagesList.count.

PFQueryTableViewController pagination doesn't work with heightForRowAtIndexPath

I am using parse.com framework with Swift and in PFQueryTableViewController when I set the pagination it won't work. If the DB has less rows than the number set in objectPerPage it works fine, but if there are more rows and when I run the app it keeps showing the loading screen and nothing is downloaded, when I do "swipe as refresh" it crash as
Error
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 5 beyond bounds [0 .. 4]
ImagesTableViewController.swift
import UIKit
import Parse
import ParseUI
import Bolts
class ImagesTableViewController: PFQueryTableViewController {
#IBAction func unwindToSegue (segue : UIStoryboardSegue) {}
// Initialise the PFQueryTable tableview
override init(style: UITableViewStyle, className: String!) {
super.init(style: style, className: className)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Configure the PFQueryTableView
self.parseClassName = "Image"
self.pullToRefreshEnabled = true
self.paginationEnabled = true
self.objectsPerPage = 5
}
// Define the query that will provide the data for the table view
override func queryForTable() -> PFQuery {
var query = PFQuery(className: "Image")
query.whereKey("deleted", notEqualTo: 1)
query.orderByDescending("createdAt")
return query
}
//override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("ImageCell") as! ImageTVCell!
if cell == nil {
cell = ImageTVCell(style: UITableViewCellStyle.Default, reuseIdentifier: "ImageCell")
}
// Extract values from the PFObject to display in the table cell HEADLINE
if let caption = object?["caption"] as? String {
cell?.headlineLabel?.text = caption
}
// Display image
var initialThumbnail = UIImage(named: "question")
cell.postImageView.image = initialThumbnail
if let thumbnail = object?["image"] as? PFFile {
cell.postImageView.file = thumbnail
cell.postImageView.loadInBackground()
}
return cell
}
// if I remove this code pagination work but the cell height is wrong
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return calculateHeightForRowAtIndexPath(indexPath)
}
func calculateHeightForRowAtIndexPath(indexPath: NSIndexPath) -> CGFloat {
if let ratio = objectAtIndexPath(indexPath)?["aspect"] as? Float {
println("Ratio: \(ratio)")
return tableView.bounds.size.width / CGFloat(ratio)
} else {
return 50.0
}
}
#IBAction func addNewPhotoButton(sender: UIBarButtonItem) {
self.tabBarController?.tabBar.hidden = true
self.performSegueWithIdentifier("showUploadNewImage", sender: self)
}
}
This problem occurs because of PFQueryTableViewController's implementation of the method tableView:numberOfRowsInSection from the UITableViewDataSource. I've copy/pasted it from the GitHub repo containing PFQueryTableViewController.m
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSInteger count = [self.objects count];
if ([self _shouldShowPaginationCell]) {
count += 1;
}
return count;
}
It simply returns the count of objects to display (which makes sense), but if pagination is enabled, then it requires for an extra cell to be shown. This means you have to manually created another cell with the text "Load more data" or something like that, which would trigger a refresh.
A way to overcome this is simply by overriding tableView:numberOfRowsInSection yourself with the following:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.objects!.count
}
UPDATE 1
The prebuilt Parse pagination button was gone in previous answer
Use the following code snippet for calculating the height of the cells to display the prebuilt Parse pagination button
func calculateHeightForRowAtIndexPath(indexPath: NSIndexPath) -> CGFloat {
// Special case for pagination, using the pre-built one by Parse
if (indexPath.row >= objects!.count) { return 50.0 }
// Determines the height if an image ratio is present
if let ratio = objectAtIndexPath(indexPath)?["aspect"] as? Float {
println("Ratio: \(ratio)")
return tableView.bounds.size.width / CGFloat(ratio)
} else {
return 50.0
}
}
Using Parse 1.11 with iOS 9.2 and Xcode 7.2 Parse Pagination works perfectly.
Problems surface when the user override some funcs used by Parse itself without properly managing the "Load More ..." row added by Parse.
In my case I needed to override tableView-canEditRowAtIndexPath to determine whether the current user can or cannot delete the row according to the object's ACL.
My initial func was:
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
if let curUser = PFUser.currentUser() {
let currentObject = objects![indexPath.row]
if let acl = currentObject.ACL {
return acl.getWriteAccessForUser(curUser)
} else {
return true
}
}
return true
}
but I got the exception of indexpath out of bounds when the Load More line was met during list scrolling.
Problem was solved adding this test:
if (indexPath.row == self.objects!.count) { // row "Load More ..."
return true
}
Without this code the "Load More ..." row was not added by Parse!!
So the complete correct overriding func is:
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
if (indexPath.row == self.objects!.count) { // row "Load More ..."
return true
}
if let curUser = PFUser.currentUser() {
let currentObject = objects![indexPath.row]
if let acl = currentObject.ACL {
return acl.getWriteAccessForUser(curUser)
} else {
return true
}
}
return true
}
Generally speaking all overridden funcs including heightForRowAtIndexpath, must take care of the extra line added by Parse when pagination is enabled.
HTH
Roberto Targa

Resources