app crash on iOS Swift insert rows at index path with animation - ios

I have a table view controller with static cells in 5 sections. I would like to add a second row to one of the sections when a UISegmentedControl object in the first row of that section is tapped (value change action function). I do not have a data source for this as I only intend to use the new row to display a text field. At this time I am only looking to see an empty row appear with animation, so that I may add the text field programmatically later on.The iOS simulator crashes whenever I tap the segmented control. No error message. Xcode 7 iOS 9. I've looked up other responses to similar queries but unable to identify the issue. Any help appreciated. My code is below. Thanks!
class MyTableViewController: UITableViewController, UITextFieldDelegate {
// init with 1 row in section 3 of table view. This row contains the segment
// control object
var rowsInMySection = 1
// index path for new row (second row) to be added to section 3
var indexPath1 = NSIndexPath(forRow: 1, inSection: 3)
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 5
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var rows = 1
if section == 3 {
rows = rowsInMySection
}
else {
rows = 1
}
return rows
}
#IBAction func segmentControlTapped(sender: UISegmentedControl) {
if(segmentControl.selectedSegmentIndex == 1)
{
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths([indexPath1],
withRowAnimation : UITableViewRowAnimation.Automatic)
self.rowsInMySection = 2
self.tableView.endUpdates()
}
}

You answered your own question. You have a static tableView so by definition it can't be added to or removed from.

Related

UITableView with Full Screen cells - pagination breaks after fetch more rows

My app uses a UITableView to implement a TikTok-style UX. Each cell is the height of the entire screen. Pagination is enabled, and this works fine for the first batch of 10 records I load. Each UITableViewCell is one "page". The user can "flip" through the pages by swiping, and initially each "page" fully flips as expected. However when I add additional rows by checking to see if the currently visible cell is the last one and then loading 10 more rows, the pagination goes haywire. Swiping results in a partially "flipped" cell -- parts of two cells are visible at the same time. I've tried various things but I'm not even sure what the problem is. The tableView seems to lose track of geometry.
Note: After the pagination goes haywire I can flip all the way back to the first cell. At that point the UITableView seems to regain composure and once again I'm able to flip correctly through all of the loaded rows, including the new ones.
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// Pause the video if the cell is ended displaying
if let cell = cell as? HomeTableViewCell {
cell.pause()
}
if let indices = tableView.indexPathsForVisibleRows {
for index in indices {
if index.row >= self.data.count - 1 {
self.viewModel!.getPosts()
break
}
}
}
}
In order to create a "Tik Tok" style UX, I ended up using the Texture framework together with a cloud video provider (mux.com). Works fine now.
I was facing the same issue and as I couldn't find a solution anywhere else here's how I solved it without using Texture:
I used the UITableViewDataSourcePrefetching protocol to fetch the new data to be inserted
extension TikTokTableView: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
viewModel.prefetchRows(at: indexPaths)
}
}
prefetchRows will execute the request if the visible cell is the last one, as in my case
func prefetchRows(at indexPaths: [IndexPath]) {
if indexPaths.contains(where: isLastCell) {
getPosts(type: typeOfPosts, offset: posts.count, lastPostId: lastPost)
}
}
private func isLastCell(for indexPath: IndexPath) -> Bool {
return indexPath.row == posts.count - 1
}
I have a weak var view delegate type TikTokTableViewDelegate in my view model to have access to a function insertItems implemented by my TikTokTableView. This function is used to inform the UITableView where to insert the incoming posts at
self.posts.append(contentsOf: response.posts)
let indexPathsToReload = self.calculateIndexPathToReload(from: response.posts)
self.view?.insertItems(at: indexPathsToReload)
private func calculateIndexPathToReload(from newPosts: [Post]) -> [IndexPath] {
let startIndex = posts.count - newPosts.count
let endIndex = startIndex + newPosts.count
print(startIndex, endIndex)
return (startIndex..<endIndex).map { IndexPath(row: $0, section: 0) }
}
and this is the insertItems function implemented in TikTokTableView and here is the key: If we try to insert those rows, the pagination of the table will fail and leave that weird offset, we have to store the indexPaths in a local property and insert them once the scroll animation has finished.
extension TikTokTableView: TikTokTableViewDelegate {
func insertItems(at indexPathsToReload: [IndexPath]) {
DispatchQueue.main.async {
// if we try to insert rows in the table, the scroll animation will be stopped and the cell will have a weird offset
// that's why we keep the indexPaths and insert them on scrollViewDidEndDecelerating(:)
self.indexPathsToReload = indexPathsToReload
}
}
}
Since UITableView is a subclass of UIScrollView, we have access to scrollViewDidEndDecelerating, this func is triggered at the end of a user's scroll and this is the time when we insert the new rows
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if !indexPathsToReload.isEmpty {
tableView.insertRows(at: indexPathsToReload, with: .none)
indexPathsToReload = []
}
}

iOS: Invalid update: invalid number of rows in section n

I am building an iOS app, basically the user create an item by pressing the "+" button and then the app should put the new item in according section of the table based on the location of the item. However I got the error: Invalid update: invalid number of rows in section 1. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update. Here is my code, thank you
let sections = ["Bathroom", "Bedroom", "Dining Room", "Garage", "Kitchen", "Living Room"]
#IBAction func addNewItem(_ sender: UIBarButtonItem) {
/*
//Make a new index path for the 0th section, last row
let lastRow = tableView.numberOfRows(inSection: 0)
let indexPath = IndexPath(row: lastRow, section: 0)
//insert this new row into the table
tableView.insertRows(at: [indexPath], with: .automatic)*/
//Create a new item and add it to the store
let newItem = itemStore.createItem()
var Num: Int = 0
if let index = itemStore.allItems.index(of: newItem) {
switch newItem.room {
case "Bathroom":
Num = 0
print("I am in here")
case "Bedroom":
Num = 1
case "Dining Room":
Num = 2
case "Garage":
Num = 3
case "Kitchen":
Num = 4
case "Living Room":
Num = 5
default:
Num = 0
print("I am in Default")
}
let indexPath = IndexPath(row: index, section: Num)
tableView.insertRows(at: [indexPath], with: .automatic)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemStore.allItems.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//create an instance of UITableViewCell, with default appearance
//let cell = UITableViewCell(style: .value, reuseIdentifier: "UITableViewCell")
//get a new or recycled cell
//if indexPath.row < itemStore.allItems.count {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
//Set the text on the cell with the decription of the item
//that is at the nth index of items, where n = row this cell
//will appear in on the table view
let item = itemStore.allItems[indexPath.row]
if item.name == "" {
cell.nameLabel.text = "Name"
} else {
cell.nameLabel.text = item.name
}
if item.serialNumber == nil {
cell.serialNumberLabel.text = "Serial Number"
} else {
cell.serialNumberLabel.text = item.serialNumber
}
cell.valueLabel.text = "$\(item.valueInDollars)"
cell.roomLabel.text = item.room
if item.valueInDollars < 50 {
cell.valueLabel.textColor = UIColor.green
}else if item.valueInDollars >= 50 {
cell.valueLabel.textColor = UIColor.red
}
cell.backgroundColor = UIColor.clear
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let LocationName = sections[section]
return LocationName
}
thank you so much for your time!
and this is how I create item
#discardableResult func createItem() -> Item {
let newItem = Item(random: false)
allItems.append(newItem)
return newItem
}
The problem is that you should insert the item in the array first:
itemStore.allItems.append(newItem)
Also, there is a difference between sections and rows in numberOfRowsInSection(return number of rows for every section) you have a switch that returns the same number, it should be
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemStore.allItems.count
}
Edit :
The problem is when the table loads there is 0 rows for all the sections ( itemStore.allItems.count is zero ), when you try to insert a row say at section 0 , row 0 -- the dataSource must be updated only for that section , which is not happen in your case as it's the same array that is returned for all sections , so you must either have an array of array where inner array represent number of rows so addition/deletion from it doesn't affect other ones ,,,,, or lock the insert to say section 0 like this
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (section == 0 ) {
return itemStore.allItems.count
}
return 0
}
in this edit i inserted in 2 sections 0 and 2 with no crash because i handled numberOfRowsInSection to return old numbers for old section that why to be able to insert in all sections you must have a different data source array or manage from numberOfRowsInSection , see edited demo here Homepwner
Instead of setting footer in viewDidLoad implement this method
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
if(section == 5 ) {
let textLabel = UILabel()
textLabel.text = "No more Item"
return textLabel
}
return nil
}
There is something terribly wrong with your data source. Your numberOfRow for all sections is the same. i.e. itemStore.allItems.count. Which means you are setting the same number of rows in each section.
The main issue is, while adding a new item, you are inserting a new row in the specific section which means
new number of rows for section = previous number of rows in section +
1
However, there is no addition in the data source.
So according to the code, you have inserted a new row, but your data source has one record less since you didn't add the item there.
So I would like you do the following in order of these steps :
Add item in required data source.
inserRowInSection
Update : Remove the condition in cellForRowAtIndexPath :
if indexPath.section <= 1 and it's else block. You don't need that. If number of sections is less than 1, it won't be called. Moreover it's the else block which is getting called all the time because you already have sections in your code. So if case will never be called.
So I got the issue. In itemStore.createItem() the value for room is always Room. In the switch-case, you never have Room case. So it always falls in default case. You need to fix that.
#Jacky.S I downloads your code and try to debug it.Fist look our error properly.
reason: 'Invalid update: invalid number of rows in section 1. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (0)
So It says before update, your sections has 0 rows. Now you try to update you tableview using this piece of code.
print(indexPath)
tableView.insertRows(at: [indexPath], with: .automatic)
I just added the print statement for debugging purpose and it prints [0, 0] .So you say your tableview to insert 0th row on section 0.
Now when you insert any row into tableview it calls the delegate method.Now look at the below delegate method.
override func tableView(_ tableView: UITableView, numberOfRowsInSection
section: Int) -> Int {
if section == 0{
print("0",itemStore.allItems.count)
}
if section == 1{
print("1",itemStore.allItems.count)
}
// print(itemStore.allItems)
return itemStore.allItems.count
}
In your code you just return itemStore.allItems.count.I added the above code for debugging.Now look that itemStore.allItems.count always return 1.So for first section it works perfectly because few times ago you insert a row in first section whose index is [0 = row, 0 = section].So you previously returned rows for section 0 is equal to the recently return rows for section 0.But When it comes for section 1 you previously return no row that means your section 1 has no rows in previous but in your above delegate method you also return 1 row for section 1.Which is conflicting because in previous you never update or return any rows for section 1.That's why you code crash and and say that
your number of rows contained in an existing section after the update is 1 which must be equal to the number of rows contained in that section before the update. Which is 0.
So this is the explanation for you crash.
Now, to recover from crash your allItems should be a dictionary or an array of array.
var allItems = [[Item]]()
or
var allItems = ["String":[Item]]()
guess allItems element is something like this
allItems = ["bathroom":[item1, item2, item3],"bedroom":[item1, item2, item3, item4]]
so here you have 2 section one is bathroom and another is bedroom.First section have 3 row and second one have 4 row and in you tableview delegate method you have to return 3 for section 0 and 4 for section 1

Expand and Contract UITableView row one at a time swift

I am working on expanding and collapsing table view cell. The current situation works well , where assume Row-0 is tapped a new row is added at index-1 and shown as expanded and when again Row-0 is tapped the expanded index-1 is collapsed. If Row-0 is in expanded state and even if I tap on another Row, the row will expand keeping both open at the same time.
Here is what I want to change, I want to contract any other row which is open when I tap on another row. i.e. One row should be open at a time. Please see the below code for ref which I tried using an array to detect which row is currently expanded, this works only if we go sequentially else it starts deleting the contracted rows. Any help will be highly appreciated.
Thank You !
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
if expandCellArray.object(at: indexPath.row) as! String == "contract"
{
for i in 0 ..< self.expandCellArray.count
{
if expandCellArray.object(at: i) as! String == "expanded"
{
self.contractCell(tableView: tableView, index: i)
self.expandCellArray.replaceObject(at: i, with: "contract")
print("Updated Expand Cell array",expandCellArray)
}
}
self.expandCell(tableView: tableView, index: indexPath.row)
self.expandCellArray.replaceObject(at: indexPath.row, with: "expanded")
print("Updated Expand Cell array",expandCellArray)
}
else
{
self.contractCell(tableView: tableView, index: self.selectedIndexPath.row)
}
}
Function to Expand Row
private func expandCell(tableView: UITableView, index: Int)
{
if (dataArray[index]?.title) != nil {
dataArray.insert(nil, at: index + 1)
myTableView.insertRows(at: [IndexPath(row: index + 1, section: 0)], with: .none)
}
}
Function to Contract row
private func contractCell(tableView: UITableView, index: Int)
{
if (dataArray[index]?.title) != nil {
dataArray.remove(at: index + 1)
myTableView.deleteRows(at: [IndexPath(row: index + 1, section: 0 )], with: .none)
}
}
Adding or removing cells is tricky because you have to handle the indexes. Alternatively, a simpler implementation of this is to use expandable sections.
Instead of adding and deleting, you just simply showing and hiding them with animation. If you want to keep only one section open, when user click on another section, simply collapse the opened section and do section reload on both sections(The one that user clicked and the one that is currently open)

Filter tableView rows doesn't reload tableView

My first iOS app works with simple custom cells, but enhancement to filter tableView rows is causing delays and frustration. Searched online for help on filter rows, read dataSource and delegate protocols in Apple Developer guides, no luck so far.
Using slider value to refresh table rows. Extracted data from line array (100 items) to linefilter array (20). Then want to refresh/reload the tableview.
Slider is declared with 0 and all line array items show up. moving the slider does not alter display. If slider is declared with say 1, then 20 filter items show.
Quite new to Apple/Xcode/Swift so have no Objective C knowledge.
Any answers will probably help me get there.
Jim L
Relevant selection of code :
#IBAction func moveSlider(sender: AnyObject) {
// Non-continuous ******
_ = false
// integer 0 to 5 ******
let slider = Int(lineSlider.value)
}
}
// Global Variable ******
var slider = 0
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
if slider == 0 {
return self.line.count
} else {
return self.linefilter.count
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! myTableViewCell
if slider == 0 {
cell.myCellLabel.text = line[indexPath.row]
} else {
cell.myCellLabel.text = linefilter[indexPath.row]
}
cell.myImageView.image = UIImage(named: img[indexPath.row])
return cell
}
tableView.reloadata()
try to put
tableView.reloadData()
like this
let slider = Int(lineSlider.value)
tableView.reloadData()
}
in your moveSlider function

SegmentedControl hide and show TableView cells

I'm working on an exercise application where the user is able to do use a UISwitch to set if a exercise is active or not. I have a segmented controller that is used to switch to show "All" or only to show "Active".
Is it possible to get the specific cells with this property within my UISegmentedControl Action?
My code looks like this:
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCellWithIdentifier("ChallengeCell") as? ChallengeListCell {
cell.setTitle("Utmaning " + String(indexPath.row + 1))
let active = indexPath.row % 3 == 0 || indexPath.row % 5 == 0
cell.setActive(active)
if active {
cell.setCompleted((Double(indexPath.row % 5) + Double(indexPath.row % 3)) / 6.0)
}
else {
cell.setCompleted(0)
}
return cell
}
return UITableViewCell()
}
and my SegmentedControl function looks like this:
#IBAction func segmentedControlChanged(sender: UISegmentedControl) {
let selectedSegment = segmentedControl!.selectedSegmentIndex
switch selectedSegment{
case 0: //SHOW ALL
print("Selected 0")
case 1: //SHOW ONLY ACTIVE
print("Selected 1")
//case 2: //NOT ACTIVE
//print("Selected 2")
default:
print(sender)
}
}
All of this is in the same Controller.
You should have three datasource arrays: for example allItems activeItems and inactiveItems, and switch between them.
Inside the tableView delegates check the segment selected with the segmentedControl and use the right array as datasource.
Inside segmentedControlChanged call self.tableView.reloadData() and set a variable with the selected segment.
One way would be to keep track of the index paths of "Active" cells in an array that's an instance property of your view controller. Than when the segmented controller is toggled, you can determine what cells to remove/add from the table view just by looking at the array.
Another idea would be to have an active property on the model objects that hold the data for each table view cell. When the segmented control is toggled, you can iterate over all of these objects to determine if they're "Active" or not and proceed accordingly.
From your code provided, it sounds like my first suggestion would be most appropriate.

Resources