I have an iPad-application. In landscape orientation the UIViewController's view actual width = 1024px and height = 768 - 20 (statusBar) - 44 (navigationBar) = 704px.
So I wanna get this [1024 x 704] size and I'm using self.view.bounds for it. It returns [748 x 1024], which is wrong! But when I rotate the screen twice (current -> portrait -> current), the view's bounds are correct - [1024 x 704].
The view was initialized like this:
- (void)loadView {
self.view = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
self.view.backgroundColor = [UIColor lightGrayColor];
}
And bounds were get like this:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"bounds = %#", NSStringFromCGRect(self.view.bounds));
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
NSLog(#"bounds = %#", NSStringFromCGRect(self.view.bounds));
}
So the question is.. How can I get the correct view's bound in the very beginning?
How to do this correctly
Your UIViewController subclass should override the method viewWillLayoutSubviews, see also here.
When this method is called, the viewController's view has its correct size and you can make any necessary adjustments to subviews prior to the layout pass over the subviews.
Swift
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
NSLog("bounds = \(self.view.bounds)")
}
Obj-C
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
NSLog(#"bounds = %#", NSStringFromCGRect(self.view.bounds));
}
Documentation
When a view's bounds change, the view adjusts the position of its subviews. Your view controller can override this method to make changes before the view lays out its subviews. The default implementation of this method does nothing.
As you see from the emphasised part, this method is called every time the view controller's view changes size, as well as when the view first appears. This lets you respond correctly to rotation and other bounds change events
Several ways that don't work
A few approaches suggested in other answers don't work well, or have serious drawbacks. I urge you to avoid these approaches and I'll go through them to discuss the reasons you should avoid them.
viewDidLoad and viewWillAppear – during these calls view does not yet have its final size. The size you get will only ever be correct by pure chance, so as to mislead you.
viewDidAppear – this is too late. Your view is already on screen and visible to the user. Making changes here will cause visible changes / abrupt glitches and will look amateurish. Once again, please – for your sake, for my sake, for everyone's sake: don't do it! You're better than that and so are your users.
UIScreen.mainScreen.bounds.size – this is extremely low level. You're implementing a UIViewController and the size of its view depends on the controllers it is nested in (navigation, tab, paging, any custom controllers, etc), how the device is rotated, and potentially, how the screen has been split up for multitasking. So, while you might be able to compensate for all these and calculate the final size of your view, you'll end up with complex and brittle code that can easily break if Apple decide to change any of these metrics. UIViewController will do all this for you if you just override viewWillLayoutSubviews.
Other than not providing correct information, these problematic approaches will not help you with auto-rotation or other events that cause the view controller's view to change size, such as multitasking gestures. This is something you really want to handle smoothly.
So please: be a champ. Do it the right way. Use viewWillLayoutSubviews. Your implementation will be called for every size change, and your users, future self and team members will celebrate you for it. Bravo!
Further tips
When viewWillLayoutSubviews is called, the only view in your hierarchy that will be resized to its final size is viewController.view. The give away for this is in the name of the method. It's telling you view… (your view controller's root view) …WillLayout… (really soon now, but it's not happened yet) …Subviews (everything else in its hierarchy under the root view).
So subview layout has not happened yet. Every child under the root does not yet have a valid final size. Any size information you query from the child views will be at best completely wrong.
More likely, and enormously worse, it will be misleadingly correct.
It happens to be what you expect and need due to a default at this size and orientation, or due to your storyboard settings. But this is only by chance and isn't something you can rely on with different device sizes or orientations.
If you need to be told when a particular subview changes size, and know its exact final size, you should generally override the layoutSubviews method of that particular UIView subclass.
As per some of the other answers, the issue you are seeing is because viewDidLoad is called before the rotation happens. Because the iPad always initializes in portrait mode, if you get the size values in viewDidLoad, they will always be the portrait sizes - this is irrespective of any orientations you've configured.
To get the size after the orientation/rotation happens, get the size values in viewDidAppear.
I don't particularly understand why iOS doesn't handle this better - especially given that you define the orientations in the project settings, and, in Xcode Interface Builder. But, I'm sure there is a good reason ;-).
I've always used:
CGSize sizeOfScreen = [[UIScreen mainScreen] bounds].size;
to get the size of the screen, and:
CGSize sizeOfView = self.view.bounds.size;
to get the view's size. I have just tested it on viewDidLoad and it returned:
2012-07-17 10:25:46.562 Project[3904:15203] bounds = {768, 1004}
Which is correct since the CGSize is defined as {Width, Height}.
This is what I found in my last project: the frame of self.view will be adjusted after viewDidLoad according to if this screen has navigation bar etc.
So maybe you want to use that value after viewDidLoad (maybe in viewWillAppear or viewDidAppear) or adjust it manually by substract the height of bars.
I ran into this issue and found that getting the bounds in viewDidAppear worked for my needs.
If you want to get correct bounds of view in ViewDidLoad method, you can use Dispatch async with delay for it. See this example below:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Get your bounds here
}
Related
The problem: Let's suppose I have an image and an element which constantly triggers viewDidLayoutSubviews (a scrollview.. so that at every scroll viewDidLayout would be called... or whatever element that triggers viewDidLayout quite often).
This image as well as the "viewDidLayout_element" are all set up well with autolayout in the storyboard.
Now:
I need to make the image to be a rounded one.
with the typical: layer.cornerRadius.. and layer.masksToBounds. This requires a calculated imageView frame.
In what view controller cycle do I make it programatically? Taking into consideration that:
*except for the viewDidLayoutSubviews I don't get the right frame
*the viewDidLayoutSubviews can be called even thousands of times depending on the "viewDidLayout_element" that triggers it.
*If I call layoutIfNeeded on the imageView in viewDidAppear (because it's the only case when frames are already available so we can force their calculation) the user will already catch a glimpse of the transformation from a square image to a circular image. In other words in viewDidAppear the frame becomes available for our manipulation, but is also available for milliseconds to the user's eyes.
*it does not make sense to fill the viewDidLayoutSubviews with flags (especially if there will be something more performance intensive than a circular imageView transformation) like below:
if !iDidChangedTheImage
{
imageView.applyCornerRadius()
}
The question:
Where do I have the correct frame size inside a viewController cycle without the problems from above? Or how do you usually solve this problem?
Subclass UIImageView to your custom class, and override layoutSubviews. After that you don't need to care about image view size change and updating corner radius, just use that class for your custom image view
#implementation CustomImageView
- (void)awakeFromNib {
[super awakeFromNib];
self.layer.masksToBounds = YES;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.layer.cornerRadius = self.frame.size.height / 2.0;
}
#end
I am working on my first project using Auto Layout and custom views. My question is this:
I created my custom view in Interface Builder then added constraints to stretch the view if needed which is working the way I want it to, however, consider the following code snippet from my custom view class -
// MyCustomView
-(id)initWithCoder:(NSCoder*)decoder
{
self = [super initWithCoder:decoder];
if(self != nil)
{
CGFloat layerWidth = self.bounds.size.width;
CGFloat layerHeight = self.bounds.size.height;
}
return self;
}
This code return the size set in IB. My drawing code relies on the new width and height (if the view has been stretched) but I don't know how to retrieve them.
First, that code is utterly silly because you are creating variables layerWidth and layerHeight and throwing them away, which is pointless.
Second, self.bounds.size is always the view's width and height. However, it is pointless to ask about this in initWithCoder:, which (as you have rightly seen) happens long before the view is put into the interface and even longer before the auto layout takes place that resizes it. If your drawing code relies on the bounds size, then retrieve the bounds size when you draw. If you need to draw again because the view has changed size, and if this is not happening all by itself, then implement layoutSubviews to tell the view that it needs to be drawn again.
Using the Storyboard editor with auto-layout and constraints, I have created a hierarchy with a UIViewController (1) inside of which there is a Container(2) into which in turn a UIViewcontroller(3) is embedded.
Setting the background color of the embedded UIViewController(3) to a different color than (1) shows me that the restraints are being enforced: Visually, it appears to have the size I expect both in the storyboard-editor, in the emulator and on my device.
But I fail to get the size of UIViewController(3) from code, I have tried accessing the UIViewControllers(3) views frame and bounds, both with incorrect results, they seem to return the same size as the size of the host UIViewController(1), which is clearly wrong since once is indeed embedded in the other.
I have tried also accessing the self.parentViewController.view frame and bounds, but these both return 0.0.
In the embedded UIViewController (3):
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"Size: %f x %f", self.view.frame.size.width, self.view.frame.size.height);
NSLog(#"Origin: %f x %f", self.view.frame.origin.x, self.view.frame.origin.y);
}
I would expect the output of this to be something which resembles, atleast in aspect ratio, the actual visible size of the UIViewController (3), but it is in fact equal to what I get if I put the same code from UIViewController (1), which is clearly not true since they are not of same size.
I usually make an outlet for the view, which makes it much easier to get the parameters e.g. in your interface make an IBOutlet and connect to your view
IBOutlet UIView *outletYourView
and wherever you need the information call
outletYourView.frame.size.width;
outletYourView.frame.size.height;
etc.
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.
I'm building an iPad app with views that are split horizontally and animate in from the top and bottom (think of jaws sliding closed and open to appear and disappear respectively).
My problem is the layout of the custom jaws subview is broken only when the view loads in a landscape orientation. (The jaws-view container loads at the proper size, but the subsequent subviews for the top and bottom half are too tall, and going off the screen. They are the correct width though.)
I can start in portrait and then rotate and everything is arranged correctly.
I've tried setting the frame of the new view to the bounds of the original in a bunch of places (as suggested by many answers that didn't work for me, links upon request) but either haven't found the right spot, or need something more.
Do I need to do anything special to get the size to propagate? Is there a point before which I should not do animation? (I'm trying to move the top and bottom in my new view controller's viewDidLoad.)
The solution to this required 2 parts.
The first was described in this answer:
https://stackoverflow.com/a/8574519/1143123
which describes using viewWillAppear method instead of viewDidLoad (called earlier and "incorrect" values for bounds). This solved the problem of the view being layed out properly when loading that view in landscape (and propagated to subsequent rotations).
The second part was that the view could still get messed up if I started animating it and then did a rotation in the meantime. I changed my animation class to only move the center coordinate (as opposed to sliding the frame) which would have been better in the first place, but that didn't solve it. In the end I hardcoded the following in the ViewController for the class exhibiting these issues:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
// The container view should match the size of the current view
gameView.frame = self.view.bounds;
CGFloat width = self.view.bounds.size.width;
if(roundInProgress) {
gameView.jawsTop.frame = CGRectMake(0, 0, width, self.view.bounds.size.height/2);
gameView.jawsBottom.frame = CGRectMake(0, self.view.bounds.size.height/2, width, self.view.bounds.size.height/2);
} else {
// If round not in progress, game cards should be offscreen
CGFloat height = self.view.bounds.size.height/2;
gameView.jawsTop.frame = CGRectMake(0, -height, width, height);
gameView.jawsBottom.frame = CGRectMake(0, self.view.bounds.size.height, width, height);
}
}
Without seeing your code my guess is that it has to do with the "Autoresize Subviews" property of your parent view and/or the autosizing set-up for your subviews. Try changing that property in Interface Builder to see if that fixes your issue.