Search code examples
swiftuikit

Why is a UIView's frame correctly resized on rotation screen if held by an UIViewController?


Given this example:

When I add a view programatically and set its frame...

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let subview = UIView()
        subview.backgroundColor = .red
        subview.frame = view.frame
        view.addSubview(subview)
    }
}

enter image description here

It doesn't get resized when I rotate the screen

enter image description here

However, when I add a view that is generated by a view controller like so:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let subview = UIViewController().view!
        subview.backgroundColor = .red
        subview.frame = view.frame
        view.addSubview(subview)
    }
}

It gets resized when I rotate the screen.

enter image description here

What is happening? Why those two views handle trait collection changes differently?

Does it implies that I should use constraints no matter what (even in custom presentation modal transitions and push navigation transitions, on which Apple's doc seems to recommend to use frames?)


Solution

  • This is very odd...

    let subviewFromVC = UIViewController().view!
    

    You are creating a new instance of a UIViewController() (NOT related to the current view controller), and grabbing a reference to its .view.

    Then you add that view as a subview, and set its frame.

    The "base" view of a UIViewController comes with:

    .autoresizingMask = [.flexibleWidth, .flexibleHeight]
    

    so it "auto-resizes" ...

    However, when you do this:

    let subview = UIView()
    subview.backgroundColor = .red
    subview.frame = view.frame
    view.addSubview(subview)
    

    By default, subview is created with:

    .autoresizingMask = []
    

    so it doesn't "auto-resize" ...

    If you want subview to auto-resize based on the frame you set, you need to set that mask:

    let subview = UIView()
    subview.backgroundColor = .red
    subview.frame = view.frame
    
    subview.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    
    view.addSubview(subview)
    

    If you are creating a new subview - or, probably, multiple subviews - that auto-resize and automatically adjust their positions, you will need a combination of the various .autoresizingMask flags:

    .flexibleWidth, .flexibleHeight,
    .flexibleTopMargin, .flexibleBottomMargin,
    .flexibleLeftMargin, .flexibleRightMargin,
    

    Without seeing the UI layout you're going for, can't offer much else -- but that should at least get you on your way.


    Edit - mixing frame and constraints...

    The reason we see something like viewToPresent.frame.origin.x = in a custom UIPresentationController is because that view is instantiated with .autoresizingMask = [.flexibleWidth, .flexibleHeight]. So, to "slide it in from the left" for example, we'd animate it along these lines:

    viewToPresent.frame.origin.x = -viewToPresent.frame.width
    
    UIView.animate(withDuration: 0.3, animations: {
        viewToPresent.frame.origin.x = 0.0
    })
    

    That can also be done with constraints, but for this use-case setting frame values is (perhaps) easier.

    For the "dimming view," though, it would seem to make more sense to use constraints, since its size/position won't be animated.

    What often is not obvious is that:

    someView.translatesAutoresizingMaskIntoConstraints = true
    

    does Not mean auto-layout is not being used... it means that the view's .autoresizingMask is being translated into constraints.