UIView created in IB has no frame in viewDidLoad and awakeFromNib - ios

I'm sorry if I missed something here, but I thought UIView objects that were created in IB should have their frames created in viewDidLoad so you can do initial setup based off of this view in viewDidLoad or awakeFromNib or viewWillAppear. I logged the output in each method:
NSLog(#"%# %s", NSStringFromCGRect(self.zoomView.frame), __FUNCTION__);
And in all I get {0, 0, 0, 0}.
This is the first nib in my UIStoryboard, and I'm using Autolayout and iOS 6. I could have sworn on previous apps I have used the frame of other UIView objects created in IB to set things up. Is there something that has changed? Or do I just remember it incorrectly? Thanks!

I had the exact same issue. Yes, you remember correctly - it used to be different in iOS 5. I always set up my views in viewDidLoad: and the frame was already the way it was going to be when the view was actually on screen.
Now in iOS 6, you'll need to put your code into viewDidAppear: to have a valid frame to work with, if auto layout is enabled. Apparently the laying-out is done in between those two calls.

I think it has to do with the device you are using and possibly other things but the frames and bounds "could" sometimes be set by the time viewDidLoad is invoked and sometimes not (depends on ....) On the other hand, frames and bounds are guaranteed to have been set by the time "ViewDidAppear" has been called. And you want to put your code related to frames and bounds where it is guaranteed!

Related

When can you call UIEdgeInsets safeAreaInsets?

I need to adjust my views for iPhone X but I can’t figure out when the safeAreaInsets are initialized. According to the documentation,
If the view is not currently installed in a view hierarchy, or is not
yet visible onscreen, the edge insets in this property are 0.
I would think that when viewDidLoad is called, that the values would be set, but that is not the case. I can get values when viewDidLayoutSubviews is called, but that seems to be too late and doesn’t return the correct values anyway.
Can anyone explain how to use the safeAreaInsets property to me?
They are initialized before view is layouted (hence within view hierarchy). Best place to access them and act accordingly is within viewWillLayoutSubviews method. As you have already mentioned viewDidLayoutSubviews is little too late, but willLayoutSubviews works just fine. Happy coding!

When can I activate/deactivate layout constraints?

I've set up multiple sets of constraints in IB, and I'd like to programmatically toggle between them depending on some state. There's a constraintsA outlet collection all of which are marked as installed from IB, and a constraintsB outlet collection all of which are uninstalled in IB.
I can programmatically toggle between the two sets like so:
NSLayoutConstraint.deactivateConstraints(constraintsA)
NSLayoutConstraint.activateConstraints(constraintsB)
But... I can't figure out when to do that. It seems like I should be able to do that once in viewDidLoad, but I can't get that to work. I've tried calling view.updateConstraints() and view.layoutSubviews() after setting the constraints, but to no avail.
I did find that if I set the constraints in viewDidLayoutSubviews everything works as expected. I guess I'd like to know two things...
Why am I getting this behavior?
Is it possible to activate/deactivate constraints from viewDidLoad?
I activate and deactivate NSLayoutConstraints in viewDidLoad, and I do not have any problems with it. So it does work. There must be a difference in setup between your app and mine :-)
I'll just describe my setup - maybe it can give you a lead:
I set up #IBOutlets for all the constraints that I need to activate/deactivate.
In the ViewController, I save the constraints into class properties that are not weak. The reason for this is that I found that after deactivating a constraint, I could not reactivate it - it was nil. So, it seems to be deleted when deactivated.
I do not use NSLayoutConstraint.deactivate/activate like you do, I use constraint.active = YES/NO instead.
After setting the constraints, I call view.layoutIfNeeded().
Maybe you could check your #properties, replace weak with strong.
Sometimes it because active = NO set self.yourConstraint = nil, so that you couldn't use self.yourConstraint again.
override func viewDidLayoutSubviews() {
// do it here, after constraints have been materialized
}
I believe the problem you are experiencing is due to constraints not being added to their views until AFTER viewDidLoad() is called. You have a number of options:
A) You can connect your layout constraints to an IBOutlet and access them in your code by these references. Since the outlets are connected before viewDidLoad() kicks off, the constraints should be accessible and you can continue to activate and deactivate them there.
B) If you wish to use UIView's constraints() function to access the various constraints you must wait for viewDidLayoutSubviews() to kick off and do it there, since that is the first point after creating a view controller from a nib that it will have any installed constraints. Don't forget to call layoutIfNeeded() when you're done. This does have the disadvantage that the layout pass will be performed twice if there are any changes to apply and you must ensure that there is no possibility that an infinite loop will be triggered.
A quick word of warning: disabled constraints are NOT returned by the constraints() method! This means if you DO disable a constraint with the intention of turning it back on again later you will need to keep a reference to it.
C) You can forget about the storyboard approach and add your constraints manually instead. Since you're doing this in viewDidLoad() I assume that the intention is to only do it once for the full lifetime of the object rather than changing the layout on the fly, so this ought to be an acceptable method.
You can also adjust the priority property to "enable" and "disable" them (750 value to enable and 250 to disable for example). For some reason changing the active BOOL didn't had any effect on my UI. No need for layoutIfNeeded and can be set and changed at viewDidLoad or any time after that.
The proper time to deactivate unused constraints:
-(void)viewWillLayoutSubviews{
[super viewWillLayoutSubviews];
self.myLittleConstraint.active = NO;
}
Keep in mind that viewWillLayoutSubviews could be called multiple times, so no heavy calculations here, okay?
Note: if you want to reactive some of the constraints later, then always store strong reference to them.
When a view is being created the following life cycle methods are called in order:
loadView
viewDidLoad
viewWillAppear
viewWillLayoutSubviews
viewDidLayoutSubviews
viewDidAppear
Now to your questions.
Why am I getting this behavior?
Answer: Because when you try to set the constraints on the views in viewDidLoad the view does not have its bounds, hence constraints cannot be set. It's only after viewDidLayoutSubviews that the view's bounds are finalized.
Is it possible to activate/deactivate constraints from viewDidLoad?
Answer: No. Reason explained above.
I have found as long as you set up the constraints per normal in the override of - (void)updateConstraints (objective c), with a strong reference for the initiality used active and un-active constraints. And elsewhere in the view cycle deactivate and/or activate what you need, then calling layoutIfNeeded, you should have no issues.
The main thing is not to constantly reuse the override of updateConstraints and to separate the activations of the constraints, as long as you call updateConstraints after your first initialization and layout. It does seem to matter after that where in the view cycle.

updateViewConstraints called when creating constraints in viewDidLoad

Currently I'm experiencing a strange behavior. I removed the auto resizing mask from a view for viewForHeaderInSection. When I'm doing this the app crashes because I tried to remove a constraint which is nil.
I'm creating the concerned constraints in viewDidLoad in my parent controller. In debug mode I found out that when the system tries to create a constraint where my child view controller is involved (which has the removed auto resizing mask from the section header view) it directly jumps to updateViewConstraints. Of course the constraints are nil because the weren't created yet.
If I add again the auto resizing mask the app works, but I can't do what I'm trying to do (to layout my views correctly).
If I create my constraints in updateViewConstraints the app also works.
I don't understand why this is happening. On a similar view controller it is working without problems. Sometimes I think auto layout is more a pain than a gain. On a server error an alert was displayed. Here the view could be correctly loaded. Seems that this is a kind of timing problem.
I want to know why this is happening and how should I proceed in future that such an error doesn't happen anymore. Am I doing something wrong?
Edit:
Don't know if it helps but if I call setNeedsLayout and layoutIfNeeded on the view of my child view controller in viewDidLoad of my parent then also the app crashes.
Edit 2:
Seems that it occurs when I add multiple views with constraints on different places to my view controller. For my table I add an empty message if there are currently no entries. If I don't add the label as subview to my table everything works fine.
So when I'm allowed to add my constraints? Currently I add them right after the view was added as subview. For the empty message it is in viewDidLoad and for the section header it is viewForHeaderInSection. Do I have to use something like setNeedsLayout?
Edit 3:
Adding a subview to the table view isn't a good idea at all (especially when using auto layout). For now I'm using the background view, but that's not the solution I'm looking for.
You do not need to change the value of translatesAutoresizingMaskIntoConstraints unless you actually have set the autoresizingMask to something other than 0. If you are using constraints, the solution is to completely ignore autoresizingMask and translatesAutoresizingMaskIntoConstraints.
Now I got the same problem again. My workaround didn't worked:
// on iOS 7 this would bring "Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super." and the app crashes
// on iOS 8 this is needed, otherwise "Unable to simultaneously satisfy constraints."
// bug?
if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) {
sectionHeader.TranslatesAutoresizingMaskIntoConstraints = false;
}
What I've found out so far is that it has to do with the headerview and the empty message (view). When both are used I get this problem. Either the app crashes or I get
Unable to simultaneously satisfy constraints
depending if I have the autoresizing mask turned off or not. Interestingly, only iOS 8 made this problem with my workaround. A solution would be to set a boolean variable in viewDidLoad if the constraints have been set up and check for this in updateViewConstraints. But I wanted to know the real cause for this and not using a workaround for a workaround ...
Every time I retrieve new data for my table I checked if the number of records is zero or not. If they were zero I showed my empty message. The problem seems to be caused by the reloadData, which I called before this check. Than UITableViewHeaderFooterContentView got a height of zero from the NSAutoresizingMaskLayoutConstraint. Restructuring my code to this variant
if (myList.Count == 0) {
this.TableView.BackgroundView = emptyMessage;
this.TableView.SeparatorStyle = UITableViewCellSeparatorStyle.None;
} else {
this.TableView.BackgroundView = null;
this.TableView.SeparatorStyle = UITableViewCellSeparatorStyle.SingleLine;
if (emptyMessage != null) {
emptyMessage.RemoveFromSuperview ();
}
}
// call this afterwards!
TableView.ReloadData();
seems to solve my issue.

iOS - Where to initialize views

If I want to initialize views programmatically, where in the viewcontroller lifecycle should this happen?
The initial intuition is loadView. However, here, we don't yet have the frame of the view itself (necessary for calculating the sizes/positions of the views). Ditto for viewDidLoad.
Next intuition is viewWillAppear- here we DO (finally) have a guarantee of the frame of the view. However, this has potential to be called many times throughout the vc lifecycle. Ditto for viewDidAppear, etc...
Finally, I found viewWillLayoutSubviews. This works for the initialization of most static layouts- however, whenever any view moves this gets called again (same problem as viewWillAppear).
I've seen recommendations to init the views in loadView and set their frames in viewWillLayoutSubviews (since setting frames should be idempotent, who cares if it gets called a couple times). But then why does apple so strongly encourage initWithFrame: as the standard initialization method of UIViews (https://developer.apple.com/library/ios/documentation/windowsviews/conceptual/viewpg_iphoneos/CreatingViews/CreatingViews.html)?
Would it be crazy to subclass all my UIViewControllers to have an initWithViewFrame: method? That way I can pass in a frame, manually set it immediately in loadView and be done with it? Or is it better to have a viewHasBeenFormatted flag in viewWillAppear that, if not set, calls the formatting of views and then sets it?
Or is this just apple's way of saying "use interface builder or you're screwed"?
Any help is appreciated!
edit- accidentally wrote loadView where I meant viewWillAppear (in final paragraph)
update- I guess I've come to terms with the fact that there is no place where
The frame is confidently known
The code will only be run once (on setup)
Looks like you're expected to initWithFrame: all your views in viewDidLoad (but then I guess the contents of that view shouldn't treat that frame as even remotely final? because how could it be when it was derived on an assumption? ugh...). Then re-set their frames in layoutSubviews. And make sure to manually handle the differences between initial layout and layout as a result of a moved view there... Man I feel like I've GOT to be missing something... (lol denial...)
I guess that, OR submit and use IB.
update2- viewWillLayoutSubviews WILL get called when one of its subviews is resized. So it is still disqualified as it fails property 2 of the required characteristics that I'm looking for. :(
If you're doing layout with IB, it's fine to do additional view initialization in viewDidLoad (for example, if you need to do stuff that IB doesn't handle well, or if you have UIView subclasses with properties not supported by IB). Alternatively, if you're not using IB, the documentation says you should use loadView to manually initialize your view hierarchy.
You're right, though, that you can't rely on the frame being accurate at that point. So you can accomplish layout via each view's autoResizingMask property, layout constraints (if you're iOS 6 and later), and/or overriding layoutSubviews.
My usual approach is to do layout to some degree in IB, then do anything else I need to (nontrivial layout, custom classes, etc) in viewDidLoad. Then, if I have layout to figure out that autoResizingMask doesn't cover (I'm supporting down to iOS 5), I override viewWillAppear (or layoutSubviews if I'm subclassing UIView) and do some pixel math. I've got a category on UIView to help with this that has things like:
-(void)centerSubviewHorizontally:(UIView *)view pixelsFromTop:(float)pixels;
-(void)centerSubviewHorizontally:(UIView *)view pixelsBelow:(float)pixels siblingView:(UIView *)sibling;
View controllers should not have initWithFrame: methods. What I do in all of my code (I never use IB) is to let the default loadView do its own thing. I create and setup all subviews in viewDidLoad. At this point the view controller's frame has at least a sane value. All subviews can be created with their own sane frames based on the initial size of the view controller's view. With proper autoresizingMask values this may be all you need.
If you need more specific subview layout, put the appropriate layout code in the viewWillLayoutSubviews method. This will deal with any view controller view frame changes including rotation, in-call status bars, etc.
If you don't use interface builder you should override loadView and initialize the views there. If you use autolayout you can also add your constraints there. If you don't use autolayout you can override the layoutSubviews method of your views to adjust the frames.

cannot update view.layer.frame in viewDidLoad?

I am trying to execute following code in viewDidLoad method of my single view controller project:
self.view.layer.frame = CGRectInset(self.view.layer.frame, 20, 20);
But it does not give the desired inset. However other UI changes i make in the same method do work e.g
self.view.layer.backgroundColor = [UIColor orangeColor].CGColor;
Above line of code does work and background is change to orange but the frame does not.
The inset works only if I place the line of code in viewDidAppear. I would like to understand the key reason for this behavior if anyone can explain. Thank you in advance.
I think the issue you are running into with this line:
self.view.layer.frame = CGRectInset(self.view.layer.frame, 20, 20);
can be explained like so :
in viewDidLoad, the properties are set, but the frame of the views are not yet defined.
by the time viewWillAppear is triggered, they will be set. That explains why that line of code works there.
But since iOS 5, there is another method called after viewDidLoad and before viewWillAppear in which the view frames are set : viewDidLayoutSubviews.
You can find the complete explanation of this in this season's Stanford CS193P course about iOS programming (very cool by the way).
So if you want it to work just once, use :
- (void)viewDidLayoutSubviews
{
self.view.layer.frame = CGRectInset(self.view.layer.frame, 20, 20);
}
PS : I posted this answer on Ray's forum too.
Regards,
Fred
The viewDidLoad method is too early to set your view's frame because something else is changing the frame later.
For example, if this is your root view controller, then the UIWindow object will set the view's frame when it adds the view to itself as a subview. The viewDidLoad method runs as soon as loadView returns, before any outside object (like the UIWindow) can get its hands on the view.
You can test this by using a custom view (if you're not already) and overriding setFrame: in the custom view class:
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
}
Put a breakpoint in that method and you'll see that the view's frame gets set some time after viewDidLoad returns.
Rob Mayoff's answer is correct and excellent, but putting it a slightly different way: viewDidLoad only means the view has loaded, i.e. that the view controller has obtained its view. It doesn't mean that the view has been placed in the interface. That, indeed, is one of the things that viewDidAppear: does mean — and that's why it worked when you ran your code there.
The trick in this sort of situation, where you want to initialize something about the view, is to do it late enough but do it only once. viewDidAppear: could easily be called again later, but you don't want to initialize the view again (unless it has been unloaded). In iOS 5, isMovingToParentViewController allows you to distinguish the particular circumstances you're looking for. Before that, it might be necessary to set up a BOOL flag so that you perform final initializations only once.
A related trap is what happens when the app launches into landscape orientation. Here, too, viewDidLoad is too soon because the interface has not yet rotated into landscape.
However, this issue should not be arising at all. It should be none of your business to inset a view controller's view. Either the view is the root view controller, in which case its size is correctly taken care of automatically, or it is the child of a parent view controller, in which case it is the parent view controller's job to size the view (as UINavigationController, for example, already does), or the view is to be presented modally, in which case its size will be set automatically to match the view it replaces. So I would suggest that you very question suggests you're doing something wrong.
Create your project with an older version of Xcode (for instance I'm using Xcode 4.3.3 for this) . Then you can use setFrame: method with viewDidLoad in any version of Xcode .
I also experienced this issue on Ray Wenderlich's tutorial 'Introduction to CALayers'.
http://www.raywenderlich.com/2502/introduction-to-calayers-tutorial
It seems shrinking the entire view controllers view is not the best thing to do. Instead create a sub view and call CGRectInset on that e.g in your view controllers viewDidLoad
UIView *viewToManipulate = [[UIView alloc] initWithFrame:CGRectMake(0.0,0.0, self.view.bounds.size.width, self.view.bounds.size.height)];
[self.view addSubview:viewToManipulate];
viewToManipulate.backgroundColor = [UIColor redColor];
viewToManipulate.layer.cornerRadius = 20.0;
viewToManipulate.layer.frame = CGRectInset(self.view.layer.frame, 20, 20);

Resources