Search code examples
iosswiftautolayoutvisual-format-language

Undesired button height being calculated using Visual Formatting Language


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.


Solution

  • 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)