Autolayout in UIView where is safe to ask computed layout values? - ios

I've got this issue. I'm using a collection view as a subview of the main view of a UIViewController. The collection view is pinned to the superview size, thus it has 4 constraints for lead,trail, top and bottom with constants equal to 0.
The cell is a subclass to UICollectionViewCell and is composed by:
first header view
second header view
UITableView
The collection view cell is loaded from xib and the interface of the views is made for 4 inches devices.
The first header view is pinned at top, trail, lead with constant 0 and fixed height to its superview.
The second header view is constrained to first header view with vertical spacing equal to 0, fixed height, pinned trail, lead with constant 0 to its superview.
The table view is constrained to second header view with vertical spacing equal to 0 and pinned trail, lead and bottom with constant 0 to its superview.
On 4 inches screen everything is OK, but when I load on 3.5 I have some problems, the fact is that I want to create UITableViewCell with a dynamic height. The height should be the height of the UITableView dived by the number of rows, in this way they will appear on screen.
The table view delegate and datasource are valorized only when I set the data that need to be loaded and that happens after creating it, but before the collection view cell is returned from the data source method.
In the -layoutSubviews method of the UICollectionViewCell subclass I set the tableview row height, to the desired value that is
- (void) layoutSubviews {
[super layoutSubviews];
self.answerTableView.rowHeight = self.answerTableView.bounds.size.height / self.quizSection.answers.count;
}
The fact is that here the table view is stil not resized thus the resulting rowHeight is wrong, the value is the same I receive in 4 inches display. The superview (the contentView of the collection view cell) is ok, but the table view has a size that is not correct. When displayed the collection view is fine except for the fact that the table view's cells are wrong in size.
So I started to trace the cycle of the collection view cell.
In the initWithFrame methods the collection view cell has the same size of the original xib (4 inches screen)
In the view controller if I ask the size of the collection view cell right after dequeuing, if resized to the correct screen size, but the table view inside not
After loading the data and setting delegate and datasource, collection view is ok, but tableview not
In the -updateConstraints of the collection view cell the table view size is still wrong
In the -layoutSubviewsof the collection view cell the table view size is still wrong
The first call of the dtasource method of the table view returns a correct size
I've found the solution that is use the delegate methods - (float) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath, but I'm really curios why the other approach doesn't work can someone explain why?
THX
[CODE FOR HELP]
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) { // Initialization code
NSArray *arrayOfViews = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil];
if ([arrayOfViews count] < 1) { return nil; }
if (![[arrayOfViews objectAtIndex:0] isKindOfClass:[UICollectionViewCell class]]) { return nil; }
self = [arrayOfViews objectAtIndex:0];
[self.answerTableView registerClass:[UITableViewCell class] forCellReuseIdentifier:AnswerCellIdentifier];
}
return self;
}
- (void) layoutSubviews {
[super layoutSubviews];
self.answerTableView.rowHeight = self.answerTableView.bounds.size.height / self.quizSection.answers.count; //WRONG
}
- (void) updateConstraints {
[super updateConstraints];
self.answerTableView.rowHeight = self.answerTableView.bounds.size.height / self.quizSection.answers.count; //WRONG
}
#pragma mark -
#pragma mark custom getter/setter
- (void) setQuizSection:(QuizSection *)quizSection {
if (_quizSection == quizSection) {
return;
}
_quizSection = quizSection;
//Set data
self.questionLabel.text = _quizSection.question[KEY_TEXT];
self.answerTableView.delegate = self;
self.answerTableView.dataSource = self;
}
#pragma mark -
#pragma mark TableViewDelegate TableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.quizSection.answers.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:AnswerCellIdentifier];
NSDictionary * answerDict = self.quizSection.answers[indexPath.row];
cell.textLabel.text = answerDict[KEY_TEXT];
return cell;
}

Related

UIView's autolayout size reported from wrong size class the first time

I have a UITableViewController with some custom cells and an UIView within each cell. I have implemented the controller's cellForRowAtIndexPath to configure the UIView in each cell. For this purpose I need to know the width of the UIView on screen. Since I am using autolayout and size classes to automatically change the size of the UIView based on device orientation, I have implemented an additional method of getting the width runtime.
The problem is that when the table view is presented the first time, my code reports width for UIVIew from a compact width size class even when I am using the device in the landscape orientation. The system renders all the views as should, but my code to get the width is not working. Scrolling new cells visible or an orientation change will remedy the situation immediately.
My code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// ...
CustomCell* cell = [tableView dequeueReusableCellWithIdentifier:#"CustomCell"];
if (cell == nil) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"CustomCell" owner:self options:nil];
cell = [nib firstObject];
} else {
// Clear custom content
NSArray *viewsToRemove = [cell.histogramView subviews];
for (UIView *v in viewsToRemove) {
[v removeFromSuperview];
}
}
[cell setNeedsLayout];
[cell layoutIfNeeded];
int width = ((CustomView*)cell.customView).getWidth;
NSLog(#"width = %d", width);
// ...
}
And then:
#implementation CustomView
- (int)getWidth {
[self setNeedsLayout];
[self layoutIfNeeded];
int width = self.frame.size.width;
return width;
}
#end
Edited to add:
The problem seems to be that at when cellForRowAtIndexPath is called the first time tableview appears, autolayout has not occurred for the cell. Forcing it with [cell setNeedsLayout] and [cell layoutIfNeeded] right after creating the cell does not do the trick either.
It seems my problem root cause is a potential duplicate of How to know the width of an UITableViewCell when using auto layout? So the problem has to do with fact that when my CustomCell is loaded from a nib, it will have the default frame. Special tricks should be done to force autolayout. However, the accepted answer does not work for cells that are initially out of visible area. Any takers on this?
I would suggest trying to use constraints and ratios instead of actual width of the frame, since, as stated by Marcus Adams, the frame will have the value you are actually looking for only after viewDidAppear.
For example if your cells contain a UILabel that is 1/3 the width of the cell, and a UIImageView that fills the space left, you can set constraints between them and the parent view.
Can your logic be changed like this?
Another suggestion would be to call reloadData on your table view after viewDidAppear, but that is definitely not a nice option UX-wise.
I found out the correct solution to this:
Create a custom cell like in the original question.
Set cell.contentMode = UIViewContentModeRedraw in tableview: cellForRowAtIndexPath:
Implement layoutSubviews for the custom cell like so:
- (void)layoutSubviews {
[super layoutSubviews];
NSLog(#"cell width == %f", self.bounds.size.width);
}
This way you will have access to the size of the cell as it will appear on screen.

Multiple Scrollviews?

I don't really know how to explain what I want, so here's an picture :
I have a view with a lot of subviews (gray lines). Then the background (blue) is a picture (UIImageView + Blur effect), so I need it to stay and not to scroll. Behind the background, there's a view (orange). I want the picture (blue) to scroll only when the subviews (gray) are at the bottom (3rd picture).
Should I use embed scrollviews, or can I get this effect with only one UIScrollView ? If multiple scrollviews, does someone have an example ?
Thanks a lot for your help
Use only one scroll view, and use its delegate method 'scrollViewDidScroll' to read the content offset. Use this value to translate your background image.
It's not necessary to use 2 scroll views. But I would use it since I prefer setting contentOffset of the background scroll view than setting it's frame directly.
The idea is that you implement scrollViewDidScroll: delegate method for the front scroll view. And track the contentOffset to check whether the scrolling has reached the end of it's content by checking whether scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height or not.
If it has, then you offset the location of the background view with the amount of offset that exceed the content size.
Please see the code below. You can skip all the code and only look at scrollViewDidScroll:.
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.backgroundScrollView.userInteractionEnabled = NO;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 20;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
cell.textLabel.text = [NSString stringWithFormat:#"Item %ld", (long)indexPath.row];
return cell;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat backgroundVerticalOffset = MAX(scrollView.contentOffset.y + scrollView.bounds.size.height - scrollView.contentSize.height, 0);
CGPoint backgroundContentOffset = CGPointMake(0.0, backgroundVerticalOffset);
self.backgroundScrollView.contentOffset = backgroundContentOffset;
// If you decide not to use two scroll views, then please use backgroundContentOffset to set the backgroundView.frame.origin.y instead
// e.g. backgroundView.frame.origin.y = -backgroundContentOffset;
}
The result shown below:
P.S. I used blue background view instead of an image view and everything else with clear background color.

Nested UITableVIew heightForRowAtIndexPath

I'm trying to figure out how to optimize performance and improve code quality.
I have the following layout:
OuterTableView
Outer Cell
UILabel (title label)
InnerTableView
Inner Cell (item cell)
Inner Cell (item cell)
...
The purpose of this layout is expandable outer table. In collapse state only UILabel (only title label) is visible. When user clicks on outer cell, cell is expanded and reveals inner cells.
In outer table controller I have 3 functions (simplified):
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.tableView) {
// ServiceGroupCell - outer cell
ServiceGroupCell *serviceCell = [tableView dequeueReusableCellWithIdentifier:#"service_group_cell"];
[self configureCell:serviceCell forIndexPath:indexPath];
return serviceCell;
}
return nil;
}
- (void) configureCell:(ServiceGroupCell *) cell forIndexPath:(NSIndexPath *) indexPath {
// styling for outer cell and passing data for inner cells
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
ServiceGroupCell *serviceGroupCell = [tableView dequeueReusableCellWithIdentifier:#"service_group_cell"];
[self configureCell:serviceGroupCell forIndexPath:indexPath];
[serviceGroupCell layoutSubviews];
CGFloat height = [serviceGroupCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
return height;
}
Inner table controller
- (void)layoutSubviews {
[self.cellTableView layoutSubviews];
// set height constraint for inner table view
self.heightLayoutConstraint.constant = self.cellTableView.contentSize.height;
}
I have questions:
1) As you can see for each cell of outer table configureCell is invoked twice. Are there any ways to cache or just call it once?
2) systemLayoutSizeFittingSize returns 0. Are there any ideas why?
2) systemLayoutSizeFittingSize returns 0. Are there any ideas why?
Your constraints are probably missing. Make sure you specify constraints for the width and height of the cell in the .xib file.
Cells inner other cells is a bad idea, i think. You can make some sections, and using method didSelectRowAtIndexPath for when user tap on cell in section add cells to this section. Or use scrollview with custom nsview in which put tableview. Thats just work around.
Are there any ways to cache or just call it once? - try willDisplayCell method.

uitable view scroll to bottom issue after dismissing modal view controller

I'm having a problem with my table view scrolling to bottom. I tried to search stack overflow for a solution but nothing could help.
Here is what I have:
I have a table view that I'm loading extra cells into each time the user scrolls to the bottom of it. I did it as follows:
- (void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == ([self.tableView numberOfRowsInSection:0] - 1))
//load more data..
}
after the data gets loaded successfully through an internet connection, I'm calling [self.tableView reloadData] to refresh the table and increase its size.
Now here comes the problem:
When the user selects a cell, a details view controller is presented. I noticed that before presenting the details view, the table is quickly resized to its original size showing only the initial number of cells inside.
When the details view is dismissed, the table view comes back to its original size (without fitting the data loaded on willDisplayCell: forRowAtIndexPath:) and there is no way it could be scrolled down till the end of all data.
I checked all these points, but nothing helps:
the data in table view is reloaded by calling [self.tableView reloadData] in viewWillAppear method.
the delegate and dataSource of the table view are set correctly to the container view controller.
checked calling tableView: numberOfRowsInSection: when the view appears again, and it is returning the exact number of all the data I have, but the problem is in the table not fitting its size to my total data.
I also tried to resize the table frame as follows:
CGRect frame = self.tableView.frame;
frame.size.height = self.tableView.contentSize.height;
self.tableView.frame = frame;
But nothing changes.
Any help please??
Update:
I'm doing nothing unusual in didSelectRowAtIndexPath. The code is simply:
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
DetailsViewController *vc=[[DetailsViewController alloc]initWithNibName:#"DetailsViewController" bundle:nil];
[self presentViewController:vc animated:YES completion:nil];
}
take object of uiscrollview in uiview
take object of uitableview in uiscrollview
set the property scrollenabled = flase of uitableview
write the delegate method of uiscrollview
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView == scrollObj) {
CGFloat scrollPosition = scrollObj.contentSize.height - scrollObj.frame.size.height - scrollObj.contentOffset.y;
if (scrollPosition < 30) {
if ((!spinnerBottom.isAnimating) || intGetDataCallRunning == 0) {
//[spinnerBottom startAnimating];
//[self getLoginFollowingUserVideosCall];
//run WebService call and after that reload the table or if you have data exists then only reload the table
//and after reloading table follow step no 5.
}
}
}
}
after reload the table set height of table and scroll
tblVideo.frame = CGRectMake(0, 0, 320, tblVideo.contentSize.height);
scrollObj.contentSize = CGSizeMake(320,tblVideo.contentSize.height+60);

resizing a UITableViewCell frame issue

I am trying to resize my UITableViewCell's frame via:
[cell setFrame:CGRectMake(cell.frame.origin.x,
cell.frame.origin.y,
cell.frame.size.width,
cell.frame.size.height+25)];
however, it's not resizing after I do this... why is this?
This is weird as if I add a UIToolBar into the cell, it resizes but when I am adding a UIView, it doesn't:
[cell.contentView addSubview:sideSwipeView];
Here's the long and short of it:
Your cell width is determined by the width of the tableview it's in.
[EDIT: If it's a grouped table view, the cell is 20 - 60 pixels narrower than the tableview width, depending if you're using an iPhone, or an iPad.]
Your cell height is determined by the heightForRowAtIndexPath method.
If you're manually setting the cell's frame, it's going to be useless except when you're using a subclassed cell where you want to add subviews based on the cell's dimensions.
Even in this case, it's recommended to get the cell's frame from the tableview by using rectForRowAtIndexPath:(NSIndexPath*)indexPath method and then setting that frame as the cell's frame (after setting the frame's origin Y as 0).
I'm not quite sure about the UIToolBar, but your subview's frame won't change on changing the cell frame.
Maybe if you could tell us what you're trying to achieve, we can suggest a solution for you?
--------------------EDIT--------------------
So you need to dynamically add a subview to a cell on tapping it and resize it's height according to the new subview. This is gonna get hairy so here goes:
In your .h file declare:
BOOL subviewAdded;
In your .m file, in the init, do:
subviewAdded = NO;
Let's assume that you want the cell's height to be 50 without the subview and 100 with the subview. Accordingly, your heightForRow method should be:
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return (subviewAdded?100.0f:50.0f);
}
This means that initially since subviewAdded is NO, all your cells will have the smaller height.
Now, to add a subview to a cell on tapping it, and to change it's height dynamically, do this in your didSelectRow method:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
//Get the cell at this indexPath
UITableViewCell *thisCell = [tableView cellForRowAtIndexPath:indexPath];
if(subviewAdded)
{
subviewAdded = NO;
for(int i = 0; i < [thisCell.contentView.subviews count]; i++)
{
UIView *thisSubview = [thisCell.contentView.subviews objectAtIndex:i];
[thisSubview removeFromSuperview];
}
}
else
{
UIView *someView = [[UIView alloc] initWithFrame:someFrame];
[thisCell.contentView addSubview:someView];
[someView release];
subviewAdded = YES;
}
NSMutableArray *array = [NSMutableArray array];
[array addObject:indexPath];
[tableView reloadRowsAtIndexPaths:array
withRowAnimation:UITableViewRowAnimationFade];
}
So what's going to happen here is you're adding a subview to this cell you've tapped. Reloading this cell will call heightForRowAtIndexPath and do a nice little fade animation and change your tableview heights.
IMPORTANT: Ideally, you should maintain an array of NSNumbers with boolean values. The array size should be the same size as the number of tableview cells you have.
In heightForRow, you would then check against this array instead of using a single boolean for the whole tableView. This would ensure that you could have different heights for different cells.
That would look something like:
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
BOOL thisBool = (BOOL)[[booleanArray objectAtIndex:indexPath.row] boolValue];
return (thisBool?100.0f:50.0f);
}
I didn't post all that code here since it's implied and what I've posted should put you well on your way to doing the boolean array thing.
Anyway, there you are. I just tested this code myself so it works :)
If you want to increase the height of your cell based on some parameter eg. text, image,
you must implement the heightForRowAtIndexPath method of UITableViewDelegate in your code.
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath

Resources