Undesired button height being calculated using Visual Formatting Language

111 Views Asked by At

I have a UIViewController with four UIButtons (2 x 2) on it that I laid out in interface builder that worked perfectly. I'm going to have a free and ad-supported version of my app, so I need to redo that scene to load based on whether the app is a paid or ad-supported version. Based on that, I'm attempting to use Visual Formatting Language to lay out the view. I'm getting incorrect values for my UIButton heights despite accounting for them when I calculate them. I can't figure out my mistake (or omission?).

Here's a screenshot of my interface builder. enter image description here

I do not have constraints on my buttons in interface builder, but I do have IBOutlets wired to MyViewController. MyViewController is in a navigation controller and has a tab bar at the bottom.

I created a method called layoutButtons that I call in viewDidLoad just after super.viewDidLoad(). Here it is:

func layoutButtons() {
    // Configure layout constraints

    // Remove interface builder constraints from storyboard
    view.removeConstraints(view.constraints)

    // create array to dump constraints into
    var allConstraints = [NSLayoutConstraint]()

    // determine screen size
    let screenSize: CGRect = UIScreen.mainScreen().bounds

    let navBarRect = navigationController!.navigationBar.frame
    let navBarHeight = navBarRect.height

    let tabBarRect = tabBarController!.tabBar.frame
    let tabBarHeight: CGFloat = tabBarRect.height

    // calculate button width based on screen size

    // padding for left + middle + right = 8.0 + 8.0 + 8.0 = 24.0
    let buttonWidth = (screenSize.width - 24.0) / 2

    /*
    My buttons are extending under the top & bottom layout guides despite accounting
    for them when I set the buttonHeight.
    */

    // padding for top + middle + bottom = 8.0 + 8.0 + 8.0 = 24.0
    let buttonHeight = (screenSize.height - topLayoutGuide.length - bottomLayoutGuide.length - 24.0) / 2

    // create dictionary of metrics
    let metrics = ["buttonWidth": buttonWidth,
        "buttonHeight": buttonHeight,
        "navBarHeight": navBarHeight,
        "tabBarHeight": tabBarHeight,
        "bannerAdWidth": bannerAdWidth,
        "bannerAdHeight": bannerAdHeight]

    // create dictionary of views
    var views: [String : AnyObject] = ["firstButton": firstButton,
        "secondButton": secondButton,
        "thirdButton": thirdButton,
        "fourthButton": fourthButton,
        "topLayoutGuide": topLayoutGuide,
        "bottomLayoutGuide": bottomLayoutGuide]

    let topRowHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
        "H:|-[firstButton(buttonWidth)]-[secondButton(buttonWidth)]-|",
        options: [.AlignAllCenterY],
        metrics: metrics,
        views: views)
    allConstraints += topRowHorizontalConstraints

    let bottomRowHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
        "H:|-[thirdButton(buttonWidth)]-[fourthButton(buttonWidth)]-|",
        options: [.AlignAllCenterY],
        metrics: metrics,
        views: views)
    allConstraints += bottomRowHorizontalConstraints

    let leftColumnVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
        "V:|[topLayoutGuide]-[firstButton(buttonHeight)]-[thirdButton(buttonHeight)]-[bottomLayoutGuide]|",
        options: [],
        metrics: metrics,
        views: views)
    allConstraints += leftColumnVerticalConstraints

    let rightColumnVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
        "V:|[topLayoutGuide]-[secondButton(buttonHeight)]-[fourthButton(buttonHeight)]-[bottomLayoutGuide]|",
        options: [],
        metrics: metrics,
        views: views)
    allConstraints += rightColumnVerticalConstraints

    NSLayoutConstraint.activateConstraints(allConstraints)
}

I've fiddled with my buttonHeight variable, but every iteration I've tried results in the buttons extending under the topLayoutGuide and bottomLayoutGuide. Here's what it looks like at runtime:

enter image description here

I welcome any suggestions where to look for my mistake. Thank you for reading.

1

There are 1 best solutions below

0
On BEST ANSWER

I originally followed a Ray Wenderlich's Visual Format Language tutorial and the tutorial's setup was similar to mine in that the subViews were on a storyboard and wired to MyViewController with IBOutlets.

Out of desperation, I nuked the storyboard and created the UIButtons in code. Along the way, I discovered my problem was I was setting the image on the button before it was laid out. The moral of the story is don't set images on UIButtons until they're laid out!

Below is the code called from a method in my viewDidLoad that lays out a 2 x 2 grid of buttons:

    // Configure Buttons
    firstButton.translatesAutoresizingMaskIntoConstraints = false
    // code to customize button

    secondButton.translatesAutoresizingMaskIntoConstraints = false
    // code to customize button

    thirdButton.translatesAutoresizingMaskIntoConstraints = false
    // code to customize button

    fourthButton.translatesAutoresizingMaskIntoConstraints = false
    // code to customize button

    // Add buttons to the subview
    view.addSubview(firstButton)
    view.addSubview(secondButton)
    view.addSubview(thirdButton)
    view.addSubview(fourthButton)

    // create views dictionary
    var allConstraints = [NSLayoutConstraint]()

    let views: [String : AnyObject] = ["firstButton": firstButton,
        "secondButton": secondButton,
        "thirdButton": thirdButton,
        "fourthButton": fourthButton,
        "topLayoutGuide": topLayoutGuide,
        "bottomLayoutGuide": bottomLayoutGuide]

    // Calculate width and height of buttons
    // 24.0 = left padding + middle padding + right padding
    let buttonWidth = (screenSize.width - 24.0) / 2
    let buttonHeight = (screenSize.height - topLayoutGuide.length - bottomLayoutGuide.length - 24.0) / 2

    // Create a dictionary of metrics
    let metrics = ["buttonWidth": buttonWidth,
    "buttonHeight" : buttonHeight]

    let topVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
        "V:|[topLayoutGuide]-[firstButton(buttonHeight)]",
        options: [],
        metrics: metrics,
        views: views)
    allConstraints += topVerticalConstraints

    let topRowHorizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
        "H:|-[firstButton(buttonWidth)]-[secondButton(==firstButton)]-|",
        options: NSLayoutFormatOptions.AlignAllCenterY,
        metrics: metrics,
        views: views)
    allConstraints += topRowHorizontalConstraints

    let bottomRowHorizontalContraints = NSLayoutConstraint.constraintsWithVisualFormat(
        "H:|-[thirdButton(buttonWidth)]-[fourthButton(==thirdButton)]-|",
        options: NSLayoutFormatOptions.AlignAllCenterY,
        metrics: metrics,
        views: views)
    allConstraints += bottomRowHorizontalContraints

    let leftColumnVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
        "V:[firstButton]-[thirdButton(==firstButton)]-[bottomLayoutGuide]-|",
        options: [],
        metrics: metrics,
        views: views)
    allConstraints += leftColumnVerticalConstraints

    let rightColumnVerticalConstriants = NSLayoutConstraint.constraintsWithVisualFormat(
        "V:[secondButton(buttonHeight)]-[fourthButton(==secondButton)]-[bottomLayoutGuide]-|",
        options: [],
        metrics: metrics,
        views: views)
    allConstraints += rightColumnVerticalConstriants

    NSLayoutConstraint.activateConstraints(allConstraints)

    // ** DON'T SET IMAGES ON THE BUTTONS UNTIL THE BUTTONS ARE LAID OUT!!!**
    firstButton.imageView?.contentMode = UIViewContentMode.ScaleAspectFit
    firstButton.setImage(UIImage(named: "first.png"), forState: .Normal)

    secondButton.imageView?.contentMode = UIViewContentMode.ScaleAspectFit
    secondButton.setImage(UIImage(named: "second.png"), forState: .Normal)

    thirdButton.imageView?.contentMode = UIViewContentMode.ScaleAspectFit
    thirdButton.setImage(UIImage(named: "third.png"), forState: .Normal)

    fourthButton.imageView?.contentMode = UIViewContentMode.ScaleAspectFit
    fourthButton.setImage(UIImage(named: "fourth.png"), forState: .Normal)