Set dynamic Custom Cell from certain row - ios

I'm making an social function in which people can comment on pictures.
The only dynamic cell is also the comment cell. I'm using Parse for this.
How can I get different comments on each comment cell?
I tried accessing indexPath.row, but it gives me a error: '*** -[__NSArrayM objectAtIndex:]: index 2 beyond bounds [0 .. 1]'
Now playing with a custom NSIndexPath but I only managed to acces the forRow method manually. Result is that the comments are all the same.
Row 0 & 1 are working.
Using Storyboard.
userComments is the Mutable Array in which the comments are being loaded.
The println(comments) gives me 2 objects which are the same.
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return userComments.count + 2
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let Postcell:PostTableViewCell = tableView.dequeueReusableCellWithIdentifier("imageCell") as PostTableViewCell
....
return Postcell
}
if indexPath.row == 1 {
let likeCell:likedTableViewCell = tableView.dequeueReusableCellWithIdentifier("likeCell") as likedTableViewCell
....
return likeCell
}else {
let commentCell:commentTableViewCell = tableView.dequeueReusableCellWithIdentifier("commentCell") as commentTableViewCell
let commentIndex:NSIndexPath = NSIndexPath(forRow: 0, inSection: 0)
let comment:PFObject = userComments.objectAtIndex(commentIndex.row) as PFObject
println(comment)
// Comment Label
commentCell.commentLabel.text = comment.objectForKey("content") as String!
commentCell.userImageView.image = UIImage(named: "dummy")
return commentCell
}
}

That is happening because in your last bit you are always asking for row 0 section 0 in parse, you will need something like this:
else {
let commentCell:commentTableViewCell = tableView.dequeueReusableCellWithIdentifier("commentCell") as commentTableViewCell
//indexPath.row is the actual row of the table,
//so you will have for table row 2 parse row 0, for 3 row 1 and so on
let commentIndex:NSIndexPath = NSIndexPath(forRow: indexPath.row-2, inSection: 0)
let comment:PFObject = userComments.objectAtIndex(commentIndex.row) as PFObject
println(comment)
// Comment Label
commentCell.commentLabel.text = comment.objectForKey("content") as String!
commentCell.userImageView.image = UIImage(named: "dummy")
return commentCell
}

Related

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.

Programmatically altering NSIndexPath in Swift

I'm implementing AdMob in a UITableView by putting banner ad in the first row of a section. I'm most of the way there implementing it, however I'm having a tough time getting cellForRowAtIndexPath to work as desired.
This is what my numberOfRowsInSection looks like:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var count = Int()
if let sections = fetchedResultsController.sections {
let currentSection = sections[section]
count = currentSection.numberOfObjects
count = count + 1 // add another row for an ad
}
return count
}
My cellForRowAtIndexPath looks like this:
override func tableView(tableView: UITableView, var cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let adCell: BannerAdTableViewCell = tableView.dequeueReusableCellWithIdentifier(BannerAdTableViewCell.reuseIdentifier(), forIndexPath: indexPath) as! BannerAdTableViewCell
// customization
return adCell
} else {
// Cell for vanilla item to display
// TODO: fix indexpath here. need to add 1
let newIndexPath = indexPath.indexPathByAddingIndex(indexPath.row+1)
indexPath = newIndexPath
// Cell for a Routine
let customCell = tableView.dequeueReusableCellWithIdentifier(RoutineSelectionTableViewCell.reuseIdentifier(), forIndexPath: indexPath) as! RoutineSelectionTableViewCell
let routine = fetchedResultsController.objectAtIndexPath(indexPath) as! SavedRoutines
customCell.routineNameLabel.text = routine.routineTitle
return customCell
}
}
I know I need to adjust the value of the indexPath to account for an extra row in the indexPathSection, but everything I've tried triggers out of bounds exceptions of some sort. Any feedback would be greatly appreciated.
indexPathByAddingIndex adds a new index, it does not increment a value of an index but adds one. If you previously had two indices / dimensions (section and row) you now have 3 indices / dimension: section, row and "the newly added one".
Provides an index path containing the indexes in the receiving index path and another index.
What you should do instead is either create a new NSIndexPath by hand. And I do not think you need to add one, but subtract one, since the item at index 1 should actually be the element in your result at index 0:
let customIndexPath = NSIndexPath(forRow: indexPath.row - 1, inSection: indexPath.section)
which you then use to access the "correct" routine at the right index:
let routine = fetchedResultsController.objectAtIndexPath(customIndexPath) as! SavedRoutines
Your call to tableView.dequeueReusableCellWithIdentifier should stay the same and still pass in the default indexPath.

Number of rows in section error Swift

I have a tableview with two custom-cell xibs.
The first xib only contains uilabel and the second one only uibutton.
Once the uibutton is clicked the someTagsArray (array which i use for count in numberOfRows function) is appended and new rows should be inserted, but instead i'm getting this nasty error
Invalid update: invalid number of rows in section 0. The number of
rows contained in an existing section after the update (8) must be
equal to the number of rows contained in that section before the
update (4), 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).
Here is my code (numberOfRowsInSection)
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) ->
Int {
return someTagsArray.count + 1
}
cellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.row < someTagsArray.count){
var cell:TblCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! TblCell
cell.lblCarName.text = linesmain["start"]![indexPath.row]
return cell
} else {
var celle:vwAnswers = self.tableView.dequeueReusableCellWithIdentifier("cell2") as! vwAnswers
celle.Answer1.setTitle(answersmain["start"]![0], forState:UIControlState.Normal)
// answertitle is a global string variable
answertitle1 = "\(celle.Answer1.currentTitle!)"
return celle
}}
and finally the function code which crashes the app
func insertData(){
// appending the array to increase count
someTagsArray += linesmain[answertitle1]!
tableView.beginUpdates()
let insertedIndexPathRange = 0..<self.linesmain[answertitle2]!.count-4
var insertedIndexPaths = insertedIndexPathRange.map { NSIndexPath(forRow: $0, inSection: 0) }
tableView.insertRowsAtIndexPaths(insertedIndexPaths, withRowAnimation: .Fade)
tableView.endUpdates()
}
Thank you guys.
Try this code for inserting rows:
func insertData(){
let initialCount = someTagsArray.count as Int
let newObjects = linesmain[answertitle1] as! NSArray
// appending the array to increase count
//someTagsArray += newObjects
someTagsArray.addObjectsFromArray(newObjects)
self.tableView!.beginUpdates()
var insertedIndexPaths: NSMutableArray = []
for var i = 0; i < newObjects.count; ++i {
insertedIndexPaths.addObject(NSIndexPath(forRow: initialCount+i, inSection: 0))
}
self.tableView?.insertRowsAtIndexPaths(insertedIndexPaths as [AnyObject], withRowAnimation: .Fade)
self.tableView!.endUpdates()
}

Invalid Update When adding Rows to UITableView

Invalid update: invalid number of rows in section 0. The number of
rows contained in an existing section after the update (5) must be
equal to the number of rows contained in that section before the
update (1), plus or minus the number of rows inserted or deleted from
that section (1 inserted, 0 deleted) and plus or minus the number of
rows moved into or out of that section (0 moved in, 0 moved out).
I'm trying to add rows to a table view when a user taps a row, to create an expandable section, however the extra rows aren't being counted before Xcode tries to add them in and as such causes this error (I think). Can anybody point me in the right direction?
// sectionExpanded is set to false in viewDidLoad. It is set to true when
// the user taps on the expandable section (section 0 in this case)
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 && sectionExpanded {
return 5
} else {
return 1
}
}
// This should recount the rows, add the new ones to a temporary array and then add
// them to the table causing the section to 'expand'.
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedItem = menu[indexPath.row]
let cell = tableView.cellForRowAtIndexPath(indexPath) as MenuCell
if indexPath.section == 0 {
var rows: Int
var tmpArray: NSMutableArray = NSMutableArray()
sectionExpanded = !sectionExpanded
rows = tableView.numberOfRowsInSection(0)
for i in 1...rows {
var tmpIndexPath: NSIndexPath
tmpIndexPath = NSIndexPath(forRow: i, inSection: 0)
tmpArray.addObject(tmpIndexPath)
}
if !sectionExpanded {
tableView.deleteRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
} else {
tableView.insertRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
}
} else {
delegate?.rightItemSelected(selectedItem)
}
}
It is telling you that you are trying to insert 1 new row, but numberofrows should be 5, before was 1 and you are trying to insert 1 new row, thats 2. Theres your problem.
rows = tableView.numberOfRowsInSection(0) //this returns 1
for i in 1...rows { //
var tmpIndexPath: NSIndexPath
tmpIndexPath = NSIndexPath(forRow: i, inSection: 0)
tmpArray.addObject(tmpIndexPath)//this will contain only 1 object, because the loop will run only for 1 cycle
}
EDIT
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedItem = menu[indexPath.row]
let cell = tableView.cellForRowAtIndexPath(indexPath) as MenuCell
if indexPath.section == 0 {
var rows: Int
var tmpArray: NSMutableArray = NSMutableArray()
sectionExpanded = !sectionExpanded
rows = 1
if sectionExpanded {
rows = 5
}
for i in 1...rows {
var tmpIndexPath: NSIndexPath
tmpIndexPath = NSIndexPath(forRow: i, inSection: 0)
tmpArray.addObject(tmpIndexPath)
}
if !sectionExpanded {
tableView.deleteRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
} else {
tableView.insertRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
}
} else {
delegate?.rightItemSelected(selectedItem)
}
}
Since you know number of rows will be always 5 or 1, you can try something like this. However, this is not a standard approach, I would suggest to alter your datasource array.
Here is some example how to do it: http://www.nsprogrammer.com/2013/07/updating-uitableview-with-dynamic-data.html its for Objective-C but you will get the gist of it.
You can try modifying the data source and then reload the table.
You should use insertRowsAtIndexPaths... and the like between a beginUpdates() and endUpdates(). The tableView will collect all the changes after beginUpdates() and then will apply them coherently after endUpdates(). So try something like:
tableView.beginUpdates()
if !sectionExpanded {
tableView.deleteRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
} else {
tableView.insertRowsAtIndexPaths(tmpArray, withRowAnimation: UITableViewRowAnimation.Top)
}
tableView.endUpdates()
Remember that after the call to endUpdates() the number of sections and rows must be consistent with your model.
Since I don't know about your model, here's a simple example:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var sectionExpanded: Bool = false {
didSet {
if oldValue != sectionExpanded {
let expIndexes = map(0..<model.count) { r in
NSIndexPath(forRow: r, inSection: 0)
}
// Here we start the updates
tableView.beginUpdates()
switch sectionExpanded {
case false:
// Collapsing
tableView.deleteRowsAtIndexPaths(expIndexes, withRowAnimation: .Top)
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Top)
default:
// Expanding
tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Top)
tableView.insertRowsAtIndexPaths(expIndexes, withRowAnimation: .Bottom)
}
// Updates ended
tableView.endUpdates()
}
}
}
let model = ["foo", "bar", "zoo"]
//MARK: UITableView DataSource
struct TableConstants {
static let sectionCellIdentifier = "SectionCell"
static let expandedCellIdentifier = "ExpandedCell"
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sectionExpanded ? model.count : 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
switch sectionExpanded {
case false:
let cell = tableView.dequeueReusableCellWithIdentifier(
TableConstants.sectionCellIdentifier,
forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = "The Section Collapsed Cell"
return cell
default:
let cell = tableView.dequeueReusableCellWithIdentifier(
TableConstants.expandedCellIdentifier,
forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = "\(model[indexPath.row])"
cell.detailTextLabel?.text = "Index: \(indexPath.row)"
return cell
}
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
sectionExpanded = !sectionExpanded
}
}
Note that I moved the table updates to the sectionExpanded observer.
You already have 1 row in section = 0, and trying to insert 5 new rows. You can only add 4 rows more to map with numberOfRowsInsection.
Try following code:
sectionExpanded = !sectionExpanded
rows = self.numberOfRowsInSection(0)-1
for i in 1...rows {
var tmpIndexPath: NSIndexPath
tmpIndexPath = NSIndexPath(forRow: i, inSection: 0)
tmpArray.addObject(tmpIndexPath)
}

Get indexPath of "next" UITableViewCell

I have some code when tapping on a cell of a table view. Under certain circumstances I want to call the function tableView(_, didSelectRowAtIndexPath) recursively for the next cell. That means that when I selected row 5, I want to select row 6, etc.
How can I get the indexPath of the next cell based on another row?
Here's an answer in Swift:
private func nextIndexPath(for currentIndexPath: IndexPath, in tableView: UITableView) -> IndexPath? {
var nextRow = 0
var nextSection = 0
var iteration = 0
var startRow = currentIndexPath.row
for section in currentIndexPath.section ..< tableView.numberOfSections {
nextSection = section
for row in startRow ..< tableView.numberOfRows(inSection: section) {
nextRow = row
iteration += 1
if iteration == 2 {
let nextIndexPath = IndexPath(row: nextRow, section: nextSection)
return nextIndexPath
}
}
startRow = 0
}
return nil
}
I use this code because I have a tableview with custom cells that contain a UITextField. It's configured with a Next button, and when that button is tapped, the focus is moved to the next UITextField.
To go to the previous indexPath, see this answer: https://stackoverflow.com/a/56867271/
For an example project that includes a previous/next button as a toolbar above a keyboard, check out the example project:
https://github.com/bvankuik/TableViewWithTextFieldNextButton
For previous indexPath I have made the following extension on UITableView
( Swift 5.0 )
extension UITableView {
func previousIndexPath(currentIndexPath: IndexPath) -> IndexPath? {
let startRow = currentIndexPath.row
let startSection = currentIndexPath.section
var previousRow = startRow
var previousSection = startSection
if startRow == 0 && startSection == 0 {
return nil
} else if startRow == 0 {
previousSection -= 1
previousRow = numberOfRows(inSection: previousSection) - 1
} else {
previousRow -= 1
}
return IndexPath(row: previousRow, section: previousSection)
}
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let nextIndexPath=NSIndexPath(forRow: indexPath.row + 1, inSection: indexPath.section);
// You should be sure than this NSIndexPath exist, and ...make what you want
}
this will work in swift 4
for previous and next
let nextIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section)
let previousIndexPath = IndexPath(row: indexPath.row - 1, section: indexPath.section)
I wrote an IndexPath extension method, I found its logic is a bit easier to understand than #Bart van Kuik's solution.
Written in Swift 5, Xcode 11, works for multi-section UITableView.
import UIKit
extension IndexPath {
// Helper Methods
func incrementRow(plus: Int=1) -> IndexPath {
return IndexPath(row: row + plus, section: section)
}
func incrementSection(plus: Int=1) -> IndexPath {
return IndexPath(row: 0, section: section + plus)
}
func next(in table: UITableView) -> IndexPath? {
// if can find cell for next row, return next row's IndexPath
if let _ = table.cellForRow(at: incrementRow()) {
return incrementRow()
}
// cannot find next row, try to find row 0 in next section
else if let _ = table.cellForRow(at: incrementSection()) {
return incrementSection()
}
// can find neither next row nor next section, the current indexPath is already the very last IndexPath in the given table
return nil
}
}
As for the previous IndexPath, #Bishal Ghimire's answer is valid, but here's the IndexPath version extension.
func previous(in table: UITableView) -> IndexPath? {
// if the current indexPath is the very first IndexPath, then there's no previous
if row == 0 && section == 0 { return nil }
// if the current indexPath is the first row in a section, return table's previous section's last row's IndexPath
if row == 0 {
let lastRowInPrevSection = table.numberOfRows(inSection: section - 1) - 1
return IndexPath(row: lastRowInPrevSection, section: section - 1)
}
// else just return previous row's IndexPath in the same section
else {
return IndexPath(row: row - 1, section: section)
}
}
You can drag & drop these method into any of your project and use them directly, in my case, I'm trying to highlight next cell's textField when the user hit return key, so the usage is like this:
...
if let nextIndexPath = currentIndexPath.next(in: myTableView),
let nextCell = myTableView.cellForRow(at: nextIndexPath) as? MyCell {
nextCell.textField.becomeFirstResponder()
} else {
// there's no next IndexPath in the given table, simply resign first responder for the current cell's textField
currentCell.textField.resignFirstResponder()
}
...
Currently, it seems to me that only(?) Bart van Kuiks answer currently considers the possibility, that a section could consists of none rows.
The other posters might correct their answers. Meanwhile I post my code for next and previous cells as UITableView-Extensions. Feel free to edit the code, if you find any mistakes.
extension UITableView {
func indexPathOfCell(after indexPath: IndexPath) -> IndexPath? {
var row = indexPath.row + 1
for section in indexPath.section..<numberOfSections {
if row < numberOfRows(inSection: section) {
return IndexPath(row: row, section: section)
}
row = 0
}
return nil
}
func indexPathOfCell(before indexPath: IndexPath) -> IndexPath? {
var row = indexPath.row - 1
for section in (0...indexPath.section).reversed() {
if row >= 0 {
return IndexPath(row: row, section: section)
}
if section > 0 {
row = numberOfRows(inSection: section - 1) - 1
}
}
return nil
}
}
For those who liked #Bishal Ghimire's previousIndexPath() method, here is what the nextIndexPath() method would be.
import UIKit
extension UITableView {
func nextIndexPath(currentIndexPath: IndexPath) -> IndexPath? {
let startRow = currentIndexPath.row
let startSection = currentIndexPath.section
var nextRow = startRow
var nextSection = startSection
if startSection == numberOfSections-1 && startRow == numberOfRows(inSection: startSection)-1 {
return nil
} else if startRow == numberOfRows(inSection: startSection)-1 {
nextSection += 1
nextRow = 0
} else {
nextRow += 1
}
return IndexPath(row: nextRow, section: nextSection)
}
}
You can get the IndexOFObeject
NSUInteger indexOfTheObject = [Array indexOfObject:indexPath];
and for Cell tap:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *temp = [Array objectAtIndex:indexPath.row+1];
temp...
}

Resources