How would I create an UIView / UILabel with vertical text flow which would look like the red view of this example screen?
I have read about view.transform = CGAffineTransform(... which allows for easy rotation, BUT it would break the auto-layout constraints.
I would be happy to use a third-party library, but I cannot find any.
As noted in Apple's docs:
In iOS 8.0 and later, the transform property does not affect Auto Layout. Auto layout calculates a view’s alignment rectangle based on its untransformed frame.
So, to get transformed views to "play nice" with auto layout, we need to - in effect - tell constraints to use the opposite axis.
For example, if we embed a UILabel
in a UIView
and rotate the label 90-degrees, we want to constrain the "container" view's Width to the label's Height and its Height to the label's Width.
Here's a sample VerticalLabelView
view subclass:
class VerticalLabelView: UIView {
public var numberOfLines: Int = 1 {
didSet {
label.numberOfLines = numberOfLines
}
}
public var text: String = "" {
didSet {
label.text = text
}
}
// vertical and horizontal "padding"
// defaults to 16-ps (8-pts on each side)
public var vPad: CGFloat = 16.0 {
didSet {
h.constant = vPad
}
}
public var hPad: CGFloat = 16.0 {
didSet {
w.constant = hPad
}
}
// because the label is rotated, we need to swap the axis
override func setContentHuggingPriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) {
label.setContentHuggingPriority(priority, for: axis == .horizontal ? .vertical : .horizontal)
}
// this is just for development
// show/hide border of label
public var showBorder: Bool = false {
didSet {
label.layer.borderWidth = showBorder ? 1 : 0
label.layer.borderColor = showBorder ? UIColor.red.cgColor : UIColor.clear.cgColor
}
}
public let label = UILabel()
private var w: NSLayoutConstraint!
private var h: NSLayoutConstraint!
private var mh: NSLayoutConstraint!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
addSubview(label)
label.backgroundColor = .clear
label.translatesAutoresizingMaskIntoConstraints = false
// rotate 90-degrees
let angle = .pi * 0.5
label.transform = CGAffineTransform(rotationAngle: angle)
// so we can change the "padding" dynamically
w = self.widthAnchor.constraint(equalTo: label.heightAnchor, constant: hPad)
h = self.heightAnchor.constraint(equalTo: label.widthAnchor, constant: vPad)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
label.centerYAnchor.constraint(equalTo: self.centerYAnchor),
w, h,
])
}
}
I've added a few properties to allow the view to be treated like a label, so we can do:
let v = VerticalLabelView()
// "pass-through" properties
v.text = "Some text which will be put into the label."
v.numberOfLines = 0
// directly setting properties
v.label.textColor = .red
This could, of course, be extended to "pass through" all label properties we need to use so we wouldn't need to reference the .label
directly.
This VerticalLabelView
can now be used much like a normal UILabel
.
Here are two examples - they both use this BaseVC
to setup the views:
class BaseVC: UIViewController {
let greenView: UIView = {
let v = UIView()
v.backgroundColor = .green
return v
}()
let normalLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
let lYellow: VerticalLabelView = {
let v = VerticalLabelView()
v.backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 0.5, alpha: 1.0)
v.numberOfLines = 0
return v
}()
let lRed: VerticalLabelView = {
let v = VerticalLabelView()
v.backgroundColor = UIColor(red: 1.0, green: 0.5, blue: 0.5, alpha: 1.0)
v.numberOfLines = 0
return v
}()
let lBlue: VerticalLabelView = {
let v = VerticalLabelView()
v.backgroundColor = UIColor(red: 0.3, green: 0.8, blue: 1.0, alpha: 1.0)
v.numberOfLines = 1
return v
}()
let container: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
let strs: [String] = [
"Multiline Vertical Text",
"Vertical Text",
"Overflow Vertical Text",
]
// default UILabel
normalLabel.text = "Regular UILabel wrapping text"
// add the normal label to the green view
greenView.addSubview(normalLabel)
// set text of vertical labels
for (s, v) in zip(strs, [lYellow, lRed, lBlue]) {
v.text = s
}
[container, greenView, normalLabel, lYellow, lRed, lBlue].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add greenView to the container
container.addSubview(greenView)
// add container to self's view
view.addSubview(container)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain container Top and CenterX
container.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
container.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// comment next line to allow container subviews to set the height
container.heightAnchor.constraint(equalToConstant: 260.0),
// comment next line to allow container subviews to set the width
container.widthAnchor.constraint(equalToConstant: 160.0),
// green view at Top, stretched full width
greenView.topAnchor.constraint(equalTo: container.topAnchor, constant: 0.0),
greenView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
greenView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
// constrain normal label in green view
// with 8-pts "padding" on all 4 sides
normalLabel.topAnchor.constraint(equalTo: greenView.topAnchor, constant: 8.0),
normalLabel.leadingAnchor.constraint(equalTo: greenView.leadingAnchor, constant: 8.0),
normalLabel.trailingAnchor.constraint(equalTo: greenView.trailingAnchor, constant: -8.0),
normalLabel.bottomAnchor.constraint(equalTo: greenView.bottomAnchor, constant: -8.0),
])
}
}
The first example - SubviewsExampleVC
- adds each as a subview, and then we add constraints between the views:
class SubviewsExampleVC: BaseVC {
override func viewDidLoad() {
super.viewDidLoad()
// add vertical labels to the container
[lYellow, lRed, lBlue].forEach { v in
container.addSubview(v)
}
NSLayoutConstraint.activate([
// yellow label constrained to Bottom of green view
lYellow.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
// Leading to container Leading
lYellow.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
// red label constrained to Bottom of green view
lRed.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
// Leading to yellow label Trailing
lRed.leadingAnchor.constraint(equalTo: lYellow.trailingAnchor, constant: 0.0),
// blue label constrained to Bottom of green view
lBlue.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
// Leading to red label Trailing
lBlue.leadingAnchor.constraint(equalTo: lRed.trailingAnchor, constant: 0.0),
// if we want the labels to fill the container width
// blue label Trailing constrained to container Trailing
lBlue.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
// using constraints to set the vertical label heights
lYellow.heightAnchor.constraint(equalToConstant: 132.0),
lRed.heightAnchor.constraint(equalTo: lYellow.heightAnchor),
lBlue.heightAnchor.constraint(equalTo: lYellow.heightAnchor),
])
// as always, we need to control which view(s)
// hug their content
// so, for example, if we want the Yellow label to "stretch" horizontally
lRed.setContentHuggingPriority(.required, for: .horizontal)
lBlue.setContentHuggingPriority(.required, for: .horizontal)
// or, for example, if we want the Red label to "stretch" horizontally
//lYellow.setContentHuggingPriority(.required, for: .horizontal)
//lBlue.setContentHuggingPriority(.required, for: .horizontal)
}
}
The second example = StackviewExampleVC
- adds each as an arranged subview of a UIStackView
:
class StackviewExampleVC: BaseVC {
override func viewDidLoad() {
super.viewDidLoad()
// horizontal stack view
let stackView = UIStackView()
// add vertical labels to the stack view
[lYellow, lRed, lBlue].forEach { v in
stackView.addArrangedSubview(v)
}
stackView.translatesAutoresizingMaskIntoConstraints = false
// add stack view to container
container.addSubview(stackView)
NSLayoutConstraint.activate([
// constrain stack view Top to green view Bottom
stackView.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 0.0),
// Leading / Trailing to container Leading / Trailing
stackView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
// stack view height
stackView.heightAnchor.constraint(equalToConstant: 132.0),
])
// as always, we need to control which view(s)
// hug their content
// so, for example, if we want the Yellow label to "stretch" horizontally
lRed.setContentHuggingPriority(.required, for: .horizontal)
lBlue.setContentHuggingPriority(.required, for: .horizontal)
// or, for example, if we want the Red label to "stretch" horizontally
//lYellow.setContentHuggingPriority(.required, for: .horizontal)
//lBlue.setContentHuggingPriority(.required, for: .horizontal)
}
}
Both examples produce this output:
Please note: this is Example Code Only - it is not intended to be, nor should it be considered to be, Production Ready