Don’t do premature layout

This comes up on Stack Overflow often enough that it seems worth a little article here. The issue is this: You set the frame of a view or layer in code, but it doesn’t seem to come out in the right place. The reason is usually that you’re doing this too soon.

When your app gets started or when a new view controller is loaded and its view appears, you start getting events before layout takes place. During this time, your view do not yet have their “real” positions and sizes. That’s true especially if you’re using autolayout, but it can be true even if you’re not, because the size at which views are loaded from the storyboard is not necessarily their “real” size.

A view example

Here’s a recent Stack Overflow example involving just views:

class FooterActionView : UIView {
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    func commonInit() {
        guard let view = loadViewFromNib() else { return }
        view.frame = self.bounds // *
        self.addSubview(view)
    }
    // ...
}

This programmer is using his init implementations to call a commonInit method that gets hold of a subview (somehow) and set its frame, saying view.frame = self.bounds. That isn’t working correctly on some iPhone screen sizes.

The reason is that we don’t yet know what self.bounds really is going to be. This code is running too early. During init, the view has some bounds, but it doesn’t yet have the bounds it will have once everything gets laid out properly.

So what’s the right solution? One possibility is to come back during layout and set the subview’s frame again so that it matches when the view’s bounds are now. Let’s keep a reference to the subview so we can do that:

    func commonInit() {
        guard let view = loadViewFromNib() else { return }
        view.frame = self.bounds
        self.theSubview = view // *
        self.addSubview(view)
    }
    func layoutSubviews() {
        super.layoutSubviews()
        self.theSubview.frame = self.bounds
    }

Another solution would have been to give view.frame the ability to resize itself when the superview self resizes:

    func commonInit() {
        guard let view = loadViewFromNib() else { return }
        view.frame = self.bounds
        view.autoresizingMask = [.flexibleWidth, .flexibleHeight] // *
        self.addSubview(view)
    }

Do note, however, that that second approach wouldn’t work if we were dealing with a layer instead of a view, because layers don’t have autoresizing in iOS. The first approach would work, and is what you need to do if you have a layer whose size has to be coordinated with the size of its superlayer.

View controllers

The usual way to make this same mistake in a view controller’s code is to configure a view or layer’s frame in viewDidLoad.

Again, that is too early. You don’t yet know the real frame or bounds of any of your views, so you can’t yet set the frame of a view or layer with respect to that value. You want to wait until viewDidLayoutSubviews before performing that sort of setting.

Be careful not to add subviews twice

A view’s layoutSubviews or a view controller’s viewDidLayoutSubviews is an appropriate place to set the frame of a subview or sublayer, including a mask.

But it is not an appropriate place to add a subview or sublayer. The reason is that these methods are called many times during the life of your app, and you don’t want to end up adding the same subview or sublayer over and over.

So follow this rule: add subviews or sublayers in some early one-time method, like a view’s init or didMoveToSuperview, or a view controller’s viewDidLoad. But do layout on subviews or sublayers in a layout method!