Autolayout not working with reusable tableview cells - iphone

I'm trying to use Autolayout and Interface Builder to create a custom TableViewCell. I setup my cell in viewDidLoad like this:
[tableView registerNib:[UINib nibWithNibName:#"BChatCell" bundle:Nil] forCellReuseIdentifier:#"ChatCell"];
Then I dequeue the cell like this:
- (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath: (NSIndexPath *)indexPath {
BChatCell * cell = [tableView_ dequeueReusableCellWithIdentifier:#"ChatCell"];
return cell;
}
When the table view first loads it loads correctly:
However, after the table scrolls, Autolayout breaks:
Here are the constraints:
Any help would be greatly appreciated.

I had the exact same issue. Drove me crazy because everything looks great until the cells get reused.
I discovered that the view that was getting resized was not the cell itself (which is always screen width), but an inner content view I had. You're able to print all the constraints on any view with NSLog(#"%#", [theView constraints]); which helped with debugging
Also tried to no avail:
setting all the important views and subviews to translatesAutoresizingMaskIntoConstraints=NO
manually setting bounds inside -(void)prepareForReuse
calling setNeedsUpdateConstraints
The issue was that I had named my inner card view "contentView" which is already a named view in UITableViewCell. Changing it fixed the resizing issue.

The problem with your approach is the static leading and trailing. Your trailing constant is 200 and when you rotate it you want it to become more than that 200 (because of the width of the screen). This breaks the constraint and probably in the console is saying that you have ambiguity in your constraints and it did the automatically add a new one (which results in weird behavior).
So what I suggest is that to try making the trailing constraint Greater than or equal 200 and see if it works.
Another solution would be adding a width constraint for that blue view and removing the trailing constraint.
I hope this helps!

Related

UITableViewCell content moving around

I have an iPhone app in which I'm using a UITableView to format a set of text responses, one per row. I've set up the cell in the storyboard to have a label inside it, and set up a constraint saying that the label should be 10 points from the edge of the cell. I then set up a custom subclass of UITableViewCell, set the cell in the storyboard to be of that class, and connected the outlet.
However, when I load the table, I see the text in the cell moving slightly to the right under some circumstances: when I select the cell, or when I load additional cells into the table. In fact, in the latter case, sometimes everything gets shifted to the right, even cells which were already there!
What the heck is going on here? The only changes I'm making in tableView:cellForRowAtIndexPath: are to the text in the label, and I'm always setting it. And I've unset "Indent While Editing" on the cell in the storyboard.
Answering some of the questions: I'm setting the view up using the storyboard. Xcode isn't reporting any ambiguity with the constraints. Also, here are the screenshots, before and after:
My guess is that the constraints for the label are ambiguous. Ambiguity can make UI components jump around for inexplicable reasons. You probably need to set more constraints for the label to define its position on both axes.
Or, maybe all you need to do is set the label to the "size that fits content" (intrinsic content size) under the Editor menu in IB.
Did you add a new label to the UITableViewCell, or are you working with the textLabel that already exists in it? If you added a new one, consider removing it and using the cell's existing textLabel property instead. If that's not an option for some reason, double-check that the label you've added is in the contentView of the cell, and that all the constraints are relative to the parent view, not to the cell itself.
Also, for debugging, you could set the cell's contentView background color to red (cell.contentView.backgroundColor = [UIColor redColor];) - this might give you a better sense of what's moving, the label or the whole view.
This is just a guess without seeing your code. I had a similar problem once, when the app reused an existing cell, the dimensions of the label were not correct. So, I had to remove the old label in my cellForRowAtIndexPath method, before adding a new label. Here's how I removed the old one:
UIView *oldLabel = [cell viewWithTag:3];
if (oldLabel != nil)
{
[oldLabel removeFromSuperview];
}
Then I added a new label like this:
[cell.contentView addSubview:newLabelOrWhatever];
It might be worth checking to see that your string content doesn't have a space prefixed on it. Also, you could verify the actual position of the label by setting the background color.
I had a similar issue when the label would move when the cell was selected. It was a custom cell that I was loading from a custom Nib.
In the Nib I had not set the backgroundView of the UITableViewCell (superclass) to any view. Once I set it (I set it to the ContentView) the issue stopped.
My auto-layout constrains seems fine and had no issues, so I'm assuming it was the above that fixed it.
If you have a custom subclass of UITableViewCell, try implementing layoutSubviews. Let me try from the top of my head:
static CGFloat const kLeftMargin = 10.0f;
- (void) layoutSubviews
{
[super layoutSubviews];
/* Prepare for processing */
CGSize cellSize = self.bounds.size;
/* Change the textLabel frame */
{
/* Compute the new label frame */
CGSize labelSize = self.textLabel.bounds.size;
CGFloat xCoor = kLeftMargin;
CGFloat yCoor = roundf((cellSize.height - labelSize.height) / 2.0f);
CGRect labelFrame = CGRectMake(xCoor, yCoor,
labelSize.width, labelSize.height);
/* Set the new label frame */
self.textLabel.frame = labelFrame;
}
}
Now, this isn't what is usually recommended (since a lot of people use Storyboards and NIBs), but from experience, if you want some correct layouting done, implement layoutSubviews.

Pushing a view controller after scrolling is complete

When a user adds an item to my list, I want to scroll to the new row, highlight it, and select it (which will push a new controller). The key part is waiting for the scroll animation to complete before pushing the new controller.
In this answer, I learned how to use the animation delegate to wait until the scroll is complete.
However, if the insertion row is already on scree, the table view will not scroll and the method will not fire.
How can I wait to push the new controller until the end of the scroll, and deal with the case where no scroll will be initiated - and how might I tell the difference between each case?
The easiest way to check whether a given row is visible in your table view is something like this:
if (!CGRectContainsRect([self.tableView bounds], [self.tableView rectForRowAtIndexPath:indexPath])
{
// the row is partially outside the table view’s boundaries and needs to be scrolled for full visibility
}
else
{
// the row is within the boundaries and does not need to be scrolled
}
Try creating a method to see if scrolling is needed. If no scrolling is needed, call the push right away, otherwise wait for the delegate call and push.
- (BOOL)isSrollingingNeededForIndexPath:(NSIndexPath *)indexPath {
NSArray *visibleIndices = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *visibleIndexPath in visibleIndices)
if ([indexPath compare:visibleIndexPath] == NSOrderedSame)
return NO;
return YES;
}
Edit: Good point. Since indexPathsForVisibleRows is used for data rendering.
You could do essentially the same thing with indexPathsForRowsInRect where you use the content.offset.y and the tableview.frame.size.height to determine your "visible rect".
Then to account for partially visible rows at the top and bottom you could add rowHeight-1 to the top of the rect and subtract rowHeight - 1 from the bottom of the rect. Code shouldn't be too gnarly if you have static height rows. If you have varying height rows it would still work, but it would be a bit more involved.
All said though, it seems like a lot of code for something which you'd think would have a simple answer.

cellForRowAtIndexPath: not called

My app has two states: logged in and not logged in, and I have the following architecture (vastly simplified):
- ViewController A which contains a search box and a table view.
- ViewController B which is used for logging in the app.
The flow is the following:
- the user is not logged in;
- A is pushed on the stack. In viewWillAppear I check if the user is logged in and if yes, an async network request is being made and, once that is done, the table is loaded with the data from the network. However since the user is not logged in at this point, the table view is empty;
- the user taps on the search box; because he's not logged in, B is pushed (after a confirmation);
- he logs in successfully in B, then presses a button which pops B and shows A again;
- at this point in time, because he's logged in, from viewWillAppear I do the async request;
- when that is completed, I call reloadData on the table view.
What I notice is that numberOfRowsInSection: is called and it returns the correct result, however cellForRowAtIndexPath: is NOT called afterwards and the table remains empty.
I've checked and reloadData is called on the main thread.
Any idea what can it be? Cause it's driving me nuts!
Thanks,
S.
EDIT: Here's the async bit of code from viewWillAppear in A.
if ([User isLoggedIn]) {
[self.asyncRequest fetchDataWithCompletionHandler:^(id response, NSError *error) {
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
if (error) {
[Utils displayError:error];
} else {
self.array = response;
self.isLoaded = YES;
[self.tableView reloadData];
[self.tableView setContentOffset:CGPointMake(0.0f, 0.0f) animated:NO];
}
}];
}
I've checked that the async request completes successfully and that response contains the correct data (this is the array used to back the UITableView).
After reloadData, I've put a breakpoint in tableView:numberOfRowsInSection: and it stops there and it returns the correct number of elements in array. After that, however, the breakpoint in tableView:cellForRowAtIndexPath: is never hit.
One valid scenario for why numberOfRowsInSection would be called but cellForRowAtIndexPath would not be called is when the size or positioning of the table does not require any rows to be displayed.
For example, let's say you had a table that was 20 pixels high, but the header for the first section was 30 high and you returned nil for the header (or did not implement viewForHeaderInSection). In this case, no rows would be displayed and it would look like the table just isn't there.
I see you are using IB. The sizing of the table can be deceptive in IB as it is assuming header and footer sizes. I would log the frame for the table so you understand where it is when the appliction is run (versus where it appears in IB). I would also be sure to manually size and position the table's frame before calling reloadData. This will solve this valid case where cellForRowAtIndexPath is not called.
Check that the numberOfSectionsInTableView is not returning 0.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
I had the exact same issue and the problem was that I was trying to call reloadData: from another thread. The solution would be:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
If you are using a different dataSource, as I was, make sure you are retaining the dataSource. Merely instantiating the class that will be the dataSource and assigning it via tableView.dataSource = myDataClass will not be sufficient as the dataSource property of a tableView is weak and will be released when viewDidLoad completes. All of the other methods were being called for me — even, surprisingly, heightForRowAtIndexPath — so this took me some time to debug.
If a Table View inside a view that conflicts something like Scrool View, it does not called. You should separate the views in your Storyboard or *.xib file.
// True
▼ View
► Table View
► Scrool View
► Constraints
// False
▼ View
► Scrool View
► Table View
► Constraints
// For others showing up on this questions via Google, etc.
// Check and make sure you've set the table view's data source and delegate.
self.tableView.dataSource = self;
self.tableView.delegate = self;
We had the same/similar issue. The code reached numberOfSections and numberOfRowsInSection (and it even returned values) but could not reach cellForRowAt. At the time, Table View in the Storyboard had only constraints for right side, left side and top, but not for bottom. Once we added constraint to the bottom, it worked like a charm.
So, check that you provide all needed constraints for the Table View.
If you are using auto layout like me and adding tableview to your view controller's view make sure you have added this line when you are allocating your table view.
tableView?.translatesAutoresizingMaskIntoConstraints = false
Silly mistake from my side.
Everyone keeps talking about height, but my TableView in a StackView with leading alignment ended up with 0 width.
Make sure to check if your TableView is correct size using Debug View Hierarchy.
I solved the problem because my Subview on which i added the UITableView was not allocated so it was returning nill and tableview was not calling cellForRowAtIndexPath but numberOfRowsInSection getting called
Wasted hours and then found out that my tableview was inside a stackview. As soon as I removed the stackview, it worked like a charm. For me it's Xcode 11.3 .
I am using a customer datasource and it was released. Because it is a weak reference of tableview.
This is embarrassing and perplexing, but here's my fix.
My code:
_scanResultTable.delegate = self;
_scanResultTable.dataSource = self; // self's lifecycle was fine, wasn't getting released
[_scanResultTable reloadData];
So the weird part is: the identifier _scanResultTable was never declared in my code, anywhere in the project. I have no idea how this compiled (and I recompiled several times).
My root cause was that I had linked my table output to scanResultTable in my ViewController, but was referring to it as _scanResultTable. Once I started using scanResultTable like I should've been, things cleared up. This makes me wonder if objective-c has something special about leading underscores in identifiers...
Edit: It does! Good lord I can't wait to never touch this language again.
In my case, I had the TableView inside a StackView. When I put it outside the StackView it worked.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Make sure self.data isn't nil!
// If it is, you'll always return 0 and therefore
// cellForRowAtIndexPath will never get called.
return [self.data count];
}
I'm using ReactiveCocoa. So I've created the model for table view. The data was prepared to be displayed, so numberOfRows and so on was called. But I haven't added the tableview as subview, thus cellForRowAtIndexPath was not called)))
This maybe too obvious but in my case i was deleting an element (Label) which was a reference to the top margin of my table programmatically, so it was not a problem with delegate/source but of the table having some problems with the content-size / height, this conflict created a weird behaviour, calling numberOfRowsInSection but not cellForRowAt indexPath
Are you sure that after the user logged in and B is popped the viewWillAppear method in A gets called in order to perform the refresh?
If you show B as a modal controller, when you dismiss it you won't have the viewWillAppear method called.
As far as I know, viewWillAppear/viewDidAppear (and other like it) are events generated by the UINavigationController in the case of navigation events (push/pop viewcontrollers).
So maybe that is why you eventually get your refresh when you leave A and return ... it all depends on the way to display the next viewcontroller.
Try to insert new rows manually instead of [self.tableView reloadData]:
[self.tableView beginUpdates];
for (int i = 0; i < responseArray.count; i++) {
_rowsNumber += 1;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationBottom];
}
[self.tableView endUpdates];
In dataSource method return incremented int _rowsNumber:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _rowsNumber;
}
i solved same issue by checking tableViewCell identifier. Go To Attributes İnspector and look at identifier section. Probably is missing. So write cell identifier.
Same case here, but it was a mistake:
I included a Scrollview with 2 tablesViews inside a ViewController (design requirements), in the scrollview controller I created the ViewControllers (that contains the TableViews) programatically and I used a weak var to store it, bad idea because they were released at the end of viewDidLoad method.
If you don't see your tableview content, please, check if it was released.
My mistake was very brain-painful for me because all methods (delegate&datasource) were called except viewForCell...
I have same issue
i have use Table View inside the StackView and table view scroll disable and set height constrain but after that
tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
Not working stop calling this method all Data Source and Delegate set properly.
if you have same issue than Solution is set Table View bottom , leading , trailing constrain
None of the answers here helped me.
I was using doing my constraints programmatically but forgot to write:
myTableView.translatesAutoresizingMaskIntoConstraints = false
Make sure your constraints are correct and none of them is removed at build time.

How to hide this part of uitableview

I designed a uitable view like in the image below , I want to hide the part under the second section , which I write on it (<- I want to remove this part ->)
any suggestion to do that
If you coppied the code from someone, this element is the fotter view for the second section. Look for the method:
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section{
end erase it. It should be out.
You can try creating the table with specific height in the Interface Builder or:
UITableView* pTable = [[UITableView alloc] initWithFrame:(CGRect)];
Maybe you can also specify that scrolling is forbidden in order to show the contents that you want:
pTable.scrollEnabled = NO;
You might want to implement tableView:heightForFooterInSection: and tableView:heightForRowAtIndexPath: and return heights for the footer of the current section and header of the next section to reduce the gap between the two sections. This of course, if there are sections are the current section. If there are no more sections, then you can just reduce the frame size.

How to make a custom drawn UITableViewCell resize properly?

For performance reasons, I draw the strings for my UITableViewCell in a custom view that overrides its drawRect method to draw strings directly in the view rectangle using NSString:drawInRect. This is similar to Apple's TableViewSuite Example 5-CustomTableViewCell.
However, when I invoke setEditing on the cell to bring up the delete button, the view ends up with a squeezed appearance after the animation completes. To demonstrate this, invoke setEditing:YES on the CustomTableViewCell example mentioned above and observe the distortion. Is there any way around this or should I just revert back to using UILabels for my text?
I had a similar problem with a UIView inside a UITableViewCell. I solved it by changing the UIView's contentMode to UIViewContentModeLeft. (I wrote it up here, with screenshots.)
I had this problem too, and in my case I fixed it by handling the 2 states in my drawRect method, one while editting, the other while not. In other words I accounted for the size of the delete button, and got my UI to repaint the cell differently. I'm not sure if it's the most efficient way to go, but here is the code that I used to force a repaint:
-(void)_refreshTableAndCells{
//refresh the table
[myCustomTableView reloadData];
//refresh all the visible cells
for (UITableViewCell *cell in myCustomTableView.visibleCells){
LocationCellView *locationCell = [cell.contentView.subviews objectAtIndex:0];
[locationCell setNeedsDisplay];
}
}
I'm an Objective-C n00b though, so I'd be more than happy for someone to suggest a better way than this.
I usually just modify the x and width values (or whatever else) of whatever I want to be different when editing or not. UITableView automatically calls layoutSubviews when you begin editing, so you don't have to loop through your cells and do it yourself.
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat editingPadding = 5.0;
self.textLabel = CGRectMake((self.editing ? self.textLabel.frame.origin.x + editingPadding : self.textLabel.frame.origin.x), self.textLabel.origin.y, (self.editing ? self.textLabel.frame.size.width - editingPadding : self.textLabel.frame.size.width), self.textLabel.frame.size.height);
}
Try setting the contentMode of your own custom view (which resides inside the cell's contentView) to UIViewContentModeLeft. The "squeezing" is due to the fact that the default contentMode is UIViewContentModeScaleToFill.