UITableView jumpy on scroll after changing cell height - ios

So I have a tableView where I want to change the height of a cell when tapped. Well, actually, I am replacing it with a bigger cell.
On tap, I call:
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
tableView.endUpdate()
And then I modify my cellForRowAtIndexPath to return the right new cell and height. The cell's height is being automatically calculated by overriding sizeThatFits in the cell's implementation:
override func sizeThatFits(size: CGSize) -> CGSize {
return CGSizeMake(size.width, myHeight)
}
Oddly enough, after I do this, scrolling downwards is fine, but when I scroll upwards, the table jumps 5 or so pixels every second until I reach the top. After I reach the top of the table, the problem is gone and there is no jumping going in either direction. Any idea why this is happening? I imagine it has something to do with the new cell height displacing the other cells, but I can't see why the tableView is not taking care of this. Any help is appreciated!
Thanks!
EDIT: Added code from cellForRowAtIndexPath:
if self.openedCellIndex != nil && self.openedCellIndex == indexPath {
cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as ListCell
(cell as ListCell).updateWithDetailView(dayViewController!.view)
} else {
cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as ListCell
(cell as ListCell).updateWithData(eventDay: store.events![indexPath.row], reminderDay: store.reminders![indexPath.row])
}
return cell

You should include your code for cellForRow and heightForRow, but I will give it a blind shot.
When a cell is tapped in cellForRow you should store the index of that cell, then reload the data or just that cell. Then in heightForRow use if(yourTappedCell){return preferredHeight;}

Unfortunately, there does not seem to be a simple answer to this. I have struggled with it on multiple iOS apps.
The only solution I have found is to programmatically scroll to the top of your UITableView once it appears again.
[self.tableView setContentOffset:CGPointMake(0, 0 - self.tableView.contentInset.top) animated:YES];
OR
self.tableView.contentOffset = CGPointMake(0, 0 - self.tableView.contentInset.top);
Hope this an acceptable work around while still being able to use dynamic cell heights =)

func refreshCellState(indexPath:IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ListCell else {
return
}
if self.openedCellIndex != nil && self.openedCellIndex == indexPath {
cell.updateWithDetailView(dayViewController!.view)
} else {
cell.updateWithData(eventDay: store.events![indexPath.row], reminderDay: store.reminders![indexPath.row])
}
self.tableView.beginUpdates()
self.tableView.endUpdates()
if #available(iOS 11.0, *) {
self.tableView.performBatchUpdates {
} completion: { complete in
if complete {
self.tableView.scrollToRow(at: indexPath, at: .none, animated: true)
}
}
} else {
// Fallback on earlier versions
self.tableView.reloadData()
}
}
Here, Dont reload that cell entirely, But take that cell when clicked and provide data to cell from there.
Now call begin and endUpdated methods of tableview to update height.

Related

UICollectionView set default position

I have a CollectionView with 3 Cells which extend across the whole device's screen and basically represent my 3 main views.
I have paging enabled, so it basically works exactly like the iOS home screen right now.
My problem is that I want the "default" position of this CollectionView to be equal to view.frame.width so that the second Cell is the "default" view and I can swipe left and right to get to my secondary views.
I have already tried via
collectionView.scrollToItem()
and
collectionView.scrollRectToVisible()
as well as
collectionView.setContentOffset()
but they all seem to work only after the view has loaded (I tried them via a button in my navigation bar).
Thank you in advance!
EDIT:
Now this works, but I also have another collection view in that one middle cell which holds a list of little 2-paged UICollectionViews which are actually objects from a subclass of UICollectionViewCell called PersonCell each holding a UICollectionView. I want these UICollectionViews to be scrolled to index 1 as well, this is my code:
for tabcell in (collectionView?.visibleCells)! {
if let maincell: MainCell = tabcell as? MainCell {
for cell in maincell.collectionView.visibleCells {
if let c = cell as? PersonCell {
c.collectionView.scrollToItem(at: (NSIndexPath(item: 1, section: 0) as IndexPath), at: [], animated: false)
}
}
}
}
This is executed in the viewDidLayoutSubviews of my 'root' CollectionViewController.
EDIT 2:
Now I tried using following code in the MainCell class (it's a UICollectionViewCell subclass):
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let personCell = cell as! PersonCell
personCell.collectionView.scrollToItem(at: (NSIndexPath(item: 1, section: 0) as IndexPath), at: [], animated: false)
}
EDIT 3:
Long story short, I basically need a delegate method that is called after a cell has been added to the UICollectionView.
Did you try [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
Try calling the code in viewDidLayoutSubviews
Okay, I got it!
This SO answer gave me the final hint:
https://stackoverflow.com/a/16071589/3338129
Basically, now I just do this after initialization of the Cell:
self.collectionView.alpha = 1
self.data = data // array is filled with data
self.collectionView.reloadData()
DispatchQueue.main.async {
for cell in self.collectionView.visibleCells {
(cell as! PersonCell).collectionView.scrollToItem(at: (NSIndexPath(item: 1, section: 0) as IndexPath), at: [], animated: false)
}
self.collectionView.alpha = 1
}
Basically, the code in the DispatchQueue.main.async{...} is called on the next UI refresh cycle which means at a point where the cells are already there. To prevent the user from seeing the wrong page for a fraction of a second, I set the whole collectionView's alpha to 0 and toggle it on as soon as the data is there, the cell is there and the page has scrolled.

ios swift: UITableViewCell's tag is not updated after consecutive delete of cells

I have implemented the delete functionality for any row based on the index passed.
Each cell has a button to initiate delete for that row. I take the cell.tag to detect the row and pass to delete function which uses indexPath and deleteRowAtIndexPaths(...).
Now, the problem happens when I keep on deleting the 0th row. Initially, it deletes correctly. 0th row is gone. 1st row replaces the 0th row.
Now, if I delete 0th row again, it deletes the current 1st row.
The reason I understood is that cell.tag is not updated.
What exactly an I doing wrong ?
The problem is not consistent. If I wait between the deletes, it is ok. If I delete one row after another. It keeps on deleting some other row.
How should I proceed now ? I have searched for this already and unable to find proper solution or guide ?
Here are the main pieces of code
// Typical code having Programmatic UITableView
// ...
func addTestEvent(cell: MyCell) {
func onSomeAction() {
dispatch_async(dispatch_get_main_queue(), {
self.removeRow(cell.tag)
})
}
...
// onSomeAction() called on click on the button
}
func test(cell: MyCell) -> () {
...
addTestEvent(cell)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier( NSStringFromClass(MyCell), forIndexPath: indexPath) as! MyCell
cell.tag = indexPath.row
cell.test = { (cell) in self.test(cell) }
return cell
}
func removeRow(row: Int) {
let indexPath = NSIndexPath(forItem: row, inSection: 0)
tableView.beginUpdates()
posts.removeAtIndex(row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
tableView.endUpdates()
}
The key point is not to use cell.tag to identify the cell. Rather use the cell directly. Thanks Vadian for the comment. It is not a good practice to keep indexPath in cell tag. Now I know why !
This answer gave me the major hint to resolve the problem.
https://stackoverflow.com/a/29920564/2369867
// Modified pieces of code. Rest of the code remain the same.
func addTestEvent(cell: MyCell) {
func onSomeAction() {
dispatch_async(dispatch_get_main_queue(), {
self.removeRow(cell)
})
}
// ...
// onSomeAction() called on click on the button
}
func removeRow(cell: UITableViewCell) {
let indexPath = tableView.indexPathForRowAtPoint(cell.center)!
let rowIndex = indexPath.row
// ...
}
Add tableView.reloadData() after deleting a cell. That worked for me.

UITableView: highlight the last cell but other cells get highlighted as well

I have UITableView that I use as a sliding menu as part of SWRevealViewController.
I want to select the last cell in UITableView and implement the following:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let customCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! IGAWindAloftMenuTableViewCell
...
let sectionsAmount = tableView.numberOfSections
let rowsAmount = tableView.numberOfRowsInSection(indexPath.section)
if (indexPath.section == sectionsAmount - 1 && indexPath.row == rowsAmount - 1)
{
customCell.backgroundColor = UIColor.yellowColor()
}
return customCell
}
When I scroll all the way down, it works -- the last cell is highlighted. However, when I scroll up and down, other cells in the middle of the table get highlighted as well.
Is there any way to prevent it?
Thank you!
You have to undo the change made in the if-branch for all other cells:
if (indexPath.section == sectionsAmount - 1 && indexPath.row == rowsAmount - 1) {
customCell.backgroundColor = UIColor.yellowColor()
} else {
customCell.backgroundColor = UIColor.whiteColor() // or whatever color
}
The reason for the undesired side effect is the reusing of cells. A cell gets created, then it gets used as the last cell, then it moves off-screen and is reused somewhere else. It still contains the changed color information but is no longer at the corresponding position.

Adjust SubView constraints on a UITableView Cell

I have an UITableView with 2 section. The first section (VoucherCell) will be filled with data from a database. The second section (KodeVoucherCell) will be filled with data inputted by the user using a simple form. I create that simple form as a SubView (see picture below).
Then I set the second section to load the SubView using the code below :
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = UITableViewCell.init()
if(indexPath == NSIndexPath(forRow: 0, inSection: 1)) {
cell = tableView.dequeueReusableCellWithIdentifier("KodeVoucherCell")!
cell.addSubview(inputKodeVoucherSubView)
} else {
cell = tableView.dequeueReusableCellWithIdentifier("VoucherCell")!
cell.indentationLevel = indexPath.length - 1
cell.accessoryType = .DisclosureIndicator
cell.textLabel!.text = "Voucher A"
}
return cell
}
Now the problem is, although I have set the constraints of the SubView, while I run the app, it looks like this :
How to fix this? I tried to modify the constraint but still get the same look. Thanks.
You shouldn't be adding subviews directly to a UITableViewCell. Add them to the cell's contentView. Replace:
cell.addSubview(inputKodeVoucherSubView)
with:
cell.contentView.addSubview(inputKodeVoucherSubView)

UIWebView's entire contents embedded in UITableView

I've been struggling with this for two days, so I come hat in hand to the wise people of the internet.
I am showing an article as part of my UITableView. For this to display correctly, I need to give the delegate a height for the cell, which I want to be the same as the UIWebView's height, so I can disable scroll on the WebView and display the web content in its entirety as a static cell.
My first approach was to render it in the heightForRowAtIndexpathmethod, but this did obviously not work as I need the wait for the UIWebViewDelegate to tell me when the web view is fully loaded and has a height. After a while I found a working solution, which used the web view delegate to refresh the cell height when the web view was loaded.
The works fine until the screen size changes. Either from rotate or from full-screening my UISplitView. I forced an update on it in the didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation), but this causes it to flash about 10 times before settling into the correct height. I logged this change, and it seems the WebView is calling itself multiple times, causing a loop.
As seen in this log, starting from when I rotated the screen.
It flashes once every time it reloads, and as you can see, it reloads itself a bunch of times.
So. I need a way to show an entire web views content inside a uitableview, and reliably get the height when the screen size changes. If anyone has managed this in any way before, please tell me. I will give a bounty and my firstborn child to anyone who can resolve this, as it's driving me insane.
Here's my relevant code.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch indexPath.row {
case 4:
//Content
print("Height for row called")
return CGFloat(webViewHeight)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
switch (indexPath.row){
//HTML Content View
case 4:
let cell = tableView.dequeueReusableCellWithIdentifier("ContentCell", forIndexPath: indexPath)
var contentCell = cell as? ContentCell
if contentCell == nil {
contentCell = ContentCell()
}
contentCell?.contentWebView.delegate = self
contentCell?.contentWebView.scrollView.userInteractionEnabled = false
contentCell?.contentWebView.loadHTMLString((post?.contentHTML)!, baseURL: nil)
print("Cell For row at indexpath called")
return contentCell!
}
func webViewDidFinishLoad(webView: UIWebView) {
updateHeight()
}
func updateHeight(){
let webView = (self.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 4, inSection: 0)) as! ContentCell).contentWebView
if self.webViewHeight != Double(webView.scrollView.contentSize.height) {
print("Previous WV Height = \(self.webViewHeight), New WV Height = \(webView.scrollView.contentSize.height)")
self.webViewHeight = Double(webView.scrollView.contentSize.height)
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0)], withRowAnimation: .Automatic)
} else {
return
}
}
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
print("rotated")
self.updateHeight()
//tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0)], withRowAnimation: .Automatic)
}
I solved this by changing the .Automatic to .None in the row change animation. Its still a bad solution, but at least it doesn't flicker anymore.
I would recommend that you calculate the web view height independently of the table view and store the dimension as part of the data itself and use it return in heightForRowAtIndexPath call. Its a easier that way since you don't have to deal with calculating the table height during table view display. When the html content is not loaded use a standard height and a message for the web view.
I don't see a problem in your implementation. Trey few things
There are few things you can check
func webViewDidFinishLoad(webView: UIWebView) {
updateHeight()
//This function may get called multiple times depending on webpage.
}
//Use
self.tableView.beginUpdates()
self.tableView.endUpdates()
//Instead of
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0)], withRowAnimation: .None)
func updateHeight(){
let webView = (self.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 4, inSection: 0)) as! ContentCell).contentWebView
if self.webViewHeight != Double(webView.scrollView.contentSize.height)
{
print("Previous WV Height = \(self.webViewHeight), New WV Height = \(webView.scrollView.contentSize.height)")
self.webViewHeight = Double(webView.scrollView.contentSize.height)
self.tableView.beginUpdates()
self.tableView.endUpdates()
// tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0)], withRowAnimation: .None)
} else {
return
}
}
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation)
{
print("rotated")
let webView = (self.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 4, inSection: 0)) as! ContentCell).contentWebView
webView.reload();
}
You will need to reload webview on orientation change.

Resources