When can you call UIEdgeInsets safeAreaInsets? - ios

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!

Related

Swift - TextView layoutSubviews() not called during viewWillTransition()

Does anyone know why UITextView.layoutSubviews() is not called when rotating a device to portrait mode?
When rotating to landscape mode, these are called:
UIViewController.viewWillTransition
UIViewController.viewDidLayoutSubviews
UITextView.layoutSubviews
UILabel.layoutSubviews
But when rotating back to portrait, the UILabel.layoutSubviews() is called, but not the UITextView.layoutSubviews. This is in an empty project with no other code apart from traces in these methods.
layoutSubviews is usually called when setNeedsLayout() is invoked already in the previous invocation of run loop.
If the layout system does not think it needs to be called, it will not be called.
Ideally you should not override this function. You should just call setNeedsLayout() after making superview changes, and let the layout system call the default implementation of this function. Morever, you should define your subview layout needs inside auto-layout so it is able to get correct values from there.
If you want immediate update, you should call layoutIfNeeded().
This is because this is one of those methods that are called arbitrarily by UIKit framework and it may not be ideal for your layout needs.
There are 2 separate things here.
Understanding layoutSubviews(). I.e. when and where to use it.
How to achieve what you want to do the right way. I.e. doing something with the UITextView at device rotation.
About the layoutSubviews(), you should not put any logic here as your view is not having any sub views.
You may say that we expect iOS to call it, so we can put some implementation here, but again, that is not the right way. layoutSubviews() is not meant to alter the view itself, but just laying out sub views.
I would recommend reading more on layoutSubviews(). I learnt from here, when I started learning iOS.
Now to achieve what you want to do, i.e. do something at the time of device rotation, you proper way is to use viewWillTransition(to:with:) method of UIViewController.
In this method, you can put logic to do something just before the transition will happen.
And you can also put logic which will execute during the transition OR after the transition completes, using the UIViewControllerTransitionCoordinator parameter passed to viewWillTransition(to:with:)
Hope this helps!

The occasion to call setNeedsLayout or setNeedDisplay

I have read some articles that what's the difference between setNeedsLayout and layoutIfNeeded, while what I am focusing is:
1. do I need to call these two method together if I want layout immediately, because I saw this kind of combination so many times
2. when do I need to call setNeedsLayout? upon my understanding, If I change view's frame, it will update layout during next cycle, that I don't have to call setNeedsLayout explictly
How these things work is through invalidation to remove redundancy. A view will contain information if needs to layout or not.
So calling setNeedsLayout will just set some internal boolean value needsLayout to true. Once layoutIfNeeded is called it will check this boolean value
if needsLayout {
needsLayout = false
doMagic() // Calls layoutSubviews at some point
}
Why this is designed this way is because multiple calls may invalidate layout but we want to layout it only once or as fewer times as possible.
In most cases you will not need to call setNeedsLayout because most changes already do that for you. For instance you may change a constraint value and invalidation is done for you. All you need is to call layoutIfNeeded and your views will update. To be more correct you don't even need to call layoutIfNeeded as it will do that for you in the next cycle. But you will need to call it if you want the change animated for instance and you need to do that in animation block.
myViewConstraint.constant = 40.0 // Will already call setNeedsLayout
UIView.animate(withDuration: 0.3, animations: {
myView.layoutIfNeeded()
})
So changing constraints does nothing but change the information on how the view(s) should be layout. Only the call to layoutIfNeeded will actually use those values and change layout. That is why you only need to put that in the animation block (it is not wrong to put it all in the block though).
To be fair there have been some changes where now by default (you may disable it) animate methods already layout your views by themselves so you can do with even less code but that is not the point at the moment.
So:
You do not need to call the 2 methods together to layout immediately. If the view layout is already invalidated (which is in most cases) then layoutIfNeeded is enough. But note that setNeedsLayout is as trivial as setting a boolean internally to true so there is no harm in calling it, just a precaution. So calling both is safer. Calling setNeedsLayout alone will do nothing "immediately" though.
Hopefully you never need to call setNeedsLayout. There are some complex situations where you need to explicitly invalidate layout and there are a few possible UI bugs. In all other cases this will be done for your. But note that if you come to a situation where you need to call this "it will update layout during next cycle" will not be true. Until the view layout is invalidated it will not layout at all.
I am not sure where setNeedDisplay fits in your question (it is only in your title) but this one works the same way but is a bit more complicated. It will invalidate its content and force it to redraw, call drawRect. This must occur during its drawing pipeline, not just anytime so you may not explicitly call it to redraw. And if you do nothing will happen (maybe crash) since it will have no context to draw on. If you override drawRect and you resize your view it will try to cache your drawn content and use contentMode to resize the drawing. By default it is set to scaleToFill which means your content will be stretched as view size changes. You will need to call setNeedDisplay in order for your drawRect to be called again and you may redraw content accordingly.
I found what I want to ask, when should I call setNeedLayout, when should not. since the aim to call setNeedLayout is to call layoutSubviews, so for situations as follows, I don't have to do call setNeedLayout
Resizing a view
Adding a subview User
scrolling a UIScrollView
(layoutSubviews is called on the UIScrollView and its superview)
User rotating their device
Updating a view’s constraints

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.

UIView created in IB has no frame in viewDidLoad and awakeFromNib

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!

Resources