Layout constraints don't work in storyboard's second scene

I have a UIViewController subclass, GameController, which programatically populates its root view with a grid of UIImageViews, using auto-layout constraints and UILayoutGuides to size and position them.


If I create a basic single-view project and change the class of the scene's view controller to GameController, this works perfectly.


If I add a second scene, set the class of the second scene's view controller to GameController, change the class of the first scene's view controller back to a generic UIViewController, and add a button to the first scene's view to trigger a segue to the second scene, the grid gets created, but the subview sizing doesn't work — all the UIImage subviews get rendered at their intrinsic image size rather than the size that the constraints should be enforcing.


What's causing this behavior difference between the first and second scene?


Thanks,


Neil


(XCode 9.3; testing in the iPhone 6s Plus simulator.)

Answered by junkpile in 128615022

What are the dimensions of the second scene's view? Is it resized correctly to fit the screen?


I don't know if it's the problem or not, but in general you shouldn't do things like


        self.view.translatesAutoresizingMaskIntoConstraints = false


Normally the frame of your view controller's view should be determined by the entity displaying that view. You don't (shouldn't) know if you're in a container view controller and if so, how that container is attempting to lay out your view. Maybe it uses auto layout; maybe it doesn't.

You may need to post the code that is adding your subviews and constraints. It's difficult for anyone to guess without seeing any details.


Also take a close look at all the view controller's settings in IB (autoresize subviews checkbox, etc.) to ensure they are the same.


The view debugger in Xcode may be of use. It allows you to inspect your view hierarchy at runtime so you can see if something has a different size than you expect.

I've appended the code for GameController.swift below. To actually reproduce the bug, you need an image file Card Back.png in the project. It has a resolution of 672x1038 pixels.

  1. Create a new iOS Swift Single View Application project.
  2. Add Game Controller.swift and Card Back.png to it.
  3. In IB, change the view controller class to GameController.
  4. Drag a second view controller into the story board and change its class to GameController, too.
  5. Create a seque from the first to the second scene. (I added a Tap Gesture Recognizer to the first scene and created a Present Modally seque to the second scene; a Push/Show seque will give the same result.)
  6. Run the app on the iPhone simulator. You will see a 5x6 grid of copies of the Card Back image.
  7. Tap anywhere on the view to present the second scene.You will see part of one really big card back image.
  8. If you do Debug > View Debugging > Capture View Hierarchy and click Show Clipped Content, you will see that there is actually a grid of 672x1038 image views, which obviously does not fit on the screen.


GameController.swift

import UIKit
class GameController: UIViewController {
   
    override func viewDidLoad() {
        super.viewDidLoad()
        /
       
        self.view.translatesAutoresizingMaskIntoConstraints = false
        createCardGrid(self.view)
    }
   
    func createCardGrid(grid: UIView) {
       
        let rows = 5
        let cols = 6
       
        /*
         Create a view for each card.
         */
        let cards = (0..<rows*cols).map { c -> UIImageView in
            let card = UIImageView(image: UIImage(named: "Card Back"))
            card.contentMode = .ScaleAspectFit
            card.translatesAutoresizingMaskIntoConstraints = false
            grid.addSubview(card)
            return card
        }
       
        var constraints: [NSLayoutConstraint] = []
       
        /*
         Constrain all the card views to have the same heights and widths.
         */
        let card0 = cards.first!
        constraints += cards.dropFirst().flatMap { card in [
            card.widthAnchor.constraintEqualToAnchor(card0.widthAnchor),
            card.heightAnchor.constraintEqualToAnchor(card0.heightAnchor),
            ] }
       
        /*
         Vertical guides will manage the vertical spacing between rows. Guide [0] is between the top of
         the view and the first row, guides 1..<numRows are between rows, and guide [numRows] is between
         the last row and the bottom of the view.
         */
        let verticalGuides = (0 ... rows).map { _ -> UILayoutGuide in
            let guide = UILayoutGuide()
            grid.addLayoutGuide(guide)
            return guide
        }
       
        /*
         Inter-row spacing should be 1/10 of the card height.
         */
        constraints += verticalGuides.flatMap { guide in [
            guide.widthAnchor.constraintEqualToAnchor(grid.widthAnchor),
            guide.heightAnchor.constraintEqualToAnchor(card0.heightAnchor, multiplier: 0.10)
            ] }
       
        /*
         Horizontal guides will manage the horizontal spacing between columns. Guide [0] is between the left
         edge of the view and the first column, guides 1..<numCols are between columns, and guide [numCols]
         is between the last column and the right edge of the view.
         */
        let horizontalGuides = (0 ... cols).map { _ -> UILayoutGuide in
            let guide = UILayoutGuide()
            grid.addLayoutGuide(guide)
            return guide
        }
       
        /*
         Inter-column spacing should be 1/10 of the card width.
         */
        constraints += horizontalGuides.flatMap { guide in [
            guide.heightAnchor.constraintEqualToAnchor(grid.heightAnchor),
            guide.widthAnchor.constraintEqualToAnchor(card0.widthAnchor, multiplier: 0.10)
            ] }
       
        /*
         Attach the outer guides to the view edges.
         */
        constraints += [
            verticalGuides[0].topAnchor.constraintEqualToAnchor(grid.topAnchor),
            verticalGuides[rows].bottomAnchor.constraintEqualToAnchor(grid.bottomAnchor),
            horizontalGuides[0].leftAnchor.constraintEqualToAnchor(grid.leftAnchor),
            horizontalGuides[cols].rightAnchor.constraintEqualToAnchor(grid.rightAnchor),
        ]
       
        /*
         Attach the cards to their adjacent guides. (Lay out the list of cards in the grid in
         row-major order.)
         */
        constraints += (0 ..< rows).flatMap { r in
            (0 ..< cols).flatMap { c in [
                cards[r * cols + c].topAnchor.constraintEqualToAnchor(verticalGuides[r].bottomAnchor),
                cards[r * cols + c].bottomAnchor.constraintEqualToAnchor(verticalGuides[r+1].topAnchor),
                cards[r * cols + c].leftAnchor.constraintEqualToAnchor(horizontalGuides[c].rightAnchor),
                cards[r * cols + c].rightAnchor.constraintEqualToAnchor(horizontalGuides[c+1].leftAnchor),
                ]
            }
        }
       
        NSLayoutConstraint.activateConstraints(constraints)
    }
   
}
Accepted Answer

What are the dimensions of the second scene's view? Is it resized correctly to fit the screen?


I don't know if it's the problem or not, but in general you shouldn't do things like


        self.view.translatesAutoresizingMaskIntoConstraints = false


Normally the frame of your view controller's view should be determined by the entity displaying that view. You don't (shouldn't) know if you're in a container view controller and if so, how that container is attempting to lay out your view. Maybe it uses auto layout; maybe it doesn't.

That was the problem. Apparently the size of the root view of the second view controller is controlled by a resizing mask, so when I turned off translatesAutoresizingMaskIntoConstraints, it just resized to contain all the contained UIViews at their preferred sizes.


Thanks.

Layout constraints don't work in storyboard's second scene
 
 
Q