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:
txtButton.setTranslatesAutoresizingMaskIntoConstraints(false)
and set the constraints for the txtButton subview => the subviews' border and background disappeared.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")
}
}
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.
Nevertheless, this allows me to proceed, so I post it as a suggested answer.