iOS AutoLayout - get frame size width
Asked Answered
G

9

59

I am developing using iOS 6 auto layout

I would like to log a message displaying the frame width of the view.

I can see the textView on the screen.

But I get the width and height as zero, am I missing something ?

NSLog(@"textView    = %p", self.textView);
NSLog(@"height      = %f", self.textView.frame.size.height);
NSLog(@"width       = %f", self.textView.frame.size.width);

textView    = 0x882de00
height      = 0.000000
width       = 0.000000
Gentoo answered 21/9, 2012 at 8:49 Comment(0)
V
59

I think auto layout hasn't had the time to layout your views by the time you call this. Auto layout hasn't happened by the time viewDidLoad is called, because it's called just after the views are loaded and it's only after that that the views are placed into the view controller's view hierarchy and eventually laid out (in the view's layoutSubviews method).

Edit: this answer points out why the scenario in the question doesn't work. @dreamzor's answer points out where to place your code in order to solve it.

Vidovic answered 21/9, 2012 at 8:52 Comment(9)
I tried to log the message in viewDidLoad. Which method gets invoked after the auto layout has been completed. The reason I ask is because I want to calculate the width before the user can interact with the textView. So logging the object description might not help bcuz I want to use the width for some other calculation.Gentoo
No, viewDidLoad happens before auto layout. Auto layout happens lazily after a view has been added to another view (which is shortly after viewWillAppear has been called, but probably not reliably before viewDidAppear). The only reliable way to know is to know when layoutSubviews is finished on the view controller's view.Vidovic
Why are you trying to get the frame anyway? If you want to do calculations based on it and do layout yourself, you will probably end up battling auto layout.Vidovic
I was trying to calculate the number of letters that can fit into the UITextView to clip it and give it a "..." kind of look.Gentoo
Since you can't do that while someone's entering text, use a UILabel with lineBreakMode to one of the NSLineBreakByTruncating... values and set your constraints so that the label is always the same width, height and position as the text view.Vidovic
Thanks Jasper, but the only problem with using a UILabel is that when there is a only a single line of text in the UILabel, the text gets displayed at the vertical centre (height / 2) instead of the top of the label.Gentoo
Really appreciate your help on this. For completeness, pls can you update your answer to include the "reason the frame width is zero is bcuz viewDidLoad happens before auto layout". So that I can mark it as answered.Gentoo
Edited to fit what the problem was and good luck on solving your problem.Vidovic
Jesper, thanks a lot for your pointers, viewDidAppear seems to happen after auto layout. If there is a better approach let me know.Gentoo
P
156

Actually, above answers are not quite right. I followed them and got zeros again and again.

The trick is to place your frame-dependent code to the viewDidLayoutSubviews method, which

Notifies the view controller that its view just laid out its subviews.

Do not forget that this method is called multiple times and is not the part of ViewController's life cycle, so be careful while using this.

Hope it will be helpful to someone.

Just wanted to add, that for my program without landscape mode NOT using Auto Layout is way much simplier... I tried, though =D

Phonetist answered 3/1, 2013 at 21:11 Comment(8)
From experience, this is the earliest callback after autolayout does its work. I recommend using, for example, a BOOL to catch the first time this is called and do frame dependent logic. It's not very elegant but gets the job done until there's proper callbacks for specific autolayout constraints.Lyrist
@dreamzor, how do I know if it is the last viewDidLayoutSubviews? I found on iOS 7, only one viewDidLayoutSubviews will be called before viewDidAppear, but on iOS 8, I see mutiple viewDidLayoutSubviews before viewDidAppear. This is annoying, because the first viewDidLayoutSubviews will give wrong frame size on iOS 8, so I have to ignore it for first timeMccrea
@Mccrea In my project the fifth time is correct, so how can I ignore others?Deangelo
@Deangelo I think you can't. it is totally a system behaviour, and you have no control. As long as iOS think the view needs layout, it will trigger itMccrea
@Mccrea What's your solution at last?Deangelo
it is work around I guess. I have to put some code that needs frame information to viewDidAppear, or in viewDidLayoutSubViews, I just accept whatever the frame is, though it is a waste.Mccrea
It should probably be noted that if you use [viewDidLayoutSubviews] to (synchronously) refresh a view, the effects will be visible in a subsequent from-view-controller-Foo-to-view-controller-Bar transition animation, if any. If you wait until [viewDidAppear:] with a refresh, it will be "surprise" rendered already post- view controller transition animation.Nope
Add if yourView.bounds != .zero If your view's bounds is still nil when using viewDidLayoutSubviews. Your view might not be the first that is laid out.Squiffy
V
59

I think auto layout hasn't had the time to layout your views by the time you call this. Auto layout hasn't happened by the time viewDidLoad is called, because it's called just after the views are loaded and it's only after that that the views are placed into the view controller's view hierarchy and eventually laid out (in the view's layoutSubviews method).

Edit: this answer points out why the scenario in the question doesn't work. @dreamzor's answer points out where to place your code in order to solve it.

Vidovic answered 21/9, 2012 at 8:52 Comment(9)
I tried to log the message in viewDidLoad. Which method gets invoked after the auto layout has been completed. The reason I ask is because I want to calculate the width before the user can interact with the textView. So logging the object description might not help bcuz I want to use the width for some other calculation.Gentoo
No, viewDidLoad happens before auto layout. Auto layout happens lazily after a view has been added to another view (which is shortly after viewWillAppear has been called, but probably not reliably before viewDidAppear). The only reliable way to know is to know when layoutSubviews is finished on the view controller's view.Vidovic
Why are you trying to get the frame anyway? If you want to do calculations based on it and do layout yourself, you will probably end up battling auto layout.Vidovic
I was trying to calculate the number of letters that can fit into the UITextView to clip it and give it a "..." kind of look.Gentoo
Since you can't do that while someone's entering text, use a UILabel with lineBreakMode to one of the NSLineBreakByTruncating... values and set your constraints so that the label is always the same width, height and position as the text view.Vidovic
Thanks Jasper, but the only problem with using a UILabel is that when there is a only a single line of text in the UILabel, the text gets displayed at the vertical centre (height / 2) instead of the top of the label.Gentoo
Really appreciate your help on this. For completeness, pls can you update your answer to include the "reason the frame width is zero is bcuz viewDidLoad happens before auto layout". So that I can mark it as answered.Gentoo
Edited to fit what the problem was and good luck on solving your problem.Vidovic
Jesper, thanks a lot for your pointers, viewDidAppear seems to happen after auto layout. If there is a better approach let me know.Gentoo
I
35

Include in your viewDidLoad()

self.view.setNeedsLayout()
self.view.layoutIfNeeded()

Before accessing yourview.frame.size.width

Incorrigible answered 4/3, 2015 at 15:29 Comment(1)
The best answer I think!Povertystricken
G
31

Solution

  • Place that code in viewDidAppear

Reason

  • viewDidLoad happens before autolayout is completed. So the position is not yet set by autolayout that was specified in xib
  • viewDidAppear happens after autolayout is completed.
Gentoo answered 28/9, 2012 at 2:38 Comment(3)
I placed the code in viewDidAppear and yet still, my view's frame NSLog shows zeros.Phonetist
Actually viewDidAppear is called before viewDidLayoutSubviews as for iOS 6.0 so dreamzor is right.Halden
In iOS 6, based on my testing, I used navigation controller in the 2nd controller, the viewDidLayoutSubviews was called before viewDidAppear but in viewDidLayoutSubviews the subview's frame width remains to be zero. Only in viewDidAppear, the subview's frame width seems to have the correct value.Gentoo
C
13

Actually I managed to force the layout update before my code within the viewDidLoad:

override func viewDidLoad() {
        super.viewDidLoad()

        println("bounds before \(self.previewContainer.bounds)");
        //on iPhone 6 plus -> prints bounds before (0.0,0.0,320.0,320.0)

        self.view.setNeedsLayout()
        self.view.layoutIfNeeded()

        println("bounds after \(self.previewContainer.bounds)");
        //on iPhone 6 plus -> prints bounds after (0.0,0.0,414.0,414.0)

        //Size dependent code works here
        self.create()
    }

UPDATE: This doesn't seem to work anymore

Cruciform answered 18/12, 2014 at 13:51 Comment(0)
B
10

This is really strange feature. But I found:

If you want to get frame without using layoutsubviews method, Use this one:

dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"View frame: %@", NSStringFromCGRect(view.frame));
    });

This is really strange!!!

Brnaba answered 29/12, 2015 at 13:2 Comment(0)
B
7

None of the above answers worked completely for me, except viewDidLoad but that has the side effect of not displaying what I want until after the view has animated in, which looks poor.

viewDidLayoutSubviews should be the correct place to run code that relies on the autolayout being complete, but as others have pointed out it is called multiple times in recent iOS versions and you can't know which is the final call.

So I resolved this with a small hack. In my storyboard, mySubview should be smaller than its containing self.view. But when viewDidLayoutSubviews is first called, mySubview still has a width of 600, whereas self.view seems to be set correctly (this is an iPhone project). So all I have to do is monitor subsequent calls and check the relative widths. Once mySubview is smaller than self.view I can be sure it has been laid out correctly.

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if self.mySubview.bounds.size.width < self.view.bounds.size.width {

        // mySubview's width became less than the view's width, so it is
        // safe to assume it is now laid out correctly...

    }
}

This has the advantage of not relying on hard-coded numbers, so it can work on all iPhone form factors, for example. Of course it may not be a panacea in all cases or on all devices, but there are probably many ingenious ways to do similar checks of relative sizes.

And no, we shouldn't have to do this, but until Apple gives us some more reliable callbacks, we're all stuck with it.

Blader answered 4/11, 2015 at 20:44 Comment(0)
S
3

You can only find out the size after the first layout pass. Or Just call below method then after you got actual width of you view.

[yourView layoutIfNeeded];
Surgeon answered 8/7, 2017 at 10:39 Comment(0)
E
0

I agree with the solution of "user1046037"

If you are using both atuoLayout guides, and code at the same time, you would better to put frame(or NSlayoutConstraints) programming into "ViewDidAppear", especially when you are designing UI for multiple devices.

Based on my tests, iOS14 will take autoLayouts from screen(default iPhone 11 Pro Max) before "ViewDidLoad", and update it in "ViewDidAppear".

For people who are not familiar with view cycle, its "load view from storyboard" -> "ViewDidLoad" -> "ViewWillAppear" -> "ViewDidAppear".

Ebberta answered 9/11, 2020 at 17:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.