Is layoutSubviews called whenever view's size is changed? - ios

In my impression, with autoresizesSubviews = YES, layoutSubviews should be called every time view's size is changed. But I found it is not the case for my view. Is my expectation wrong?

According to sources at Apple,
"-[UIView layoutSubviews] should get called when the size of the view changes."
They also referred me to this, from the the View Programming Guide for iOS:
"Whenever the size of a view changes, UIKit applies the autoresizing behaviors of that view’s subviews and then calls the layoutSubviews method of the view to let it make manual changes. You can implement the layoutSubviews method in custom views when the autoresizing behaviors by themselves do not yield the results you want."
At this point, your best move is to create a small sample project where layoutSubviews does not get called (or, send your existing project) file a bug with Apple using BugReporter, and include that sample project with your bug.

If you need something to happen when your view is resized, you can also override setBounds: and setFrame: for your class to make sure it happens. It would look something like this
-(void)setBounds:(GCRect newBounds) {
// let the UIKit do what it would normally do
[super setBounds:newBounds];
// set the flag to tell UIKit that you'd like your layoutSubviews called
[self setNeedsLayout];
}
-(void)setFrame:(CGRect newFrame) {
// let the UIKit do what it would normally do
[super setFrame:newFrame];
// set the flag to tell UIKit that you'd like your layoutSubviews called
[self setNeedsLayout];
}
The other reason that I sometimes override these methods (temporarily) is so I can stop in the debugger and see when they are getting called and by what code.

From my understanding, layoutSubviews is called when the view's bounds change. This means that if its position changes in its superview (but not its size) then layoutSubviews won't be changed (since the origin point in the bounds is in the view's coordinate system - so it is almost always 0,0). In short, only a change in size will cause this to be fired.

whenever you want to resize the views manually and resizes automatically call layoutSubViews method
-(void)layoutSubviews
{
[super layoutSubviews];
CGRect contentRect = self.contentView.bounds;
CGFloat boundsX = contentRect.origin.x;
CGRect frame,itemlabelframe,statuslabelframe;
frame= CGRectMake(boundsX+1 ,0, 97, 50);
itemlabelframe=CGRectMake(boundsX+100, 0, 155, 50);
statuslabelframe=CGRectMake(boundsX+257, 0, 50, 50);
ItemDescButton.frame=itemlabelframe;
priorityButton.frame = frame;
statusButton.frame=statuslabelframe;
// ItemDescLabel.frame=itemlabelframe;
// statusLabel.frame=statuslabelframe;
}

Related

Strange behaviour with autolayout and frame changing with iOS 9

I'm trying to create a view controller to simulate a classic weighing scale. I have a UIView subclass (DragView) to represent the weights, and a another UIView subclass (ContainerView) to simulate the plates os the scale.
When a DragView is drag over the ContainerView, I trigger an animation to place the DragView inside the ContainerView (changing the size if is necessary). But, if the user releases the DragView outside the ContainerView, then the DragView is animated to its original position and size.
Here you can see the DragView (in green) and two ContainerView (in clear Color above the "plates")
The original frame of the DragView is set with constraints (proportional width, top and leading). Everything looks fine but when I animate the DragView back to his original position, then I've got this.
See the difference in the DragView's frame?. Why is this happening?
Here are the relevant parts of my code.
DragView.m
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
_originalFrame = self.frame;
}
return self;
}
- (void)animateBackToOrigin
{
[UIView animateWithDuration:0.1 animations:^{
self.frame = _originalFrame;
}];
}
I've checked the _originalFrame values in both methods and it returned the same values.
ANSWER:
My mistake was setting the _originalFrame within initWithCoder, layoutSubViews is the right place. Because layoutSubViews is called every time the view is set, I added a check (with CGRectIsEmpty) in order to set the frame only if there is no value.
- (void)layoutSubviews
{
if (CGRectIsEmpty(_originalFrame)) {
_originalFrame = self.frame;
}
}
It is to early in initWithCoder: to take resulting frame. The view is just instantiated and not processed through layout process. I think, the best place is layoutSubviews method.
When autolayout is present bad things will happen if you mess with frame.
Try instead of changing the full frame, change the .origin of the object

How to programmatically change UIView's frame inside an UIView Subclass?

I have a UICustomButton(subclass of UIButton) in the interface builder. I want change this button's frame inside UICustomButton.
I tried following code:
// make it 10 points wider and higher
CGRect newFrame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width + 10, self.frame.size.height + 10);
self.frame = newFrame;
in awakeFromNib:, drawRect:
none of them worked, the result is unchanged.
Don't do that! That's bad architecture!
A view should never change it's own frame. It's always the parent view resizing its children.
If you detect you need another frame inside your view: send out a delegate call to the parent view and change the frame in there. the delegate method could look like this
- customButton:(UIButton *)button requestNewFrame:(CGRect)frame
{
button.frame = newFrame;
// some storing method so you remember the frame on rotations and stuff
}
Just as a reminder: child views are always resized in
- (void)layoutSubviews //UIView
- (void)viewDidLayoutSubviews //UIViewController
That makes your code safe for different UIInterfaceOrientations and different devices.
Neither awakeFromNib nor drawRect: is appropriate.
You can change its frame in layoutSubviews, but you might run into trouble if your app uses auto layout.
I'd put it in viewDidLoad. If you put it in a recurring method, it will increase in size each time it's called.

Is there an iOS method that fires when Autolayout has completed?

I have an iOS app in which I need to know when a new view is completely visible on-screen; that is, when Autolayout has finished its calculations and the view has finished drawing.
ViewDidAppear seems to fire well before the view is completely visible. If I turn off Autolayout, the timing seems to line up as far as human perception goes, but I need to use Autolayout in this project (so this isn't a solution...just a test).
Is there any method that fires when Autolayout is done calculating? Or another method that fires when the view is ACTUALLY visible (since ViewDidAppear doesn't work for this)?
Thanks!
The following can be used to avoid multiple calls:
- (void) didFinishAutoLayout {
// Do some stuff here.
NSLog(#"didFinishAutoLayout");
}
and
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:#selector(didFinishAutoLayout)
object:nil];
[self performSelector:#selector(didFinishAutoLayout) withObject:nil
afterDelay:0];
}
I'm using viewDidLayoutSubviews for this. Apple's documentation says, "Called to notify the view controller that its view has just laid out its subviews."
If you watched 2018's WWDC about "High-Performance AutoLayout", you would know the answer to this question.
Technically, there is no such API method that will be called when autolayout has completed your view's layout. But when autolayout has completed the calculations, your view's setBounds and setCenter will be called so that your view gets its size and position.
After this, your view's layoutSubviews will be called. So, layoutSubviews can, to some degree, be thought of as the method that fires after autolayout has done calculations.
As to view controller's viewDidLayoutSubviews, this is a bit complicated. The documentation says:
When the bounds change for a view controller's view, the view adjusts the positions of its subviews and then the system calls this method. However, this method being called does not indicate that the individual layouts of the view's subviews have been adjusted. Each subview is responsible for adjusting its own layout.
So when viewDidLayoutSubviews called on a view controller, only the view controller'view 's first-level subviews are guaranteed to be laid out correctly.
What it worked in my case was request layout after changed a constraint value:
self.cnsTableviewHeight.constant = 50;
[self layoutIfNeeded];
Later on override layoutSubviews method:
- (void) layoutSubviews { //This method when auto layout engine finishes
}
You can call setNeedsLayout also instead of layoutIfNeeded
I guess implementing viewDidLayoutSubviews is the correct way but I used an animation just to write the completion callback inside the same method.
someConstraint.constant = 100; // the change
// Animate just to make sure the constraint change is fully applied
[UIView animateWithDuration:0.1f animations:^{
[self.view setNeedsLayout];
} completion:^(BOOL finished) {
// Here do whatever you need to do after constraint change
}];
You might face this problem not just with UIViewControllers but also UIViews. If you have a subview and want to know if AutoLayout has updated it's bounds, here is the Swift 5 implementation,
var viewBounds: CGFloat = 0.0
var autoLayoutHasCompleted: Bool = false
override func awakeFromNib() {
super.awakeFromNib()
// someSubView is the name of a view you want to check has changed
viewBounds = someSubView.bounds.width
}
override func layoutSubviews() {
if viewBounds != someSubView.bounds.width && !autoLayoutHasCompleted {
// Place your code here
autoLayoutHasCompleted = true
}
}

Incorrect size of label on viewWillAppear

I have following setup
XIB file which has only landscape view. This view is connection to my controller
There is a label on this view which is connected to IBOutlet UILabel* label
This label is configured like this (it occupies the whole width of screen).
I overrided viewWillAppear and do this (to get the size of label).
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGRect rect = _labelTitleLand.frame;
}
The strange thing (which I don't understand). That it returns size = (width 768, height 21) when it launched in portrait (on iPad), which is correct.
And it returns size = (width 741 height 21) when it's launched in landscape. Which is weird. I anticipated that it will return width 1024, height 21 for landscape.
I was under impression that at the moment of viewWillAppear, all controls sizes are calculated already.
Update 1
If I check labelTitleLand.frame on viewDidAppear then it returns correct results. However, I don't like this, because I want to do some actions (based on this size) which influence how view will be drawn. In the case, if I will do it on viewDidAppear, as I understand there will be visible redrawing.
The layout of the view hierarchy has to be complete before you will get the actual final frames.
So you should check the frame in viewDidLayoutSubviews, which will still be before the view hierarchy is actually drawn. If you need to make changes here you can without causing any redrawing to occur.
viewWillAppear is too early because this is before your autoresizing masks (and/or autolayout constraints) have had their effect.
This seems a problem related to when a method is actually called at runtime.
I solved similar situation using
[self performSelector:#selector(checkMethod) withObject:nil afterDelay:0.1];
in viewWillAppear, and then:
- (void)checkMethod
{
rect = _labelTitleLand.frame;
}
This gives your app the time needed to set its own frame.
It is not so elegant and it looks like a workaround, but it is very effective.
You can also try to force the frame of the UIView that is container of the UILabel in viewWillAppear like this:
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.view.frame = CGRectMake(0.0f, 0.0f, 1024.0f, 768.0f);
CGRect rect = _labelTitleLand.frame;
}
But the first solution is more reliable and usually no lag is experienced.

Best way to redraw a custom view when orientation changes

I have a custom view:
-(id)initWithFrame:(CGRect)frame
{
CGRect frameRect = CGRectMake(0, NAVIGATION_BAR_HEIGHT , frame.size.width, 4 * ROW_HEIGHT + NAVIGATION_BAR_HEIGHT + MESSAGE_BODY_PADDING);
self = [super initWithFrame:frameRect];
if (self) {
_selectionViewWidth = &frame.size.width;
[self initView];
}
return self;
}
-(void)initView
{
CGRect sectionSize = CGRectMake(0, 0 , *(_selectionViewWidth), ROW_HEIGHT * 4);
_selectionView = [[UIView alloc] initWithFrame:sectionSize];
[_selectionView setBackgroundColor:[UIColor clearColor]];
That I use in a View Controller the next way:
_mailAttributesView = [[MailAttributesView alloc]initWithFrame:self.view.frame];
_mailAttributesView.delegate = self;
[self.view addSubview:_mailAttributesView];
So when orientation changes from P to L I have the next problem:
What's the best way to get orientation change callback and redraw my custom view?
You likely need to override your UIView layoutSubviews method and proceed to manually layout your subviews (looks like to/from/cc/subject controls) there.
Or, you could better configure your subview spring/struts (or autolayout constraints) for automatic layout. You could do this in code or via a nib or storyboard.
EDIT: additional info since you seem not to be getting layoutSubviews on orientation change.
My guess is that the viewcontroller-view isn't resizing/repositioning your MailAttributes view either.
It's also not clear when/where you add your MailAttributesView to the veiwcontroller view. If you're doing it in viewDidLoad your viewcontroller view may or may not have a valid frame size (depending if it was loaded from a nib or not). It's best not to depend on the viewcontroller-view frame for layout purposes in viewDidLoad.
Rather, layout any viewcontroller-view subviews in viewWillLayoutSubviews. There your viewcontroller-view frame will be set.
Others may point out that you can set your autoresizingFlags in viewDidLoad for any subviews, but there are gotcha's with this. Primarily if the parent view has zero size, and your subviews are to be inset but have springs/struts defined to glue them to the parent view edges.
The best solution overall IMO is to setup autolayout constraints for everything contained in your viewcontroller view, on down.

Resources