I know there are tons of questions about tableViewCell disappearing on scroll, but none of those fit into my situation.
I say disappear, I mean the whole cell disappears, not some of its subviews.
Here's my situation:
I have a tableView with custom cells, each one of them is very complex, some could even host up to three collectionViews in them. Most of these cells are only displayed once, so I cache them. I retrieve data from network, and use it to populate the cells. As sometimes the data may not be fully available, some sections of the cell needs to be hidden: the cell will set some of its collectionViews' height constraint to zero, and isHidden to true, then notify the tableView, who will reload the cells' heights using:
tableView.beginUpdate()
tableView.endUpdate()
I have noticed two behaviors:
1:
If none of the cell's portion is hidden, the cell works perfectly, except when I scroll(from bottom up) too fast, the cell disappears before going out of the bound of the tableView. When I scroll slow, this does not happen.
2:
If some part of the cell is hidden, the cell will disappear before going out of tableView EVEN WHEN I scroll very slowly.
Sorry I cannot post any code, but what I might have done wrong?
P.S.: In my tableView's row height delegate method, I have:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row] ?? 0.1
}
cellHeights is where I use a dictionary to store computed height of the corresponding cell. I am certain that these height values are correct, because when I scroll from top down, the cell looks all right. This only happens when I scroll from bottom up.
I did some experiment with the default height 0.1. When I changed it to 1000, the cells are disappearing randomly even more. 0.1 looks relatively stable, but the problems above still exist.
Thanks.
Related
The setup
I have a rather complex view hierarchy which made me do terrible things with tableViews and collectionViews. Right now I'm having a regular grouped TableView with custom TableCells. TableCell itself contains a couple of views and a Collection View. The height of the table cell is calculated based on number of items in the data source. On its creation table cell creates a collection view with a calculated size to fit necessary data.
-- UITableView
---- UITableViewCell
------ UICollectionView
--------- UICollectionViewCell
The problem
I've encountered unusual problem with a custom collectionViewCell. I have a vertical single-column collectionView with dynamic amount of cells. Ideally tapping on the cell should call didSelectItemAt. The cell also has three buttons. Tapping on the button should trigger some action. All of the desired functions work only for a first cell. The rest of the cells are not responsive to any actions.
Things that look strange
By default the scrolling of a collectionView inside of the tableViewCell is disabled because it basically fits all the content based on calculated height and doesn't require scrolling. (Also I don't want it to interfere with tableView scrolling logic).
First
I've tried to hardcode some value for the height of collectionView and enable scrolling. What happened is a mystery for me.
Let's say that calculated height required for the collectionView to show all the content without scrolling is 740. When I manually set it to be 280 (this is enough for exactly 2 cells to fit) and enabled scrolling my first cell were still working, but also when I scrolled collection just a little bit my second cell started to act normally as well. When I scrolled back to the top of the collectionView it was disabled once again.
So it looks like when the scrolling is enabled and will actually occur because of insufficient height to fit the content, cells behave as they should. As soon as I set height of the collectionView to be enough to fit its content, things go wrong.
Second
In some cases I can actually tap on the second cell and it will call the delegate. But the weird thing is it works when I tap in the top area of the cell, like 10pts from the top. The other areas of the cell are unresponsive so are the rest of the cells in the collection.
The working delegates and buttons with enabled scrolling forces to think that this has nothing to do with delayed or canceled touches. The frame for collectionView and height of the table cell are calculated properly as well.
xCode 8.3, iOS9+
As it turned out when I was creating my tableViewCell, I hardcoded the height of the collection to value of 214. The reason for that is very simple. I create a cell programmatically and with tableViewCells created programmatically the height of the cell is always 44. When you override init(style: UITableViewCellStyle, reuseIdentifier: String?)
You need to either hardcore the height, or update it in cellForRowAtIndexPath. Even though I updated collection frame when it was filled with data, the container view of this collection wasn't updated resulting in some sort of clipping area for user interactions.
Sorry for the influx of UI and GUI questions but I was recently looking at an application called Ganna and in this pic below I was wondering how you would get that random Ganna Around The Globe banner beneath the Recently Played horizontal collection view and above the Top Charts horizontal collection view.
The way it's organized is that there is a main collection view with multiple horizontal collection views such as the Recently Played and Top Charts horizontal collection view.
I am thinking if the indexPath is 0 (which means you are at the Recently Played horizontal collection view) then you would insert that Ganna Around the Globe banner underneath that collection view. But this would be difficult since I would have to return different heights of the cell depending on whether that banner exists or not. And this banner appears 3 times beneath 3 different horizontal collection views.
Do you guys have any ideas? Thank you!
I can't say if this is how that particular example is implemented. But the way I would approach this is fairly simple.
You would have a single TableView with dynamic cells that covers the entire screen from nav bar to tab bar. Make two custom cells, the first custom cell contains a collectionView. The second cell would be laid out in whatever manner you want for displaying that banner.
In your cellForRow method of the tableViewDelegate you set the delegate of the collectionView inside the cell to whatever it needs to be.
The only thing left after that is a tiny piece of logic in the heightForRow method. Something like
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if (indexPath.row == kBannerCellIndex) {
return showBanner ? kBannerHeight : 0
}
// the rest of any height logic
return defaultCellHeight
}
Update: It looks like the tableView.estimatedRowHeight setting is somehow related to the problem, since the variance of the problem changes as the number changes, and setting it to 9999 makes it "go away" but that seems super hacky. The 9999 also breaks the animations I intended to do on the show/hide. 9999 also breaks the scrollbars funtionality. See the discussion thread below for additional details and full context.
I have a view controller which holds two Container Views. Each container view holds a UITableView controller. They're orientated vertically each taking up the full height.
The container on the right side has a width of 100 and starts off screen with the trailing constraint set to -100 which allows the other container to fill the whole screen initially.
The table on the left has automatic adjusting cell heights using UITableViewAutomaticDimension since when the hidden view is shown, some text on two lines may need to wrap to the next line.
When I change the constraint to be 0 and show the view, if I'm not at the top of the list, rather than expanding what's visible on the screen, it seems to be expanding everything, thus pushing what was on the screen down, further than it should be going.
What's weird though, is subsequent expand/collapses work as expected, only adjusting what is visible.
I've attached a video a video showing the issue, I'm just trying to connect things up and get the sizing working right now, the content in the right view will be different eventually.
The following is what's happening in the video
I expand/collapse from the top of the list and it works as expected
I scroll to middle of list, and expand, and my content gets pushed down further than it should be.
I then collapse and expand it again, and this time it stays in place since no sizes changed on the visible screen.
It doesn't matter how far down the list I go, the same behaviour happens, getting worse as I go further down.
I'm adjusting the constraints like this on a button press for now.
if self.timeContainerConstraint.constant == 0 {
self.timeContainerConstraint.constant = -90.0
} else {
self.timeContainerConstraint.constant = 0.0
}
Does anyone know why the content is jumping when I scroll down the list and expand the first time, but not jumping on subsequent attempts?
As you have variable cell heights, the tableview can't correctly calculate the scroll offset when its frame changes. It has to rely on the estimated row height, but as you have a fixed value for this, the more cells 'above' the current scroll position, the greater the error between the estimated and the actual value.
I was able to solve this by caching the cell heights in order to return a more accurate estimated height using estimatedHeightForRowAt:
var cellHeights = [IndexPath:CGFloat]()
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let frame = tableView.rectForRow(at: indexPath)
self.cellHeights[indexPath] = frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return self.cellHeights[indexPath] ?? 140.0
}
Using this approach, the expansion and contraction of the side bar didn't affect the table's position. I was able to animate the constraint change for a nice smooth reveal and the table didn't 'jump', even if the table was scrolling while the side bar was opened.
My sample project is here
Apologies in advance if this has been answered somewhere. I've looked everywhere and I'm still unsure what to do. (And answers that use Objective C are almost completely worthless to me.) I'm somewhat new to iOS.
I have a UITableView which serves as a newsfeed of sorts, displaying a series of posts. (a la a twitter newsfeed, for instance.) The cells in this table view are (currently) derived from a single cell prototype, created in xcode's interface builder. Each cell contains subviews to display things like username, profile image thumbnail, title, message, date, location, another image, etc.
The problem is, depending on what data a particular post contains, many of these subviews either should or should not be shown -- if a post doesn't contain an image, then that cell's image view should not be shown; if a post doesn't have a date and/or location, one or both of these views should not be shown. Not only should the unused fields be empty, but they shouldn't take up any space in the cell.
I read in Using Auto Layout in UITableView for dynamic cell layouts & variable row heights (under "2. Determine Unique Table View Cell Reuse Identifiers". Wonderful answer by #smileyborg, btw.) that for each different layout of subviews that could be in a cell, a different prototype cell and reuse identifier should be used. But this would require me to have a cell prototype for every single possible combination of data items in a post, even if the difference is single label! Surely there must be a better way.
What is the safe and correct way to do what I need to do? Is there perhaps a way to remove subviews from cells at runtime (and have the layout adjust its spacing accordingly) without completely screwing up cell recycling?
Ill assume you know everything you want to know about your layout when you see the cell.
So its basic tableview cell layout. Dequeue and decorate.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(MyIdentifier, forIndexPath: indexPath) as! MyCustomCellClass
let data:MyDataClass = myDataAtIndexPath(indexPath)
decorateCell(cell,data:data)
return cell
}
func decorateCell(cell:MyCustomCellClass,data:MyDataClass) {
//here is where you arrange/change your constraints and hide/reveal views
//depending on the data
}
but also what you need is to allow the cell to set its height properly
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return someEstimatedNumberWhichIsClose;
}
but most crucially in your XIB, the cell MUST have a continuous line of constraints from top to bottom which describe the height.
So this cell will auto-set its height correctly.
While this one will not.
NB - in the upper example , if you leave the height constraint off the text-field and make it infinite line count the cell will grow/shrink appropriately.
So to summarise you can use one multipurpose cell as long as you keep your height constraints coherent.
As a general rule - Swiss Army CellsĀ® which do lots of things may get a bit unwieldy so you do need think about how many use cases you wish to support with one cell and then maybe start creating multiple types/identifiers.
I am developing a small app, that includes messenger functionality. When showing a chat thread obviously I used a tableView, populate it with the messages in small bubbles and at some point before viewDidAppear scroll to the bottom, so the most recent messages are shown.
There is a popular solution to scrolling to the bottom of a tableView with scrollToCellAtIndexPath or scrollRectToVisible. These kinda work with little data, and constant cell heights. BUT in my case there is a lot of message data and I use UITableViewAutomaticDimensions for rowHeight. This results in slow loading, not scrolling to the bottom and crazy error messages.
Here's why (IMO): To scroll to the bottom, the tableView has to load all cells because they have automatic dimensions, and can only scroll down after he knows how far it is. Auto layout is another problem. Sometimes it doesn't scroll all the way down, because auto layout didn't finish yet and rowHeight is at the initial value still.
What I tried: putting the scrolling in didLayoutSubviews: solves the problem but loads very slowly, and scrolling is called multiple times (+ some crazy error message)
I guess an upside down tableView would solve the problem, because then the first cell could be the latest message, and no scrolling would be needed.
For optimization, you should use following method:
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
It is designed for large data sets, because it provides estimates for calculations instead of pixel-perfect size of cells (you also have to provide normal method for getting height - obviously).
Second optimization that you can do is when you have cells that does not change height with time (that is, they have the same height every time), just store already calculated height for each NSIndexPath and then use this value instead of calculating it again. To get height of the cell with AutoLayout, you can use this:
// Configure it
self.configureCell(cell!, indexPath: path)
// Update constraints
cell!.setNeedsUpdateConstraints()
cell!.updateConstraintsIfNeeded()
cell!.contentView.setNeedsLayout()
cell!.contentView.layoutIfNeeded()
// Calculate height of cell
let height : CGFloat = cell!.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height