Search code examples
swiftinterface-builderxcode6autolayoutcustom-controls

Custom View in Swift: Constraints Causing Subview's Border and Background to Disappear


I have started creating custom views in Xcode using Swift. I decided to use the approach shown at http://www.thinkandbuild.it/building-a-custom-and-designabl-control-in-swift/, allowing me to set the control's attributes in Interface Builder.

Update: I have continued building the view, arranging the labels in the custom view's subview and aligning the subview with the view. I ended up using auto layout and constraints on both levels and managed to solved the width problem that way. I updated the code below accordingly.

Two problems remain:

  1. As the last step, I set txtButton.setTranslatesAutoresizingMaskIntoConstraints(false) and set the constraints for the txtButton subview => the subviews' border and background disappeared.
  2. The intrinsic size is not visible in IB, i.e. I'm getting layout issues reported in IB that suggest setting the height to 0 or 16.

Custom View Class:

import UIKit

@IBDesignable public class TextButtonView: UIView {

    @IBInspectable var borderColor: UIColor = UIColor.clearColor()
    @IBInspectable var borderWidth: CGFloat = 0
    @IBInspectable var cornerRadius: CGFloat = 0
    @IBInspectable var viewBackgroundColor: UIColor = UIColor.clearColor()

    @IBInspectable var mainText: String = ""
    @IBInspectable var mainTextSize: CGFloat = 15.0
    @IBInspectable var mainTextColor: UIColor = UIColor.blackColor()

    @IBInspectable var secText: String = ""
    @IBInspectable var secTextSize: CGFloat = 15.0
    @IBInspectable var secTextColor: UIColor = UIColor.blackColor()

    @IBInspectable var horizMargin: CGFloat = 5.0
    @IBInspectable var secHorizOffset: CGFloat = 0.0
    @IBInspectable var verticalMargin: CGFloat = 3.0
    @IBInspectable var lineSpacing: CGFloat = 10.0


    var txtButton: UIControl!
    var buttonHeight: CGFloat = 0.0


    #if TARGET_INTERFACE_BUILDER
    override func willMoveToSuperview(newSuperview: UIView?) {

        // Build the TextButton.
        txtButton = TextButton(
            borderColor: self.borderColor,
            borderWidth: self.borderWidth,
            cornerRadius: self.cornerRadius,
            viewBackgroundColor: self.viewBackgroundColor,
            mainText: self.mainText,
            mainTextSize: self.mainTextSize,
            mainTextColor: self.mainTextColor,
            secText: self.secText,
            secTextSize: self.secTextSize,
            secTextColor: self.secTextColor,
            horizMargin: self.horizMargin,
            secHorizOffset: self.secHorizOffset,
            verticalMargin: self.verticalMargin,
            lineSpacing: self.lineSpacing,
            frame: self.bounds)

        // Add the TextButton as subview of this view
        self.addSubview(txtButton)

        // Remember height for setting intrinsic content size.
        buttonHeight = txtButton.frame.size.height

        // Set remaining attributes for the container view.
        self.backgroundColor = UIColor.clearColor()

        // Setting constraints for the subview.
        txtButton.setTranslatesAutoresizingMaskIntoConstraints(false)
        self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: 0))
        self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0))
        self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0))
    }

    #else
    override public func awakeFromNib() {
        super.awakeFromNib()

        // Build the TextButton.
        txtButton = TextButton(
            borderColor: self.borderColor,
            borderWidth: self.borderWidth,
            cornerRadius: self.cornerRadius,
            viewBackgroundColor: self.viewBackgroundColor,
            mainText: self.mainText,
            mainTextSize: self.mainTextSize,
            mainTextColor: self.mainTextColor,
            secText: self.secText,
            secTextSize: self.secTextSize,
            secTextColor: self.secTextColor,
            horizMargin: self.horizMargin,
            secHorizOffset: self.secHorizOffset,
            verticalMargin: self.verticalMargin,
            lineSpacing: self.lineSpacing,
            frame: self.bounds)

        // Add the TextButton as subview of this view.
        self.addSubview(txtButton)

        // Remember height for setting intrinsic content size.
        buttonHeight = txtButton.frame.size.height

        // Set remaining attributes for the container view.
        self.backgroundColor = UIColor.clearColor()

        // Setting constraints for the subview.
        txtButton.setTranslatesAutoresizingMaskIntoConstraints(false)
        self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: 0))
        self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0))
        self.addConstraint(NSLayoutConstraint(item: txtButton, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0))
    }
    #endif

    override public func intrinsicContentSize() -> CGSize {

        return CGSize(width: 250, height: buttonHeight)
    }
}

Control:

import UIKit

class TextButton: UIControl {

    // Designable properties and default values.
    var borderColor: UIColor?
    var borderWidth: CGFloat?
    var cornerRadius: CGFloat?
    var viewBackgroundColor: UIColor?
    var mainText: String?
    var mainTextSize: CGFloat?
    var mainTextColor: UIColor?
    var secText: String?
    var secTextSize: CGFloat?
    var secTextColor: UIColor?
    var horizMargin: CGFloat?
    var secHorizOffset: CGFloat?
    var verticalMargin: CGFloat?
    var lineSpacing: CGFloat?

    convenience init(
        borderColor: UIColor,
        borderWidth: CGFloat,
        cornerRadius: CGFloat,
        viewBackgroundColor: UIColor,
        mainText: String,
        mainTextSize: CGFloat,
        mainTextColor: UIColor,
        secText: String,
        secTextSize: CGFloat,
        secTextColor: UIColor,
        horizMargin: CGFloat,
        secHorizOffset: CGFloat,
        verticalMargin: CGFloat,
        lineSpacing: CGFloat,
        frame: CGRect) {

            self.init(frame: frame)

            self.mainText = mainText
            self.mainTextSize = mainTextSize

            // Button margins.
            self.horizMargin = horizMargin
            self.verticalMargin = verticalMargin
            self.secHorizOffset = secHorizOffset
            self.lineSpacing = lineSpacing

            // Define the Fonts
            let mainFont = UIFont(name: "Helvetica Neue", size: mainTextSize)
            let secFont = UIFont(name: "Helvetica Neue", size: secTextSize)

            // Create main label.
            let mainLabel: UILabel = UILabel()
            mainLabel.backgroundColor = UIColor.clearColor()
            mainLabel.textColor = mainTextColor
            mainLabel.textAlignment = .Left
            mainLabel.font = mainFont
            mainLabel.text = mainText

            // Calculate the main label's height.
            var mainLabelDummy: UILabel = mainLabel
            mainLabelDummy.sizeToFit()
            var mainLabelHeight: CGFloat = mainLabelDummy.frame.size.height

            // Create secondary label.
            let secLabel: UILabel = UILabel()
            secLabel.backgroundColor = UIColor.clearColor()
            secLabel.textColor = secTextColor
            secLabel.textAlignment = .Left
            secLabel.font = secFont
            secLabel.text = secText

            // Calculate the secondary label's height.
            var secLabelDummy: UILabel = secLabel
            secLabelDummy.sizeToFit()
            var secLabelHeight: CGFloat = secLabelDummy.frame.size.height

            // Add labels to view.
            addSubview(mainLabel)
            addSubview(secLabel)

            // Set constraints for labels.
            mainLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
            secLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
            self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: horizMargin))
            self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0 - horizMargin))
            self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: verticalMargin))
            self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: horizMargin + secHorizOffset))
            self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0 - horizMargin))
            self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Top, relatedBy: .Equal, toItem: mainLabel, attribute: .Bottom, multiplier: 1, constant: lineSpacing))

            // Adjust frame to match content.
            self.frame.size.height =
                2 * verticalMargin
                + 2 * borderWidth
                + lineSpacing
                + mainLabelHeight
                + secLabelHeight

            // Set remaining view properties.
            self.layer.borderColor = borderColor.CGColor
            self.layer.borderWidth = borderWidth
            self.layer.cornerRadius = cornerRadius
            self.backgroundColor = viewBackgroundColor
    }

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

    }

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

Solution

  • After further reading I came up with the following solution, simplifying by getting rid of the subview:

    import UIKit
    
    @IBDesignable class TextButtonView: UIControl {
    
        // Properties accessible in Interface Builder.
        @IBInspectable var borderColor: UIColor = UIColor.clearColor() {didSet { updateUI() }}
        @IBInspectable var borderWidth: CGFloat = 0 {didSet { updateUI() }}
        @IBInspectable var cornerRadius: CGFloat = 0 {didSet { updateUI() }}
        @IBInspectable var backgrColor: UIColor = UIColor.clearColor() {didSet { updateUI() }}
        @IBInspectable var mainText: String = "" {didSet { updateUI() }}
        @IBInspectable var mainTextSize: CGFloat = 20.0 {didSet { updateUI() }}
        @IBInspectable var mainTextColor: UIColor = UIColor.blackColor() {didSet { updateUI() }}
        @IBInspectable var secText: String = "" {didSet { updateUI() }}
        @IBInspectable var secTextSize: CGFloat = 12.0 {didSet { updateUI() }}
        @IBInspectable var secTextColor: UIColor = UIColor.blackColor() {didSet { updateUI() }}
        @IBInspectable var horizMargin: CGFloat = 0.0 {didSet { updateUI() }}
        @IBInspectable var secHorizOffset: CGFloat = 0.0 {didSet { updateUI() }}
        @IBInspectable var verticalMargin: CGFloat = 0.0 {didSet { updateUI() }}
        @IBInspectable var lineSpacing: CGFloat = 0.0 {didSet { updateUI() }}
    
        var mainLabel: UILabel!
        var secLabel: UILabel!
        var textButtonHeight: CGFloat = 0.0
        var fontName: String = "Helvetica Neue"
    
        required init(coder: NSCoder) {
            super.init(coder:coder)
            setupUI()
        }
    
        override init(frame: CGRect) {
            super.init(frame:frame)
            setupUI()
        }
    
        func setupUI() {
    
            // Set up static properties.
            mainLabel = UILabel()
            mainLabel.backgroundColor = UIColor.clearColor()
            mainLabel.textAlignment = .Left
            secLabel = UILabel()
            secLabel.backgroundColor = UIColor.clearColor()
            secLabel.textAlignment = .Left
    
            // Add labels to view.
            addSubview(mainLabel)
            addSubview(secLabel)
    
            // Update variable properties.
            updateUI()
        }
    
        func updateUI() {
    
            // Set borders and background.
            self.layer.borderColor = borderColor.CGColor
            self.layer.borderWidth = borderWidth
            self.layer.cornerRadius = cornerRadius
            self.layer.backgroundColor = backgrColor.CGColor
    
            // Update main label.
            mainLabel.textColor = mainTextColor
            mainLabel.font = UIFont(name: fontName, size: mainTextSize)
            mainLabel.text = mainText
    
            // Update secondary label.
            secLabel.textColor = secTextColor
            secLabel.font = UIFont(name: fontName, size: secTextSize)
            secLabel.text = secText
    
            // Calculate view's height.
            var mainLabelCopy: UILabel = mainLabel
            mainLabelCopy.sizeToFit()
            var mainLabelHeight: CGFloat = mainLabelCopy.frame.size.height
            var secLabelCopy: UILabel = secLabel
            secLabelCopy.sizeToFit()
            var secLabelHeight: CGFloat = secLabelCopy.frame.size.height
            textButtonHeight =
                2 * verticalMargin
                + 2 * borderWidth
                + lineSpacing
                + mainLabelHeight
                + secLabelHeight
    
            setNeedsUpdateConstraints()
        }
    
        override func updateConstraints() {
    
            // Set constraints for labels.
            setTranslatesAutoresizingMaskIntoConstraints(false)
            mainLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
            secLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
            removeConstraints(constraints())
            self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: horizMargin))
            self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0 - horizMargin))
            self.addConstraint(NSLayoutConstraint(item: mainLabel, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: verticalMargin))
            self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Left, relatedBy: .Equal, toItem: self, attribute: .Left, multiplier: 1, constant: horizMargin + secHorizOffset))
            self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Right, relatedBy: .Equal, toItem: self, attribute: .Right, multiplier: 1, constant: 0 - horizMargin))
            self.addConstraint(NSLayoutConstraint(item: secLabel, attribute: .Top, relatedBy: .Equal, toItem: mainLabel, attribute: .Bottom, multiplier: 1, constant: lineSpacing))
            self.addConstraint(NSLayoutConstraint(item: self, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: textButtonHeight))
    
            super.updateConstraints()
        }
    }
    

    At runtime this now works as expected.

    In Interface Builder it still throws some warnings about misplaced views that don't really make sense. The reported actual coordinates seem to be incorrect and neither manual nor automatic correction in IB fixes it.

    Warning Actual Measures

    Nevertheless, this allows me to proceed, so I post it as a suggested answer.