UICollectionViewCell layout issues iOS9 - ios

Since my move to the new iOS9 and Xcode 7 I have stumbled upon an issue with one of the UICollectionView in my app.
Apparently, the UICollectionView doesn't seem to update the UICollectionViewCell layout and constraints properly, only until it is reused.
Pictures speak better than words -- this is how it looks like when the UIViewController is first seen:
However this isn't the correct layout, and easily enough when I swipe the UICollectionView horizontally to the left, I get the right layout of the newly appeared cells:
When I swipe back, the old cells that weren't correct, are now reused and look good.
Now, as it was prior to upgrading to iOS9 and Xcode 7, my wanted effect is that the cells have the correct layout even when first appearing.
For your convenience, here are more details on how the UICollectionView is set up and it's constraints in the XIB:
In the code, it is pretty standard:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell : MatchmakersCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("CollectionCell", forIndexPath: indexPath) as! MatchmakersCollectionViewCell
cell.imageView.backgroundColor = UIColor.lightGrayColor()
cell.bottomName.text = "StackOverflow"
return cell
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.matchmakers.count
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
And every time I updated the datasource (self.matchmakers), I called self.collectionView.reloadData()
One last thing I had noticed which was very strange, when debugging with Xcode the debug view hierarchy, the UICollectionViewCell never presented the subviews properly and just gave me a default UIView in their stead:

To solve this problem:
Creat your custom UICollectionViewCell using Nib(.xib) files.
In your custom UICollectionViewCell ,override didMoveToSuperView() method to add this
self.setNeedLayout()
self.layoutIfNeeded()
3.In your UIViewController viewDidLoad() method
self.collectionView.registerNib(UINib(nibName: "yourNibName", bundle: nil), forCellWithReuseIdentifier: "Cell")
--------update 20150923
just need step 2, override didMoveToSuperView method to add
self.setNeedLayout()
self.layoutIfNeeded()
in your custom UICollectionViewCell class.
Thanks #macayer

I didn't have to create my own XIB for my cell, as long as I have my custom cell and have it linked into my storyboard, I just added those line in (MyCustomCollectionViewCell.m):
- (void)didMoveToSuperview{
[super didMoveToSuperview];
[self setNeedsLayout];
[self layoutIfNeeded];
}

I had same bug in iOS9 too. Perhaps you've found that it's all ok after you reloaded the collection view. So my solution: set a timer for iOS9 to reload the cells.
if #available(iOS 9, *) {
self.refreshTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("refreshDataTimer"), userInfo: nil, repeats: true)
}
And reload them :
func refreshDataTimer(){
self.collectionView?.reloadData()
}
Even though this solution is little stupid but it works in few lines.
or you can reload each cell :
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
....
self.collectionView?.reloadItemsAtIndexPaths([indexPath])
return cell
}
But it's inefficient.

I had same bug in iOS9. I would like center x and center y image to UICollectionView Cell. It doesnt work, but now I added in storyboard Align Top and Align Leading, then I added outlets NSLayoutConstraint and in
(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath. I am changing NSLayoutConstraint depending on the resolution.

I am pretty positive this is an iOS9 bug. Instead of setting up the UICollectionViewCell UI and constraints in the UICollectionView in the Storyboard, I created it its own XIB. Then added to the UIViewController:
self.collectionView.registerNib(UINib(nibName: "MatchmakersCollectionViewCell", bundle: NSBundle.mainBundle()), forCellWithReuseIdentifier: "CollectionCell")
Now it works well.

I had one part of the issue the OP described...
One last thing I had noticed which was very strange, when debugging
with Xcode the debug view hierarchy, the UICollectionViewCell never
presented the subviews properly and just gave me a default UIView in
their stead
I spent ages bashing my head against a wall trying to work out what was up, none of the suggested answers made a difference. In the end I ended up deleting all the constraints in my cells XIB and re-adding them and it fixed the problem. I guess the constraints must've corrupted at some point.
Hope it might help someone.

In my case disabling size classes in .xib file did the trick.

Here is the appropriate Swift 2.0 code needed to solve this issue in Xcode 7. Note that the correct code is 'setNeedsLayout' and not 'SetNeedLayout' as stated above. Make sure to call it in the UICollectionViewCell file.
override func didMoveToSuperview() {
self.setNeedsLayout()
self.layoutIfNeeded()
}

Related

Nib file designing in Xcode 9 using IB

I am making ios app, and I am beginer in iOS. I am making an app and in this I am trying to make the custom tableview cell.
For this I Have added a cocoa touch class and also check to create its nib file.
Now in IB I am putting views like UiLabel,Buttons etc int it , making its height bigger in IB. But this is not updating the View. I can not see any thing inside the View.
I want to see it visually so that I can set autolayout constraints
through IB. Did any one notice this thing? Why the view of IB is not
updating? I have started my mac. but it did not help. Any suggestion
please?
Note: I am using Xcode 9.2
Have you registered your nib file as UITableViewCell? First you need to register your nib file in viewDidLoad of your class where you have dropped the outlet of your tableView.
override func viewDidLoad() {
super.viewDidLoad()
tableView.registerNib(UINib(nibName: "CustomOneCell", bundle: nil), forCellReuseIdentifier: "CustomCellOne")
}
After this, go to your tableView delegates,
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCellOne", forIndexPath: indexPath) as! CustomOneCell
return cell
}
Make sure to assign the tableview delegate and datasource.

UITableViewCell layout not updating until cell is reused

I have a UITableView that I fill with autosizing cells.
UITableView setup is fairly simple:
tableView.estimatedRowHeight = 70
tableView.rowHeight = UITableViewAutomaticDimension
Exactly like Apple recommends here: https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSelf-SizingTableViewCells.html
To enable self-sizing table view cells, you must set the table view’s
rowHeight property to UITableViewAutomaticDimension. You must also
assign a value to the estimatedRowHeight property. As soon as both of
these properties are set, the system uses Auto Layout to calculate the
row’s actual height.
When configuring a cell I also disable/enable some constraints to achieve the needed look. That’s where things get interesting. Cell layout is not updated until the cell is reused. Literally. You can call layoutIfNeeded(), setNeedsLayout(), layoutSubviews() or any other method there is, there is no way you will force the cell to update its layout.
All other aspects work pretty good: labels do change their text, you hide/unhide the views, but layout is stuck until the cell is reused.
Question: what causes it and how to avoid this behavior?
I had your problem too. Instead of remove
tableView.estimatedRowHeight = 70
I just added a layoutIfNeeded at the end of the cellForRow method, just before return the cell itself:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as? MyCustomCell
...
cell?.layoutIfNeeded()
return cell!
}
Result: the cell layout is perfect always, the first time and every after reuse.
Unfortunately, none of the provided answers/comments worked out for me. I always ended up with an initially incorrect layout. Only after reusing the cell, or calling reloadData() (on the table view) it was displayed correctly.
The following was the only thing, that worked for me in the end. I'm not a big fan of such hacks, but after spending about half a day on this seemingly very simple layout issue, I just gave up and went with it. >.<
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Alternatively you could also call reloadData it in viewDidAppear (without the DispatchQueue hack), but then you can clearly see the "jump" when the layout jumps from "incorrect" to "correct".
Anyway, just wanted to share my experience and hope this helps someone else. Cheers!
In my case, the issue was caused by estimatedRowHeight.
Simply removing this line
tableView.estimatedRowHeight = 70
fixed my problems. Cell properly updated its layout and it almost fixed my issues.
But, most likely, you’re going to get another trouble, with your cell’s height being set to 43.5 points. You log will also be filled with auto layout errors, that will include a line like this
<NSLayoutConstraint:0x600000097570 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7fd4ee511d20.height == 43.5 (active)>
Apparently, if you won’t provide estimatedRowHeight, table view puts a 43.5 points height constraint on your cell’s content view, and if your cell’s “internal” height will not match (and probability of that is 99.99%), then it’s going to put errors in log.
How to avoid that error? I don’t know yet.
I post a question about that, and as soon as I find an answer, I will provide a link in this question.
Cell layout is not updated until cell is reused
If you want tableview to reflect changed cell layout.
After changing the cell Layout redraw the table view
tableView.beginUpdates()
tableView.setNeedsDisplay()
tableView.endUpdates()
For instance:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? CustomCell else { return }
cell.collapseDescriptionLabel()
// redraw the tableView
tableView.beginUpdates()
tableView.setNeedsDisplay()
tableView.endUpdates()
}
You don't need to use layoutIfNeeded(), setNeedsLayout(), and layoutSubviews() to force the layout. You can use tableView.beginUpdates() and tableView.endUpdates().
For instance:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
tableView.beginUpdates()
cell.heightConstraint.constant = 50
tableView.endUpdates()
}

iOS analog for inflating a view from a layout (xib)

I'm new to iOS and trying to rewrite an app from android.
What I do in Android - I have a layout - same as nib in xcode
And then I inflate this view as many times I want - same as ListView (TableView's android analog) is working
I need to do this on iOS :
which means having some container like Android's horisontal LinearLayout where I can add nib's with their class - like UITableViewCell and fill data.
When I asked one person who is iOS developer, he told me that it is almost impossible to do due to compexity and lack of android-like ViewGroups and that it's better to do in a WebView than natively.
So please, tell me , is there a solution - to inflate as many views as needed from a nib into container-views one under another ? Please answer in Swift, I don't know Obj-c at all
Yes you can do it by registering your tableView with Xib in viewDidLoad
tableView.registerNib(UINib(nibName: "CustomCellXib", bundle: nil), forCellReuseIdentifier: "CustomCell")
Then do this in delegate method
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath) as! CustomCell
cell.middleLabel.text = items[indexPath.row]
cell.leftLabel.text = items[indexPath.row]
cell.rightLabel.text = items[indexPath.row]
return cell
}
where CustomCellXib is your Xib file name.
CustomCell is your class of CustomCellXib
and #"CustomCell" is string identifier for reusing cells
Note down that you will have to implement other few delegates methods too for complete working of TableView.
There is nothing extremely complex here. It's a common UITableView with multiple sections.
Red text labels for every sections should be implemented as section headers
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//create a view here
}
Custom UITableViewCell with grey labels, red - and + buttons in it.
Custom cell should contain a UIImageView below it's content. Since it is different for first, middle and last rows, you should set one of three images in cellForRow method depending on the cell's indexPath.row.
P.S. Don't listen to your iOS developer. You should not use UIWebView whenever you need to implement UI a bit more complex than default UITableView. Most probably he was joking ;)

SelectedBackgroundView of UICollectionViewCell visible when it should not be

I've got a UICollectionView. With some cells inside with a white background color. I've set the selectedBackgroundView to a basic purple view.
My CollectionView has a constraint with a height of 0 and when I hit a button I update the constraint to 80. When I'm doing that, during the animation i can see the purple background on the screen until the end on the animation and i cannot understand why or how prevent this ?
Everything else working fine, it's just a "visual" bug.
Any suggestion about how to fix this ?
Gif of the bug where you can see the purple appearing during the animation
Here is my cell construction if it can be of any help :
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("WidgetMenuCellIdentifier", forIndexPath: indexPath) as UICollectionViewCell
cell.removeSubviews()
// some code setup
cell.selectedBackgroundView = UIView()
cell.selectedBackgroundView.backgroundColor = UIColor.purpleColor()
return cell
}
Subclass your UICollectionViewCell
Do
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// your code
cell.selectedBackgroundView.hidden = true
return cell
}
Then in your subclass :
override var selected:Bool {
willSet {
self.selectedBackgroundView.hidden = false
}
}
It should work.
It seems like this code is being executed within an animation, causing unexpected behavior at times based on how various properties animate. Another complicating factor is that, because cells are reused, it won't reproduce if a reused cell is already configured correctly (i.e. there is nothing to animate). Adding the following after styling the selectedBackgroundView was the least hacky solution I could think of.
[cell.selectedBackgroundView.layer removeAllAnimations];
Depending on what your cells are like you may also want to consider removing animations on other layers as well. For example:
[cell.backgroundView.layer removeAllAnimations];

Why does my UITableView's formatting go completely awry when I return from a view controller I segued to?

I have a UITableView with a custom cell, which has a few labels in it that dynamically decide the height of the cell. When I tap on one cell and segue to a new view controller, upon returning all the formatting for the cells is completely messed up, and I can't figure out what is causing it.
Here is what the cells normally look like:
And I have some pretty basic constraints set on them. The top label is pinned to the top and left margins, and must always be >= 20 from the right. The other labels are aligned to the left of this first label, with vertical spacing set between all of them. The middle label has a right spacing constraint to the margin, and the bottom labels are aligned to the baseline of the first and have horizontal spacing between all of them.
When I segue back to this table view it looks like this however:
I can't figure out what is causing it to layout differently than when I left. If I scroll around it seems to "reset" them back to what they should be, but on initial load they're really messed up. I can attach the project if desired, but there's really not much outside of the Storyboard.
cellForRowAtIndexPath:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as CustomTableViewCell
let object = objects[indexPath.row]
cell.title1.text = object.name
cell.title2.text = object.color
cell.title3.text = object.roar
return cell
}
Sample project: http://cl.ly/040L2z0q0V2d
It appears that the table view cells aren't resizing based on the contents when returning from the segue. Using the sample project, I threw a reload data in the viewWillAppear and that seemed to fix the issue.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.tableView.reloadData()
}
There are actually couple of issues with your project.
Data Loading and AutoLayout.
The first one is causing a strange behavior at the time of drawing the cells with data. When unwinding from the segue you'll see those additional cells on top of your table caused by ambiguous layout calculation.
Solution: Move the data into override func viewWillAppear(animated: Bool) { and perform a tableView.reloadData() (as correctly suggested by #rFessler).
On the other hand, Autolayout is a kind of fiery beast. Tamable. It's worth investigating the topic further. I wasn't able to make your layout work with autosizing cell height but I'll leave few references and the project for you.
References:
http://www.appcoda.com/self-sizing-cells/
http://captechconsulting.com/blog/tyler-tillage/ios-8-tutorial-series-auto-sizing-table-cells
Project:
http://cl.ly/3z3a2Z3a3U2K
I've had a similar problem myself. I downloaded your project and it seems I've solved it by removing and tweaking some constraints. This is how my constraints look now:
Also I've added this to viewDidLoad:
self.tableView.estimatedRowHeight = 120
self.tableView.rowHeight = UITableViewAutomaticDimension
I also added this to test delete:
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete
{
self.objects.removeAtIndex(indexPath.row)
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
}
Now you can even rotate the device and remove rows and it's all working splendid!
However, there's still problem if you push this view on a Navigation Controller (Which is what my problem was about in the beginning). See my storyboard below to get some funky labels:
To solve this, it seems we actually have to do a hack! (Damn you apple, what is going on with this?!)
var firstAppearance=true
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if firstAppearance
{
if let indexPaths = self.tableView.indexPathsForVisibleRows()
{
self.tableView.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
self.firstAppearance = false
}
}
}
At the moment, I think this is as good as it gets.
I played with this and find a simple solution, add this seems to fix the problem.
override func viewWillDisappear(animated:Bool) {
super.viewWillDisappear(animated)
self.tableView.estimatedRowHeight = 166.0
}
Since the the method tableView:estimatedHeightForRowAtIndexPath will be called every time you segue to a new MVC, and change the autolayout, you can just do
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
to reuse the autolayout

Resources