Since Xcode 8 and iOS10, views are not sized properly on viewDidLayoutSubviews
Asked Answered
B

13

95

It seems that with Xcode 8, on viewDidLoad, all viewcontroller subviews have the same size of 1000x1000. Strange thing, but okay, viewDidLoad has never been the better place to correctly size the views.

But viewDidLayoutSubviews is!

And on my current project, I try to print the size of a button:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    NSLog(@"%@", self.myButton);
}

The log shows a size of (1000x1000) for myButton! Then if I log on a button click, for example, the log shows a normal size.

I'm using autolayout.

Is it a bug?

Burning answered 19/9, 2016 at 17:9 Comment(5)
Having the same issue with UIImageView - when I print i get a weird frame = (0 0; 1000 1000);. I'm inside a UITableViewCell, and once I refresh the tableview the frame is what I expect it to be (also when the cell goes out of the viewport and comes back again). Anyone have any idea why this is happening (weird frame by default) ?Felucca
I think the (0, 0, 1000, 1000) bound initialization is the new way Xcode instanciates views from IB. Before Xcode8, views were created with their configured size in the xib, then resized according to screen just after. But now, there is no configured size in IB document since the size depends on your device selection (at the bottom of the screen). So the real question is: is there a reliable place where views final size could be checked?Burning
are you using rounded corners for your button ? Try calling layoutIfNeeded() before.Felucca
Interesting. I was indeed using the view frame to calculate a round border. Even if it does not answer the question, it does work. Its a good tip to keep in mind. Thanks!Burning
I think I'm having similar issues setting up an image button inside the rightview of a uitextfield. I wanted to set the height and width of the image button to the height of the textfield so that it maintained its aspect ratio and fallout out the container.Prop
B
99

Now, Interface Builder lets the user change dynamically the size of every view controllers in storyboard, to simulate the size of a certain device.

Before this functionality, the user should set manually each view controller size. So the view controller was saved with a certain size, which was used in initWithCoder to set the initial frame.

Now, it seems that initWithCoder do not use the size defined in storyboard, and define a 1000x1000 px size for the viewcontroller view & all its subviews.

This is not a problem, because views should always use either of these layout solutions:

  • autolayout, and all the constraints will layout correctly your views

  • autoresizingMask, which will layout each view which doesn't have any constraint attached to (note autolayout and margin constraints are now compatible in the same view \o/ !)

But this is a problem for all layout stuff related to the view layer, like cornerRadius, since neither autolayout nor autoresizing mask applies to layer properties.

To answer this problem, the common way is to use viewDidLayoutSubviews if you are in the controller, or layoutSubview if you are in a view. At this point (don't forget to call their super relative methods), you are pretty sure that all layout stuff has been done!

Pretty sure? Hum... not totally, I've remarked, and that's why I asked this question, in some cases the view still has its 1000x1000 size on this method. I think there is no answer to my own question. To give the maximum information about it:

1- it happends only when laying out cells! In UITableViewCell & UICollectionViewCell subclasses, layoutSubview won't be called after subviews would be correctly layed out.

2- As @EugenDimboiu remarked (please upvote his answer if useful for you), calling [myView layoutIfNeeded] on the not-layed out subview will layout it correctly just in time.

- (void)layoutSubviews {
    [super layoutSubviews];
    NSLog (self.myLabel); // 1000x1000 size 
    [self.myLabel layoutIfNeeded];
    NSLog (self.myLabel); // normal size
}

3- To my opinion, this is definitely a bug. I've submitted it to radar (id 28562874).

PS: I'm not english native, so feel free to edit my post if my grammar should be corrected ;)

PS2: If you have any better solution, feel free not write another answer. I'll move the accepted answer.

Burning answered 30/9, 2016 at 11:6 Comment(9)
thx, but I could not find the radar with the id 28562874, can I have the radar URL?Explosion
Worked like a charm for me, also for the layers of the view!Ruhl
@Joey, I don't know if I can get a link of my bug. I did not find any direct URL, and it seems that other users can't see my reports. According to this SO answer stackoverflow.com/a/145223/127493, it seems that the best way to increase the priority of a bug is to make a duplicate.Burning
This is beyond stupid on Apple's part. I have a fully specified by auto layout view controller, and several of the text fields and a UIView all have this stupid 0,0,1000,1000 frame. But not all. How can this escape QA? I suppose I can file yet another Radar they won't read.Opinionated
@Opinionated do not hesitate to file a bug. I do think Apple guys read the radars, but they are just too many. However, I don't think the bug is (0, 0, 1000, 1000) frames in viewDidLoad. viewDidLoad never was the place to read a frame because even before, views were not sized properly at this step. To my opinion, the bug is the still 1000 large frame in layoutSubviews or viewDidLayoutSubviews methods.Burning
Thanks @Martin, wasn't aware of layoutIfNeed and was getting crazy due to a UITableViewCell with a subview that had to have a gradientEndblown
I'd like to thank you and everyone else for your insights and suggestions. I spent all day pulling my hair out because a UIStackView inside of a UICollectionViewCell was not returning a correct height during viewDidLayoutSubviews. Calling layoutIfNeeded immediately rectified the problem.Ressieressler
viewDidLayoutSubviews are called multiple times. How read the xib frame of subview that later change due to animation?Decisive
Thanks , i was setting corner Radius in viewDidload, that caiuse the problem, now i am setting in viewWillappear and later, so my view shows up correctlyBlastoff
F
41

Are you using rounded corners for your button ? Try calling layoutIfNeeded() before.

Felucca answered 20/9, 2016 at 20:26 Comment(5)
ahah, trying to get more rep? As I said in my comment, this does not answer the question. However, it helped me so you earned a +1 :)Burning
It may help someone in the future, and it's easier to spot comparing to commentsFelucca
Helped me just now!Noemi
@daidai glad to hear that!Felucca
this works! but why? also but what is the right solution for getting proper frame size?Bamberg
R
26

Solution: Wrap everything inside viewDidLayoutSubviews in DispatchQueue.main.async.

// swift 3

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    DispatchQueue.main.async {
        // do stuff here
    }
}
Rubble answered 22/3, 2017 at 0:17 Comment(3)
this is working even if put into -viewDidLoad... such a strange workaroundAlexandria
Probably because when viewDidLayoutSubviews the system hadn't had the time to do what needed to be done. Calling dispatch makes you jump one runloop, allowing the system to finish his word. If i'm right when you could get the same behaviour with a sleep of a few millisecondsNature
because all UI tasks must be performed within the main thread, thanks for your solution!Smitten
S
18

I know this wasn't your exact question, but I ran into a similar problem where as on the update some of my views were messed up despite having the correct frame size in viewDidLayoutSubviews. According to iOS 10 Release notes:

"Sending layoutIfNeeded to a view is not expected to move the view, but in earlier releases, if the view had translatesAutoresizingMaskIntoConstraints set to NO, and if it was being positioned by constraints, layoutIfNeeded would move the view to match the layout engine before sending layout to the subtree. These changes correct this behavior, and the receiver’s position and usually its size won’t be affected by layoutIfNeeded.

Some existing code may be relying on this incorrect behavior that is now corrected. There is no behavior change for binaries linked before iOS 10, but when building on iOS 10 you may need to correct some situations by sending -layoutIfNeeded to a superview of the translatesAutoresizingMaskIntoConstraints view that was the previous receiver, or else positioning and sizing it before (or after, depending on your desired behavior) layoutIfNeeded.

Third party apps with custom UIView subclasses using Auto Layout that override layoutSubviews and dirty layout on self before calling super are at risk of triggering a layout feedback loop when they rebuild on iOS 10. When they are correctly sent subsequent layoutSubviews calls they must be sure to stop dirtying layout on self at some point (note that this call was skipped in release prior to iOS 10)."

Essentially you cannot call layoutIfNeeded on a child object of the View if you are using translatesAutoresizingMaskIntoConstraints - now calling layoutIfNeeded has to be on the superView, and you can still call this in viewDidLayoutSubviews.

Selfannihilation answered 21/9, 2016 at 11:1 Comment(0)
M
5

If the frames are not correct in layoutSubViews (which they are not) you can dispatch async a bit of code on the main thread. This gives the system some time to do the layout. When the block you dispatch is executed, the frames have their proper sizes.

Marienthal answered 11/10, 2016 at 8:51 Comment(0)
E
3

This fixed the (ridiculously annoying) issue for me:

- (void) viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

    self.view.frame = CGRectMake(0,0,[[UIScreen mainScreen] bounds].size.width,[[UIScreen mainScreen] bounds].size.height);

}

Edit/Note: This is for a full screen ViewController.

Epicrisis answered 27/9, 2016 at 22:47 Comment(3)
Using mainScreen bounds is not a good solution because in many cases the viewController do not take the entire screen space. In addition, you should call [super viewDidLayoutSubviews]; in this method because of many autolayout stuff done by the view itselfBurning
@Burning what do you recommend? Agreed this doesn't seem ideal.Bamberg
@Bamberg as I say in my answer, using autolayout or autoresizingMask would layout correctly your UIViews. But if you must perform special computation on a certain view frame, Eugen's answer works: call layoutIfNeeded on it. I have the feeling this is not the best solution, but I still haven't found any better.Burning
L
2

Actually viewDidLayoutSubviews also is not the best place to set frame of your view. As far as I understood, from now on the only place it should be done is layoutSubviews method in the actual view's code. I wish I wasn't right, someone correct me please if it is not true!

Leonorleonora answered 20/9, 2016 at 3:27 Comment(1)
thanks for your answer. Apple documentation on viewDidLayoutSubviews is pretty ambiguous. The second sentence in "discussions" contradicts in some way the last one. developer.apple.com/reference/uikit/uiviewcontroller/…Burning
S
0

I already reported this issue to apple, this issue exist since a long time, when you are initializing UIViewController from Xib, but i found quite nice workaround. In addition to that i found that issue in some cases when layoutIfNeeded on UICollectionView and UITableView when datasource is not set in initial moment, and needed also swizzle it.

extension UIViewController {
    open override class func initialize() {
        if self !== UIViewController.self {
            return
        }
        DispatchQueue.once(token: "io.inspace.uiviewcontroller.swizzle") {
            ins_applyFixToViewFrameWhenLoadingFromNib()
        }
    }

    @objc func ins_setView(view: UIView!) {
        // View is loaded from xib file
        if nibBundle != nil && storyboard == nil && !view.frame.equalTo(UIScreen.main.bounds) {
            view.frame = UIScreen.main.bounds
            view.layoutIfNeeded()
        }
        ins_setView(view: view)
    }

    private class func ins_applyFixToViewFrameWhenLoadingFromNib() {
        UIViewController.swizzle(originalSelector: #selector(setter: UIViewController.view),
                                 with: #selector(UIViewController.ins_setView(view:)))
        UICollectionView.swizzle(originalSelector: #selector(UICollectionView.layoutSubviews),
                                 with: #selector(UICollectionView.ins_layoutSubviews))
        UITableView.swizzle(originalSelector: #selector(UITableView.layoutSubviews),
                                 with: #selector(UITableView.ins_layoutSubviews))
     }
}

extension UITableView {
    @objc fileprivate func ins_layoutSubviews() {
        if dataSource == nil {
            super.layoutSubviews()
        } else {
            ins_layoutSubviews()
        }
    }
}

extension UICollectionView {
    @objc fileprivate func ins_layoutSubviews() {
        if dataSource == nil {
            super.layoutSubviews()
        } else {
            ins_layoutSubviews()
        }
    }
}

Dispatch once extension:

extension DispatchQueue {

    private static var _onceTracker = [String]()

    /**
     Executes a block of code, associated with a unique token, only once.  The code is thread safe and will
     only execute the code once even in the presence of multithreaded calls.

     - parameter token: A unique reverse DNS style name such as com.vectorform.<name> or a GUID
     - parameter block: Block to execute once
     */
    public class func once(token: String, block: (Void) -> Void) {
        objc_sync_enter(self); defer { objc_sync_exit(self) }

        if _onceTracker.contains(token) {
            return
        }

        _onceTracker.append(token)
        block()
    }
}

Swizzle extension:

extension NSObject {
    @discardableResult
    class func swizzle(originalSelector: Selector, with selector: Selector) -> Bool {

        var originalMethod: Method?
        var swizzledMethod: Method?

        originalMethod = class_getInstanceMethod(self, originalSelector)
        swizzledMethod = class_getInstanceMethod(self, selector)

        if originalMethod != nil && swizzledMethod != nil {
            method_exchangeImplementations(originalMethod!, swizzledMethod!)
            return true
        }
        return false
    }
}
Strati answered 15/10, 2016 at 17:21 Comment(0)
R
0

My issue was solved by changing the usage from

-(void)viewDidLayoutSubviews{
    [super viewDidLayoutSubviews];
    self.viewLoginMailTop.constant = -self.viewLoginMail.bounds.size.height;
}

to

-(void)viewWillLayoutSubviews{
    [super viewWillLayoutSubviews];
    self.viewLoginMailTop.constant = -self.viewLoginMail.bounds.size.height;
}

So, from Did to Will

Super weird

Reeta answered 19/10, 2016 at 21:50 Comment(0)
F
0

Better solution for me.

protocol LayoutComplementProtocol {
    func didLayoutSubviews(with targetView_: UIView)
}

private class LayoutCaptureView: UIView {
    var targetView: UIView!
    var layoutComplements: [LayoutComplementProtocol] = []

    override func layoutSubviews() {
        super.layoutSubviews()

        for layoutComplement in self.layoutComplements {
            layoutComplement.didLayoutSubviews(with: self.targetView)
        }
    }
}

extension UIView {
    func add(layoutComplement layoutComplement_: LayoutComplementProtocol) {
        func findLayoutCapture() -> LayoutCaptureView {
            for subView in self.subviews {
                if subView is LayoutCaptureView {
                    return subView as? LayoutCaptureView
                }
            }
            let layoutCapture = LayoutCaptureView(frame: CGRect(x: -100, y: -100, width: 10, height: 10)) // not want to show, want to have size
            layoutCapture.targetView = self
            self.addSubview(layoutCapture)
            return layoutCapture
        }

        let layoutCapture = findLayoutCapture()
        layoutCapture.layoutComplements.append(layoutComplement_)
    }
}

Using

class CircleShapeComplement: LayoutComplementProtocol {
    func didLayoutSubviews(with targetView_: UIView) {
        targetView_.layer.cornerRadius = targetView_.frame.size.height / 2
    }
}

myButton.add(layoutComplement: CircleShapeComplement())
Fasciate answered 21/10, 2016 at 5:59 Comment(0)
C
0

Override layoutSublayers(of layer: CALayer) instead of layoutSubviews in cell subview to have correct frames

Chemurgy answered 15/2, 2017 at 14:8 Comment(0)
S
0

If you need to do something based on your view's frame - override layoutSubviews and call layoutIfNeeded

    override func layoutSubviews() {
    super.layoutSubviews()

    yourView.layoutIfNeeded()
    setGradientForYourView()
}

I had the issue with viewDidLayoutSubviews returning the wrong frame for my view, for which I needed to add a gradient. And only layoutIfNeeded did the right thing :)

Summer answered 26/11, 2019 at 13:36 Comment(0)
L
-1

As per new update in ios this is actually a bug but we can reduce this by using -

If you are using xib with autolayout in your project then you have to just update frame in autolayout setting please find image for this .enter image description here

Lively answered 1/12, 2016 at 12:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.