Search code examples
iosuibuttonuikit

UIButton not render exactly as XIB's contentVerticalAlignment value


I want to button 's top is align to the button's titleLabel's top , so I set the content vertical alignment to top in xib . this is works well in xib , but after build , the titleLabel seems still layout with center vertical alignment . What did I miss?

enter image description here


Solution

  • First, a comment: What you see in Storyboard / Interface Builder:

    • is not always exactly what UIKit renders in the Simulator
    • which is not always exactly what UIKit renders on a Device

    This is why we test, test, test... on different simulators and devices.


    So, what's going on here?

    UIKit uses the frame of the button's .titleLabel to determine the button's intrinsic size.

    For a default button, with no explicit width or height set, UIKit insets the title label by 6-pts on the Top and Bottom.

    Here are 2 buttons - both with no Height constraint. Button 1 is the default Content Alignment of center/center, Button 2 is set to Left/Top. The button title label background is cyan, so we can easily see its frame.

    Storyboard / IB:

    enter image description here

    Runtime:

    enter image description here

    Debug View Hierarchy (note the label frame vs the button frame):

    enter image description here

    So, with an 18-pt system font, the title label height is 21.0 ... add 6-pts top and bottom and the button frame height is 33-pts.

    It doesn't matter whether you set the Control Alignment to Top / Center / Bottom or Fill ... UIKit still takes the label height and adds 6-pts Top and Bottom "padding."

    What to do to get actual Top alignment? Let's look at a couple approaches.

    Here are 6 buttons in a stack view:

    enter image description here

    Button 1 is at the default center/center, with no Height constraint.

    Button 2 as we've seen, has Control Alignment Left / Top ... but has no effect on the vertical alignment.

    Button 3 is also Left/Top, but let's give it an explicit Height (we'll use 80 to make things obvious). Looks better - but there is still 6-pts of "padding" added to the top of the title label.

    Now, you may have seen this at the top of the Size Inspector panel:

    enter image description here

    This looks promising! Let's set the Title Insets Top to Zero!

    Whoops -- it's already Zero?!?!?!?

    Same thing with the Content Insets!

    Turns out, if the the edge insets are at the default, the padding is added anyway.

    We could try setting the Title Inset Top to -6 and, because labels center the text vertically, we'll also have to set the Bottom inset to 6. This works... but we may not want to rely on that value of 6 to be accurate in future iOS versions.

    Button 4 - lets try Content Insets -> Bottom: 1 ... and it looks like we're on our way! The label is now top-aligned with the button frame!

    So...

    Button 5 - remove the Height: 80 constraint so we can use the default button height. D'oh! The button frame height is now title-label-height plus zero-top plus one-bottom...

    Button 6 - finally, we'll use Content Insets -> Bottom: 1 with an explicit Height constraint of 33 (the default button height).


    So... what if we don't want to set an explicit Height? Perhaps we're going to change the title label's font size later? Or we run into some other issue?

    You could use a custom UIButton subclass.

    Here's an example, marked as @IBDesignable so we can see its layout in Storyboard / IB:

    @IBDesignable
    class MyTopLeftAlignedButton: UIButton {
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        override func prepareForInterfaceBuilder() {
            super.prepareForInterfaceBuilder()
            commonInit()
        }
        func commonInit() {
            self.contentVerticalAlignment = .top
            self.contentHorizontalAlignment = .left
            // if we want to see the title label frame
            //titleLabel?.backgroundColor = .cyan
        }
        override var bounds: CGRect {
            didSet {
                if let v = titleLabel {
                    var t = titleEdgeInsets
                    let h = (bounds.height - v.frame.height) * 0.5
                    t.top = -h
                    t.bottom = h
                    titleEdgeInsets = t
                }
            }
        }
    }
    

    Edit - response to comment...

    If your goal is to match the "Button 5" style - where the button height matches the title label height - best bet is probably...

    Select the button, then in the Size Inspector panel use these settings:

    enter image description here

    The 0.1 Top and Bottom values will override the default 6-pt Top/Bottom "padding," reducing it to effectively Zero.