I'm having layout issues with my horizontal UIStackView in a custom UITableViewCell when showing/hiding arrangedSubviews when the horizontalSizeClass changes.
My stack view contains a number of subviews, each of which, depending on cell configuration and size class, are either hidden or shown. The UIStackView is designed to handle arranging the shown views, but upon rotation, layout issues arise.
Issues:
Sometimes, the appropriate subviews are either not shown or not hidden when they ought to be.
Sometimes, the subviews are inappropriately laid out, not filling the stack view's width.
Attempts:
I've attempted a number of things to address the layout:
Overriding viewWillTransitionToSize:transitionCoordinator to reload the table and/or force layout
Overriding viewWillTransitionToTraitCollection:withTransitionCoordinator to reload the table and/or force layout
Overriding layoutSubviews to re-configure the stack view's arrangedSubviews
Calling [self setNeedsLayout], [self layoutIfNeeded] after configuring the cell
Forcing layout in other places
Changing subview layout constraint priorities to 999
Limiting the UILabels to 1 line, and setting a preferredMaxLayoutWidth
Adjusting the contentCompressionResistance and contentHuggingPriority on views
Using a static value for rowHeight instead of UITableViewAutomaticDimension
Etc.
Nothing appears to fix the issues.
Furthermore, even when cells are scrolled off/on screen, prepared for reuse, and re-configured, issues either persist, go away, or new issues are introduced, despite the fact that I'm properly resetting the cell on prepareForReuse.
Sample Project
I've created a sample project to illustrate the layout issues. At this point, I'm unsure if UIStackView is buggy or if I'm misusing it.
Sample Project: https://github.com/bradgmueller/StackViewTest
The sample project uses a custom UITableViewCell with views configured in the xib. Row objects are generated with different configurations to illustrate the dynamic layouts the cell should adopt:
Indented / not indented
Showing separator or not
Showing / hiding a "like" button
Showing / hiding a "Share" button
Showing / hiding an Info button, where one info button exists for UIUserInterfaceSizeClassCompact and another for UIUserInterfaceSizeClassRegular
A text label exists with text indicating which of the views ought to be displayed, to help illustrate when views are inappropriately shown/hidden. Also, a red background view exists behind the UIStackView to illustrate when the stack view is failing to Fill the width.
Screenshots:
Initial layout - no issues
After rotating - issues marked with red "X"
I'd appreciate any insight, thanks in advance!!
Generally stack views are pretty good, but when they get more complicated and manage more views, they appear to be less reliable.
I found that the best way to avoid the layout issues is to do one or two things:
Use multiple stack views. Instead of bunching all of my views into the same stack view, having multiple smaller stack views seems to be more reliable, as each is managing fewer views.
Remove / Re-insert views, rather than hide them. Though stack views are supposed to treat "hidden" views as if they are removed from the hierarchy, I got much better layout results when I actually removed them from the hierarchy.
Ex:
More reliable layout results:
if (hideSubview == YES) {
[subview removeFromSuperview];
} else {
[stackView insertArrangedSubview:subview atIndex:0];
}
Not as reliable:
subview.hidden = hideSubview;
Related
My application gathers input from users and hence it is full of Labels, text boxes and buttons and I have to show or hide set of labels and text boxes based on certain conditions.
To accomplish it, I did the following.
Set fixed height (lets say 30) for all the controls
Set height constraint on each of the controls and created an outlet to the height constraint in the ViewController
Alter the heightConstraint.constant value programatically (between 0.0 and 30.0) based on the scenarios.
Having programmed like this, it is very difficult for me if there is any change in layout. (i.e., if user requested to add/remove any particular control from the view Controller).
I am using Auto Layout constraints. Could anyone suggest if there is a better way to accomplish my goal.
I am using IOS9 and Swift.
Many thanks in advance.
You can use UITableViewController with static cells for this case.
For hide/show row or section, you can change the size in the tableView:heightForRowAtIndexPath method.
UITableViewController automatically manages the layout and with static cell you can even create outlet for all the controls.
Have you considered using a table for this? It has mechanisms for inserting and deleting rows and would manage the layouting for you - only part you'd need to care about are the contents of the cells.
Instead of making IBOutlets to the height constraints of all the views that you might need to hide, you can just use the hidden property of UIViews to hide/show the view.
In case you need the view to make space for other views, you could set a simple animation and move the view out of screen bounds. You might face issues with layout constraints but it's surely worth the effort from a UI/UX perspective.
Additionally, if you know that you don't need a view any more you can even remove the view from it's superview.
This may sound like a silly question, but most of my experience with iOS development has revolved around Auto Layout being prominent, and I'm curious now (for performance purposes) how I'd go about laying out a cell without Auto Layout and using only frames (and perhaps auto-resizing masks). Turns out this is very hard to Google due to the prominence of Auto Layout.
Essentially am I setting up and adding the subviews in the initWithStyle method of the UITableViewCell subclass?
Positioning wise, am I just relying on the bounds of the contentView, and then if I want one view beside another view, would I basically do newView.frame.origin.x = CGRectGetMaxX(otherView.frame) + spacing?
What happens when I rotate? I know I can watch for rotation in viewWillTransitionToSize, but how do I go about re-positioning the cells? Simply calling tableView.reloadData() would be both expensive and not do much as the cells are laid out in initWithStyle, correct?
I'm targeting iOS 8+.
Any insight would be truly appreciated.
Actually its
newView.frame.origin.x = CGRectGetMaxX(otherView.frame) + spacing + leftMargin;
// you have to include all the spacing, including margins
Tip:
Do not rely on bounds of the contentView by default it is set to maximum width of 320, i suggest you use main screen's frame for that.
AutoLayout is our friend and can save us a lot of time.
how do I go about re-positioning the cells?
If you're planning to do it programmatically, you need to setup the new height and width of the view after the rotation.
Calling tableView.reloadData() to update the views in the cell is not expensive, that's how it works. We don't have a choice but to live with it.
Then it will be better to write the frame calculation portion in viewDidLayoutSubViews.
I'm not entirely certain I understand your question. Please let me know if this makes sense:
Adding Subviews
To add subviews programmatically, it's actually best to do so in the override of layoutSubviews by checking if the contentView has subviews already added and calling a method to add them if needed. This way, you can load the table cell via either a storyboard/nib or using initWithStyle. Alternatively, you can use a shared commonInit method that you call in both initWithCoder and initWithStyle.
Layout of Subviews
To layout your subviews programmatically, you override layoutSubviews (and remember to call super) and set up the frames in that method. This method will be called whenever the view changes size (rotation, initial presentation, etc) and will always include the current bounds of the content view. To calculate your subview frames, you can do so as you've suggested, but you need to define the subview frame and then set the frame for the view:
frameB.origin.x = CGRectGetMaxX(viewA.frame) + spacing;
viewB.frame = frameB;
And keep in mind that this will not correct for the width of frameB. Therefore, you might want to consider using CGRectDivide() instead.
Scrolling performance
That said, the big performance hit when using auto layout in table view cells is not auto layout itself, but the calculations that are done many times on the same set of data to lay it out - all on the main thread. Such as calculating frames for all the text to layout a bunch of labels relative to one another or something that uses drawRect rather than drawing to an image bitmap on a background thread and then loading the image in to a UIImageView once the drawing is complete. Without knowing what data you are displaying it is hard to guess what is causing the performance issues, but you should be sure to use profiling to determine if you are actually improving performance. Additionally, you may need to consider moving some of the number crunching and/or image rendering onto a background thread.
Good luck!
We're currently having a problem that only seems to affect iOS7 devices.
Within our .xib file we have two views within a container view (i.e.: not at the top level of the view hierarchy) that need to be circular on display. The views have constraints applied to their position and horizontal spacing within the container, and an aspect ratio condition requiring they are square. The views should expand in width/height on larger screen sizes, respecting the constraints described.
In our VC, we have the following in viewDidLayoutSubviews to force these views to appear circular:
- (void)viewDidLayoutSubviews {
self.progressContentContainerView.layer.cornerRadius = self.progressContentContainerView.frame.size.width/2;
}
This seems to work fine on iOS8, however on iOS7 there is a period after the view has been displayed where the constraints have not yet been applied and the size of the view/views is incorrect (see attached screenshots). This resolves itself and correctly renders a circle after half a second. This only appears to happen when the views that we intend to be circular are NOT at the top level of the VC's view hierarchy which seems to imply that viewDidLayoutSubviews is called before the subviews of subviews have also been laid out.
My guess is that we could potentially fix this issue by subclassing UIView for the nested container, adding references to the circular view within this subclass and overriding viewDidLayoutSubviews here to make the cornerRadius adjustment. This seems like a bit of a workaround though and I'm interested to see if there are other options.
Is there a cleaner/more idiomatic solution to this problem?
I know this is an old question but have you tried calling either:
[self.progressContentContainerView setNeedsUpdateConstraints];
or:
[self.progressContentContainerView layoutIfNeeded];
I'm building something similar to the compose page of the native iPhone Mail app.
But, I'm putting two text fields on one row, and I want to separate them with a vertical divider (an additional view) that's the same color & weight as the horizontal cell separators.
Apple's docs say:
If you want to customize cells by simply adding additional views, you should add them to the content view so they will be positioned appropriately as the cell transitions into and out of editing mode.
But, what if I know, like in this case, that my cell will never go into editing mode?
Also, the horizontal cell separators are subviews of the cell, not the cell's content view. So, I think it'd also make sense if I added the vertical divider to the cell, not the cell's content view.
Can I just add additional views to the cell itself, instead of its content view?
You can add additional views directly to the cell, but it's best practice to add them to the cell's content view. Even if you're cells never go into editing mode, this is what Apple and other developers that may be working on your code (or you in the future) are expecting. So, this will make it easier for them to work with your app.
For example, what if Apple hypothetically decided to put gutters on the left & right side of plain style table views in a future release of iOS? Then, they would probably inset the cell's content view but not the cell itself. That way, if you position your additional views relative to the content view, your app has a better chance of looking good and the additional views have a better chance of not getting clipped by Apple's hypothetical gutters. OK, I doubt Apple would do such a thing, but the point is that if you add your additional views to the cell's content view instead of the cell itself, you're code will be more robust.
For your specific case, I still recommend adding the vertical divider to the cell's content view because you can. You might even consider setting the tableView.separatorStyle = UITableViewCellSeparatorStyleNone and redrawing custom separators by adding a separator view to the bottom of each of your cells' content views. This way, you can be sure that the style of the vertical divider you add will always match that of the separators. Again, this will make your code more robust and protect you if Apple decides to change the style of their separators. Sure, you can use cell.separatorColor. And, you can guess the separator weight with cell.frame.size.height - cell.contentView.frame.size.height. But, such cleverness might get you in trouble.
As the Buddha admonishes Siddhartha, "Beware of too much cleverness!"
I'm working on project targeted for iOS 6 that leverages storyboards and auto layout. In the storyboard there are many places where a UITableView is added as a subview to a view controllers view. This table view uses prototype cells from the storyboard.
The issue we're running into is that if the view controller is initially loaded in landscape orientation and the device is then rotated to portrait, the table view begins to scroll both vertically and horizontally. The table views cells are drawn with the correct dimensions but there is additional white space to the right.
It appears that while the frame and bounds of the table view are being updated to the correct size on rotation, the table views content size is not. Regardless of any update rotation change the content size remains the same dimensions.
The issue doesn't present itself if programatic table view cells are used.
A few garish work arounds I've found, 1.) calling reloadData or reloadRowsAtIndexPaths:withRowAnimation: 2.) manually setting the property contentSize.
Both of these seem less than ideal.
I've added this
link to a dead simple sample project which demonstrates this issue. The only changes made are to the storyboard and the main view controllers implementation.
Before rotation
After rotation
I'm having the same issue - can't seems to find any documented answer related to this. I ended up manually modifying the UITableView contentSize like you mentioned in:
- (void) viewWillLayoutSubviews
{
self.tableView.contentSize = CGSizeMake(self.tableView.frame.size.height, self.tableView.contentSize.height);
}
I ran into this issue today and filed a bug report with Apple.
Appears that if you are using a custom cell with a UI element AND autoLayout, the UIScrollView content size is having problems.
If you remove all UI elements, OR turn off autoLayout, OR use a factory cell (basic, etc), all works fine.
Same issue I have rectified in my project.
I guess this is a bug in Storyboard.
Then I have solved it by manual coding in willAutorotate method by setting
tableview.contentsize = CGSizeMake(tableview.width, tableview.contentsize.height);
Hope this will work for you as well.
If you find any apple documentation regarding the same then please update me as well. Till then you can use the same solution.
Appears that if you are using a custom cell with a UI element AND autoLayout, the UIScrollView content size is having problems.
I had to turn off AutoLayout for my custom UITableViewCells to be able to scroll to the bottom on updating the data and then [self.tableView reloadData].
With AutoLayout turned on, the tableView.contentSize was being updated, but I still wasn't able to scroll to the bottom unless I rotated the device.
I found the following to work for me:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
dispatch_async(dispatch_get_main_queue(), ^{
self.tableView.contentSize = CGSizeMake(self.tableView.frame.size.width, self.tableView.contentSize.height);
});
}
Notice the async dispatch: if that line would be executed synchronously then the contentSize change would trigger another layout pass before the current one would have completed. This triggers an exception:
Auto Layout still required after sending -viewDidLayoutSubviews to the
view controller.
Usage of Constraints helped me. Since you are using Storyboards, it is really easy to set Constraint values for all edges, so UITableView will always fill the whole ViewController (of course if UITableView fills whole ViewController) regardless of device orientation.
I had the same problem.
I found this link. When I tried to implement this I did not find the Auto-sizing attributes for my view then I clicked on Master View Controller and then clicked on the File Inspector and uncheck Use Autolayout and then go to Attributes inspector auto-resizing should be there then you can change the attributes how you want it.
I am sure you must have managed to figure this out.