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...
}
Related
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.
I have an UITableView which is populated with an array of questions. When I check a question I want to scroll to first incomplete question from my list. So if I check question 1, after I check question 10, I want to scroll me to question 2 because is incomplete.
How can I achieve this ?
Here is a link with my progress for the moment: https://github.com/tygruletz/ChecklistScrollDown
Here is a part of my code:
// Get all the values from the ChecklistCell using the delegates
extension ChecklistVC: ChecklistCellDelegate{
// Check if user pressed Pass or Fail btn for Vehicle/Trailer and scroll to next question
func tappedOnVehicleOrTrailerButtons(vehiclePassBtn: UIButton, vehicleFailBtn: UIButton, trailerPassBtn: UIButton, trailerFailBtn: UIButton, selectedCell: ChecklistCell) {
let indexPath = questionsTableView.indexPath(for: selectedCell)!
let item = itemSections[indexPath.section].checklistItems[indexPath.row]
// Only Vehicle available
if item.showVehicle && !item.showTrailer {
if vehiclePassBtn.isSelected {
item.vehiclePass = item.PASS
item.isComplete = 1
scrollDown(row: indexPath.row, section: indexPath.section)
}
else if vehicleFailBtn.isSelected {
item.vehiclePass = item.FAIL
item.isComplete = 1
scrollDown(row: indexPath.row, section: indexPath.section)
}
}
// Only Trailer available
else if item.showTrailer && !item.showVehicle{
if trailerPassBtn.isSelected {
item.trailerPass = item.PASS
item.isComplete = 1
scrollDown(row: indexPath.row, section: indexPath.section)
}
else if trailerFailBtn.isSelected {
item.trailerPass = item.FAIL
item.isComplete = 1
scrollDown(row: indexPath.row, section: indexPath.section)
}
}
// Both available: Vehicle and Trailer
else if item.showVehicle && item.showTrailer {
if vehiclePassBtn.isSelected {
item.vehiclePass = item.PASS
}
else if vehicleFailBtn.isSelected {
item.vehiclePass = item.FAIL
}
if trailerPassBtn.isSelected {
item.trailerPass = item.PASS
}
if trailerFailBtn.isSelected {
item.trailerPass = item.FAIL
}
}
if (item.vehiclePass == 1 || item.vehiclePass == 2) && (item.trailerPass == 1 || item.trailerPass == 2) {
item.isComplete = 1
scrollDown(row: indexPath.row, section: indexPath.section)
}
print("Completed Questions: \(itemSections.map{$0.checklistItems.map {$0.isComplete}})")
questionsTableView.reloadData()
}
// Function to scroll down to next row when the user complete a question
func scrollDown(row: Int, section: Int){
let lastRowFromSection = questionsTableView.numberOfRows(inSection: section) - 1
let lastSection = questionsTableView.numberOfSections - 1
let lastRowFromLastSection = questionsTableView.numberOfRows(inSection: lastSection) - 1
itemSections.forEach {
let firstIncompleteQuestion = $0.checklistItems.first(where: {$0.isComplete == 0}) // I can't manage to display only the first questions from list, at the moment I display the first incomplete questions from each section. I need somehow to find the first incomplete question from all sections and to scroll to that question.
print("\n\nFirst incomplete question from list: \(firstIncompleteQuestion?.descript)")
print("Scroll to first incomplete question from list !")
}
// I think all below rows need to be removed and to create a single condition to scroll to first incomplete question every time.
// Not last Row and Not Last Section -> scroll to next row from the same section
if row != lastRowFromSection && section != lastSection {
questionsTableView.scrollToRow(at: NSIndexPath(row: row + 1, section: section) as IndexPath, at: .top, animated: true)
}
// Last Row but Not Last Section -> scroll to first Row from next Section
if row == lastRowFromSection && section != lastSection {
questionsTableView.scrollToRow(at: NSIndexPath(row: 0, section: section + 1) as IndexPath, at: .top, animated: true)
}
// Not Last Row but Last Section -> scroll to next row from last section
if row != lastRowFromLastSection && section == lastSection {
questionsTableView.scrollToRow(at: NSIndexPath(row: row + 1, section: section) as IndexPath, at: .top, animated: true)
}
// Last Row from Last Section -> scroll to first row from first section
if row == lastRowFromLastSection && section == lastSection {
questionsTableView.scrollToRow(at: NSIndexPath(row: 0, section: 0) as IndexPath, at: .top, animated: true)
}
questionsTableView.reloadData()
}
}
Here is a capture of screen:
Thanks for trying to help me !
You could use something like this:
var indexPath:[IndexPath] = []
for section in 0..<self.tableView.numberOfSections {
for row in 0..<self.tableView.numberOfRows(inSection: section) {
guard let cell = self.tableView.cellForRow(
at: IndexPath(row: row, section: section)) as? MyCellType else {
return
}
if myCheck { // do your check here
indexPath.append(IndexPath(row: row, section: section))
}
}
}
if let first = indexPath.first {
self.tableView.scrollToRow(at: first, at: .middle, animated: true)
}
I have used this DoImagePickerController
but it did not work for me.
It selects/ unselects a particular item on an index.
See below image from DoImagePickerController
I need to select/unselect all the items from pick index to current index using the UIPanGestureRecognizer.
I need to like below image sample.
if have you any sample app/demo for this please help me.
Trace the indexpath of selection using gesture's location and loop until the selected indexpath
func makeSelection(on indexPath : IndexPath){
let cell = collectionView(collectionView, cellForItemAt: indexPath) as! GalleryPhotoCell
if selectionMode {
//gesture used for selecting photos
print("On selection")
cell.tapSelection ? nil : collectionView(collectionView, didSelectItemAt: indexPath)
}
else {
//gesture used for unselecting photos
print("On unselection")
cell.tapSelection ? collectionView(collectionView, didSelectItemAt: indexPath) : nil
}
}
func makeSelection(upto count : Int){
guard let start = prevIndex else {return}
if count > 0 {
for item in 0 ..< count {
let index = IndexPath(item: start.item + item + 1, section: start.section)
makeSelection(on: index)
}
}
else {
for item in 0 ..< abs(count){
let index = IndexPath(item: start.item - item - 1, section: start.section)
makeSelection(on: index)
}
}
}
How can I check to see whether an indexPath is valid or not?
I want to scroll to an indexPath, but I sometimes get an error if my UICollectionView subviews aren't finished loading.
You could check
- numberOfSections
- numberOfItemsInSection:
of your UICollectionViewDataSource to see if your indexPath is a valid one.
E.g.
extension UICollectionView {
func isValid(indexPath: IndexPath) -> Bool {
guard indexPath.section < numberOfSections,
indexPath.row < numberOfItems(inSection: indexPath.section)
else { return false }
return true
}
}
A more concise solution?
func indexPathIsValid(indexPath: NSIndexPath) -> Bool {
if indexPath.section >= numberOfSectionsInCollectionView(collectionView) {
return false
}
if indexPath.row >= collectionView.numberOfItemsInSection(indexPath.section) {
return false
}
return true
}
or more compact, but less readable...
func indexPathIsValid(indexPath: NSIndexPath) -> Bool {
return indexPath.section < numberOfSectionsInCollectionView(collectionView) && indexPath.row < collectionView.numberOfItemsInSection(indexPath.section)
}
#ABakerSmith's answer is close, but not quite right.
The answer depends on your model.
If you have a multi-section collection view (or table view for that matter - same issue) then it's pretty common to use an array of arrays to save your data.
The outer array contains your sections, and each inner array contains the rows for that section.
So you might have something like this:
struct TableViewData
{
//Dummy structure, replaced with whatever you might use instead
var heading: String
var subHead: String
var value: Int
}
typealias RowArray: [TableViewData]
typeAlias SectionArray: [RowArray]
var myTableViewData: SectionArray
In that case, when presented with an indexPath, you'd need to interrogate your model object (myTableViewData, in the above example)
The code might look like this:
func indexPathIsValid(theIndexPath: NSIndexPath) -> Bool
{
let section = theIndexPath.section!
let row = theIndexPath.row!
if section > myTableViewData.count-1
{
return false
}
let aRow = myTableViewData[section]
return aRow.count < row
}
EDIT:
#ABakerSmith has an interesting twist: Asking the data source. That way you can write a solution that works regardless of the data model. His code is close, but still not quite right. It should really be this:
func indexPathIsValid(indexPath: NSIndexPath) -> Bool
{
let section = indexPath.section!
let row = indexPath.row!
let lastSectionIndex =
numberOfSectionsInCollectionView(collectionView) - 1
//Make sure the specified section exists
if section > lastSectionIndex
{
return false
}
let rowCount = self.collectionView(
collectionView, numberOfItemsInSection: indexPath.section) - 1
return row <= rowCount
}
Using swift extension:
extension UICollectionView {
func validate(indexPath: IndexPath) -> Bool {
if indexPath.section >= numberOfSections {
return false
}
if indexPath.row >= numberOfItems(inSection: indexPath.section) {
return false
}
return true
}
}
// Usage
let indexPath = IndexPath(item: 10, section: 0)
if sampleCollectionView.validate(indexPath: indexPath) {
sampleCollectionView.scrollToItem(at: indexPath, at: UICollectionViewScrollPosition.centeredHorizontally, animated: true)
}
Here's a Swift 4 snippet I wrote and have been using for a while.
It lets you either scroll to an IndexPath only if it's available, or - throw an error if the IndexPath is not available, to let you control what you want to do in this situation.
Check out the code here:
https://gist.github.com/freak4pc/0f244f41a5379f001571809197e72b90
It lets you do either:
myCollectionView.scrollToItemIfAvailable(at: indexPath, at: .top, animated: true)
Or
myCollectionView.scrollToItemOrThrow(at: indexPath, at: .top, animated: true)
The latter would throw something like:
expression unexpectedly raised an error: IndexPath [0, 2000] is not available. The last available IndexPath is [0, 36]
Objective C version:
- (BOOL)indexPathIsValid:(NSIndexPath *)indexPath
{
return indexPath.section < [self.collectionView numberOfSections] && indexPath.row < [self.collectionView numberOfItemsInSection:indexPath.section];
}
If you are trying to set the state of a cell in the collection view without knowing whether the index path is valid or not, you could try saving the indices for cells with a special state, and set the state of the cells while loading them.
You should check the validation of the index paths that will be appended with the data source(future state) and the deletion of index paths with the current existing ones(present state).
extension UITableView {
func isValid(indexPath: IndexPath, inDataSource: Bool = false) -> Bool {
guard
let numberOfSections = inDataSource
? dataSource?.numberOfSections?(in: self)
: numberOfSections,
let numberOfRows = inDataSource
? dataSource?.tableView(self, numberOfRowsInSection: indexPath.section)
: numberOfRows(inSection: indexPath.section)
else {
preconditionFailure("There must be a datasource to validate an index path")
}
return indexPath.section < numberOfSections && indexPath.row < numberOfRows
}
usage:
// insert
tableView.insertRows(at: indexPaths.filter({ tableView.isValid(indexPath: $0, inDataSource: true) }), with: .top)
// remove
tableView.deleteRows(at: indexPaths.filter({ tableView.isValid(indexPath: $0) }), with: .top)
output:
true or false
Perhaps this is what you're looking for?
- (UICollectionViewCell *)cellForItemAtIndexPath:(NSIndexPath *)indexPath
Return Value:
The cell object at the corresponding index path or nil if the cell is not visible or indexPath is out of range.
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)
}