2

I'm new to setting up StackViews and Buttons programmatically. I am getting some strange behavior with my constraints I cannot figure out what I'm doing wrong. It feels like I'm missing something simple. Any help is greatly appreciated!

I am trying to add two buttons to a StackView to create a custom tab bar. However, when I add the constraints to the buttons they are showing up outside the bottom of StackView. It's like the top constraint of Earth image isn't working. Any ideas? See image and code below.

enter image description here

// View to put in the StackView
class ProfileBottomTabBarView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.translatesAutoresizingMaskIntoConstraints = false
        self.backgroundColor = .blue
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

// Calculate the screen height
public var screenHeight: CGFloat {
    return UIScreen.main.bounds.height
}

// StackView height set to a proporation of screen height
let stackViewHeight = screenHeight * 0.07

// Views to put in the StackView
let profileIconView = ProfileBottomTabBarView()
let actIconView = ActBottomTabBarView()
let achieveIconView = AchieveBottomTabBarView()
let growIconView = GrowBottomTabBarView()

// Buttons to put in the Views
let profileButton = UIButton(type: .system)
let actButton = UIButton(type: .system)
let achieveButton = UIButton(type: .system)
let growButton = UIButton(type: .system)

let profileButtonText = UIButton(type: .system)
let actButtonText = UIButton(type: .system)
let achieveButtonText = UIButton(type: .system)
let growButtonText = UIButton(type: .system)

// Stackview setup
lazy var stackView: UIStackView = {

    let stackV = UIStackView(arrangedSubviews: [profileIconView, actIconView, achieveIconView, growIconView])

    stackV.translatesAutoresizingMaskIntoConstraints = false
    stackV.axis = .horizontal
    stackV.spacing = 20
    stackV.distribution = .fillEqually

    return stackV
}()


override func viewDidLoad() {
    super.viewDidLoad()

view.backgroundColor = .black

    // Add StackView
    view.addSubview(stackView)

    stackView.bottomAnchor.constraint(equalTo: view.safeBottomAnchor).isActive = true
    stackView.leadingAnchor.constraint(equalTo: view.safeLeadingAnchor).isActive = true
    stackView.trailingAnchor.constraint(equalTo: view.safeTrailingAnchor).isActive = true

    // Set height of the bottom tab bar as a proportion of the screen height.
    stackView.heightAnchor.constraint(equalToConstant: stackViewHeight).isActive = true

    profileIconView.topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
    profileIconView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
    profileIconView.heightAnchor.constraint(equalToConstant: stackViewHeight).isActive = true


    // Add Buttons to the View
    profileIconView.addSubview(profileButton)
    profileIconView.addSubview(profileButtonText)

    profileButton.translatesAutoresizingMaskIntoConstraints = false
    profileButtonText.translatesAutoresizingMaskIntoConstraints = false


    // Profile Button with Earth Image Setup
    profileButton.setImage(UIImage(named: "earthIcon"), for: .normal)
    profileButton.imageView?.contentMode = .scaleAspectFit

    profileButton.topAnchor.constraint(equalTo: profileIconView.topAnchor).isActive = true
    profileButton.bottomAnchor.constraint(equalTo: profileButtonText.topAnchor).isActive = true
    profileButton.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor).isActive = true

    //Set height of icon to a proportion of the stackview height
    let profileButtonHeight = stackViewHeight * 0.8
    profileButton.heightAnchor.constraint(equalTo: profileIconView.heightAnchor, constant: profileButtonHeight).isActive = true

    profileButton.widthAnchor.constraint(equalToConstant: profileButtonHeight).isActive = true
    profileButton.imageView?.widthAnchor.constraint(equalToConstant: profileButtonHeight)
    profileButton.imageView?.heightAnchor.constraint(equalToConstant: profileButtonHeight)


    // Profile Text Button Setup

    profileButtonText.setTitle("Profile", for: .normal)
    profileButtonText.titleLabel?.font = UIFont.boldSystemFont(ofSize: 12)
    profileButtonText.setTitleColor(.white, for: .normal)

    profileButtonText.topAnchor.constraint(equalTo: profileButton.bottomAnchor).isActive = true
    profileButtonText.bottomAnchor.constraint(equalTo: profileIconView.bottomAnchor).isActive = true
    profileButtonText.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor).isActive = true

    //Set height of icon to a proportion of the stackview height
    let profileButtonTextHeight = stackViewHeight * 0.2
    profileButton.heightAnchor.constraint(equalTo: profileIconView.heightAnchor, constant: profileButtonTextHeight).isActive = true
    profileButtonText.widthAnchor.constraint(equalToConstant: 40).isActive = true

}
Ben
  • 3,346
  • 6
  • 32
  • 51
  • Offtopic but a collectionview would do the job maybe better – J. Doe Feb 03 '19 at 23:25
  • sorry but i need clarification...you want the earth symbol to be centered in the box? along with the text? what exactly are you trying to achieve...you say they are outside the bottom of the stack view... but i do not understand ... please clarify – Julian Silvestri Feb 04 '19 at 02:03
  • Sure. The "Earth" and the "Profile" buttons should both be in the box. I tried to set the top constraint of the Earth to the top of the box and the bottom of the Profile button to the bottom of the box. – Ben Feb 04 '19 at 18:27
  • If anyone comes across this later, the answer below is a cleaner way to approach. The above was also missing a couple ".isActive = true" – Ben Feb 24 '19 at 23:04

1 Answers1

5

A few things wrong with your constraints...

You're calculating heights / widths and using them as constants, but those values may (almost certainly will) change based on view lifecycle.

Better to use only related constraints. For example:

        // constrain profile image button top, centerX and width relative to the iconView
        profileButton.topAnchor.constraint(equalTo: profileIconView.topAnchor),
        profileButton.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor),
        profileButton.widthAnchor.constraint(equalTo: profileIconView.widthAnchor, multiplier: 1.0),

        // constrain profile text button bottom, centerX and width relative to the iconView
        profileButtonText.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor),
        profileButtonText.widthAnchor.constraint(equalTo: profileIconView.widthAnchor, multiplier: 1.0),
        profileButtonText.bottomAnchor.constraint(equalTo: profileIconView.bottomAnchor),

        // constrain bottom of image button to top of text button (with a padding of 4-pts, change to suit)
        profileButton.bottomAnchor.constraint(equalTo: profileButtonText.topAnchor, constant: -4.0),

        // constrain height of text button to 20% of height of iconView
        profileButtonText.heightAnchor.constraint(equalTo: profileIconView.heightAnchor, multiplier: 0.2),

To make things easier on yourself, I'd suggest creating a BottomTabBarView that handles adding and constraining your buttons:

class BottomTabBarView: UIView {

    var theImageButton: UIButton = {
        let v = UIButton()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.imageView?.contentMode = .scaleAspectFit
        return v
    }()

    var theTextButton: UIButton = {
        let v = UIButton()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.titleLabel?.font = UIFont.boldSystemFont(ofSize: 12)
        v.setTitleColor(.white, for: .normal)
        return v
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    convenience init(withImageName imageName: String, labelText: String, bkgColor: UIColor) {

        self.init()
        self.commonInit()
        theImageButton.setImage(UIImage(named: imageName), for: .normal)
        theTextButton.setTitle(labelText, for: .normal)
        backgroundColor = bkgColor

    }

    func commonInit() -> Void {
        self.translatesAutoresizingMaskIntoConstraints = false

        addSubview(theImageButton)
        addSubview(theTextButton)

        NSLayoutConstraint.activate([

            // constrain profile image button top, centerX and width of the iconView
            theImageButton.topAnchor.constraint(equalTo: topAnchor),
            theImageButton.centerXAnchor.constraint(equalTo: centerXAnchor),
            theImageButton.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0),

            // constrain profile text button bottom, centerX and width of the iconView
            theTextButton.centerXAnchor.constraint(equalTo: centerXAnchor),
            theTextButton.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0),
            theTextButton.bottomAnchor.constraint(equalTo: bottomAnchor),

            // constrain bottom of image button to top of text button
            theImageButton.bottomAnchor.constraint(equalTo: theTextButton.topAnchor, constant: -4.0),

            // set text button height to 20% of view height (instead of using intrinsic height)
            theTextButton.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.2),

            ])

    }

}

Now you can create each view with a single line, as in:

    profileIconView = BottomTabBarView(withImageName: "earthIcon", labelText: "Profile", bkgColor: .blue)

And your view controller class becomes much simpler / cleaner:

class BenViewController: UIViewController {

    // Views to put in the StackView
    var profileIconView = BottomTabBarView()
    var actIconView = BottomTabBarView()
    var achieveIconView = BottomTabBarView()
    var growIconView = BottomTabBarView()

    // Stackview setup
    lazy var stackView: UIStackView = {
        let stackV = UIStackView(arrangedSubviews: [profileIconView, actIconView, achieveIconView, growIconView])

        stackV.translatesAutoresizingMaskIntoConstraints = false
        stackV.axis = .horizontal
        stackV.spacing = 20
        stackV.distribution = .fillEqually

        return stackV
    }()


    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .black

        profileIconView = BottomTabBarView(withImageName: "earthIcon", labelText: "Profile", bkgColor: .blue)
        actIconView = BottomTabBarView(withImageName: "actIcon", labelText: "Action", bkgColor: .brown)
        achieveIconView = BottomTabBarView(withImageName: "achieveIcon", labelText: "Achieve", bkgColor: .red)
        growIconView = BottomTabBarView(withImageName: "growIcon", labelText: "Grow", bkgColor: .purple)

        // Add StackView
        view.addSubview(stackView)

        NSLayoutConstraint.activate([

            // constrain stackView to bottom, leading and trailing (to safeArea)
            stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),

            // Set height of the stackView (the bottom tab bar) as a proportion of the view height (7%).
            stackView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.07),

            ])

    }
}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • With a bottom tab bar view can you do custom animations? That is the reason I didn't set it up this way in the first place. Thanks! – Ben Feb 04 '19 at 18:24
  • And for my own learning sake, how would the device screen size ever change? I was assuming that would always be a constant so could build off of that plus the size of icons would change dynamically based on the device size. – Ben Feb 04 '19 at 18:47
  • 1. Depends on what you want to do with "custom animations" ... If they are not consistent from button to button, you can subclass the common part and add the unique stuff, e.g. `class ProfileIconView: BottomTabBarView { ... }`. 2. Using the screen height may not give you what you want if the view height is substantially different - such as having this "tab bar" as a subview of a smaller subview. In addition, you probably want to handle orientation changes - of course, that applies to both screen or view, but may be easier to manage with view. – DonMag Feb 04 '19 at 19:30
  • Thanks for the thoughts! Screen will not rotate. They would be consistent. Would just be something simple like animation when user taps a particular button. Might hide the text or raise up. Something like that. With another project I went down this path with autolayout and a Tabview and I ran into issues. That might have been more about me then what's possible though. Haha! – Ben Feb 04 '19 at 20:12