I have a 4 x 1 UILabel matrix in a horizontal stackview I would like columns 1 and 3 to be the same width BUT narrower than columns 2 and 4. I have tried .fillproportionally, and others. I understood that this was possible... however, a solution evades me and Google hasn't turned anything up. Any help or a point in the right direction would be helpful. Thank you. I am not using Storyboards.
You can create your grid:
as you described:
C1-w == 1/3 of C2-w
C3-w == C1-w
C4-w == C2-w
pretty much with that description:
with the stack view .distribution = .fill
Here's a complete example:
class ColumnStackViewController: UIViewController {
let outerStack: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 8
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// create "header row" stack view
let headerRowStack = UIStackView()
headerRowStack.axis = .horizontal
headerRowStack.spacing = 4
// create 4 labels for header row
let headerC1 = UILabel()
let headerC2 = UILabel()
let headerC3 = UILabel()
let headerC4 = UILabel()
headerC1.text = "C1"
headerC2.text = "C2"
headerC3.text = "C3"
headerC4.text = "C4"
[headerC1, headerC2, headerC3, headerC4].forEach { v in
// give each "header" label a background color, so we can see the frames
v.backgroundColor = .yellow
v.textAlignment = .center
headerRowStack.addArrangedSubview(v)
}
NSLayoutConstraint.activate([
// constrain c1 width to 1/3 of c2 width
headerC1.widthAnchor.constraint(equalTo: headerC2.widthAnchor, multiplier: 1.0 / 3.0),
// constrain c3 width equal to c1 width
headerC3.widthAnchor.constraint(equalTo: headerC1.widthAnchor),
// constrain c4 width equal to c2 width
headerC4.widthAnchor.constraint(equalTo: headerC2.widthAnchor),
])
outerStack.addArrangedSubview(headerRowStack)
let c1Vals = ["HOLDING", "BOOK", "P/L"]
let c3Vals = ["PAID", "MARKET", "WEIGHT"]
for i in 0..<c1Vals.count {
// create a "row" stack view
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.spacing = headerRowStack.spacing
// add it to outer stack view
outerStack.addArrangedSubview(rowStack)
let rowC1 = UILabel()
let rowC2 = UILabel()
let rowC3 = UILabel()
let rowC4 = UILabel()
rowC1.text = c1Vals[i]
rowC3.text = c3Vals[i]
rowC2.text = "row \(i+1) c2 data"
rowC4.text = "row \(i+1) c4 data"
[rowC1, rowC2, rowC3, rowC4].forEach { v in
// give each row-label a border
v.layer.borderColor = UIColor.red.cgColor
v.layer.borderWidth = 0.5
// set the font
v.font = UIFont.systemFont(ofSize: 14.0, weight: .light)
// add it to the row stack view
rowStack.addArrangedSubview(v)
}
// constrain each row-label width to the header-row label width
for (hLabel, rowLabel) in zip([headerC1, headerC2, headerC3, headerC4], [rowC1, rowC2, rowC3, rowC4]) {
rowLabel.widthAnchor.constraint(equalTo: hLabel.widthAnchor).isActive = true
}
}
view.addSubview(outerStack)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain outer stack view to Top / Leading / Trailing with 20-pt "padding"
outerStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
outerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
outerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
}
which produces this result:
Note: you'll have to use a pretty small font to fit that layout on a small device:
This is all i ended needing v1.widthAnchor.constraint(equalTo: v2.widthAnchor, multiplier: 0.5, constant: 0).isActive = true v3.widthAnchor.constraint(equalTo: v1.widthAnchor ).isActive = true v4.widthAnchor.constraint(equalTo: v2.widthAnchor ).isActive = true
Related
I would like to make such layout via swift:
I tried to make it in such way:
var buttonArray = [UILabel]()
for (myKey,myValue) in colorDictionary{
buttonArray += [colorButton(withColor: myValue, title: myKey)]
}
let horizontalStack = UIStackView(arrangedSubviews: buttonArray)
horizontalStack.axis = .horizontal
horizontalStack.distribution = .fillEqually
horizontalStack.alignment = .fill
horizontalStack.translatesAutoresizingMaskIntoConstraints = false
horizontalStack.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
let label2 = UILabel()
label2.text = "Label"
label2.backgroundColor = .red
label2.textColor = .white
label2.textAlignment = .center
label2.lineBreakMode = .byCharWrapping
label2.numberOfLines = 0
label2.translatesAutoresizingMaskIntoConstraints = false
label2.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
let mainStackView = UIStackView()
mainStackView.axis = .horizontal
mainStackView.translatesAutoresizingMaskIntoConstraints = false
mainStackView.addArrangedSubview(label2)
mainStackView.addArrangedSubview(horizontalStack)
mainContainer.addSubview(mainStackView)
NSLayoutConstraint.activate([
mainStackView.topAnchor.constraint(equalTo: mainContainer.topAnchor, constant: 5),
mainStackView.leftAnchor.constraint(equalTo: mainContainer.leftAnchor,
constant: 20),
mainStackView.rightAnchor.constraint(equalTo: mainContainer.rightAnchor,
constant: -20),
mainStackView.heightAnchor.constraint(equalToConstant: 270),
])
where colorButton is:
func colorButton(withColor color:UIColor, title:String) -> UILabel{
let newButton = UILabel()
newButton.backgroundColor = color
newButton.text = title
newButton.textAlignment = .center
newButton.textColor = UIColor.white
return newButton
}
and here is the result which I got:
How I can make all these labels look like the desired image? And also I'm not sure whether label rotation can be done by my method.
What I would do is first get it working without any transformations so it appears as a normal, not rotated setup. Once that is working, you only need to apply a single transformation to the main stack view to get the whole thing rotated. Then finally you can tweak the constraints to get it positioned correctly.
Here is code that works:
let horizontalStack = UIStackView(arrangedSubviews: buttonArray)
horizontalStack.axis = .horizontal
horizontalStack.distribution = .fillEqually
horizontalStack.alignment = .fill
horizontalStack.translatesAutoresizingMaskIntoConstraints = false
let label2 = UILabel()
label2.text = "Label"
label2.backgroundColor = .red
label2.textColor = .white
label2.textAlignment = .center
label2.lineBreakMode = .byCharWrapping
label2.numberOfLines = 0
label2.translatesAutoresizingMaskIntoConstraints = false
let mainStackView = UIStackView()
mainStackView.axis = .vertical
mainStackView.distribution = .equalSpacing
mainStackView.translatesAutoresizingMaskIntoConstraints = false
mainStackView.addArrangedSubview(label2)
mainStackView.addArrangedSubview(horizontalStack)
mainStackView.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
mainContainer.addSubview(mainStackView)
NSLayoutConstraint.activate([
mainStackView.centerYAnchor.constraint(equalTo: mainContainer.centerYAnchor),
mainStackView.centerXAnchor.constraint(equalTo: mainContainer.leftAnchor, constant: 40),
// Set the "width", not the "height"
mainStackView.widthAnchor.constraint(equalToConstant: 270),
])
Changes:
I removed the transformation you had for the horizontal stack view and the big label.
I added a single transformation to the main stack view.
I used a vertical, not horizontal, stack view for the main stack.
Updated the properties of the main stack view so the main label fills the area above the other labels.
Updated the constraints. Adjust those to suit your needs.
Note that you need to set the width of the main stack view since the constraint is relative to the untransformed main stack view.
Hi Please take a look at the following mockup:
I wanted to know how I can create the constraint from above:
V2.top = C1.top + n * V1.height
Because this is not something like the default equation for constraints:
item1.attribute1 = multiplier × item2.attribute2 + constant
I know I can just use AutoResizingMask but it will create a real mess in my code because my code is very complicated, and I also don't like AutoResizingMask that much.
(by the way, please answer in Swift only!)
Thank you
You can do this with a UILayoutGuide -- from Apple's docs:
The UILayoutGuide class is designed to perform all the tasks previously performed by dummy views, but to do it in a safer, more efficient manner.
To get your desired layout, we can:
add a layout guide to C1
constrain its Top to C1 Top
constrain its Height to V1 Height with a "n" multiplier
constrain V2 Top to the guide's Bottom
Here is a complete example to demonstrate:
class GuideViewController: UIViewController {
// a label on each side so we can
// "tap to change" v1 Height and "n" multiplier
let labelN = UILabel()
let labelH = UILabel()
let containerView = UIView()
let v1 = UILabel()
let v2 = UILabel()
// a layout guide for v2's Top spacing
let layG = UILayoutGuide()
// we'll change these on taps
var n:CGFloat = 0
var v1H: CGFloat = 30
// constraints we'll want to modify when "n" or "v1H" change
var v1HeightConstraint: NSLayoutConstraint!
var layGHeightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
v1.text = "V1"
v2.text = "V2"
v1.textAlignment = .center
v2.textAlignment = .center
containerView.backgroundColor = .systemTeal
v1.backgroundColor = .green
v2.backgroundColor = .yellow
[containerView, v1, v2].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
containerView.addSubview(v1)
containerView.addSubview(v2)
view.addSubview(containerView)
// add the layout guide to containerView
containerView.addLayoutGuide(layG)
// respect safe area
let safeG = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// let's give the container 80-pts Top/Bottom and 120-pts on each side
containerView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 80.0),
containerView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 120.0),
containerView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -120.0),
containerView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -80.0),
// v1 Leading / Trailing / Bottom 20-pts
v1.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
v1.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
v1.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -20.0),
// just use v2's intrinisic height
// v2 Leading / Trailing 20-pts
v2.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
v2.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
// layout Guide Top / Leading / Trailing
layG.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0),
layG.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0),
layG.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0.0),
// and constrain v2 Top to layout Guide Bottom
v2.topAnchor.constraint(equalTo: layG.bottomAnchor, constant: 0.0),
])
// layout Guide Height equals v1 Height x n
layGHeightConstraint = layG.heightAnchor.constraint(equalTo: v1.heightAnchor, multiplier: n)
layGHeightConstraint.isActive = true
// v1 Height
v1HeightConstraint = v1.heightAnchor.constraint(equalToConstant: v1H)
v1HeightConstraint.isActive = true
// "tap to change" labels
[labelN, labelH].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
$0.textAlignment = .center
$0.numberOfLines = 0
view.addSubview($0)
let t = UITapGestureRecognizer(target: self, action: #selector(tapHandler(_:)))
$0.addGestureRecognizer(t)
$0.isUserInteractionEnabled = true
}
NSLayoutConstraint.activate([
labelN.topAnchor.constraint(equalTo: containerView.topAnchor),
labelN.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 8.0),
labelN.trailingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: -8.0),
labelN.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
labelH.topAnchor.constraint(equalTo: containerView.topAnchor),
labelH.leadingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 8.0),
labelH.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -8.0),
labelH.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
])
updateInfo()
}
#objc func tapHandler(_ gr: UITapGestureRecognizer) -> Void {
guard let v = gr.view else {
return
}
// if we tapped on the "cylcle N" label
if v == labelN {
n += 1
if n == 6 {
n = 0
}
// can't change multiplier directly, so
// de-Activate / set it / Activate
layGHeightConstraint.isActive = false
layGHeightConstraint = layG.heightAnchor.constraint(equalTo: v1.heightAnchor, multiplier: n)
layGHeightConstraint.isActive = true
}
// if we tapped on the "cylcle v1H" label
if v == labelH {
v1H += 5
if v1H > 50 {
v1H = 30
}
v1HeightConstraint.constant = v1H
}
updateInfo()
}
func updateInfo() -> Void {
var s: String = ""
s = "Tap to cycle \"n\" from Zero to 5\n\nn = \(n)"
labelN.text = s
s = "Tap to cycle \"v1H\" from 30 to 50\n\nv1H = \(v1H)"
labelH.text = s
}
}
When you run it, it will look like this:
Each time you tap the left side, it will cycle the n multiplier variable from Zero to 5, and update the constraints.
Each time you tap the right side, it will cycle the v1H height variable from 30 to 50, and update the constraints.
It can be solved by using a helper view.
A helper view is in this case just a UIView used for sizing purpose, without visible content of its own. Either set its alpha = 0 or hidden = true.
Set helperView.top = c1.top
Set helperView.height = v1.height
Set v2.top = helperView.bottom + 5
You also need to set the width and leading for the helper view, but their values are not important.
I have 2 UILabels as follows:
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.textColor = Constants.mainFontColor.withAlphaComponent(0.7)
label.translatesAutoresizingMaskIntoConstraints = false
label.isUserInteractionEnabled = false
label.sizeToFit()
return label
}()
private lazy var valueLabel: UILabel = {
let label = UILabel()
label.textColor = Constants.mainFontColor
label.translatesAutoresizingMaskIntoConstraints = false
label.isUserInteractionEnabled = false
label.sizeToFit()
return label
}()
These labels are then placed into a UIStackView:
private lazy var stackView: UIStackView = {
let stack = UIStackView(arrangedSubviews: [titleLabel, valueLabel])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .horizontal
stack.spacing = 5
stack.alignment = .firstBaseline
addSubview(stack)
return stack
}()
I then create several instances of these e.g.:
private lazy var flightNumber: FuelSheetHeaderField = {
return FuelSheetHeaderField(title: FuelSheetStringsField.flightNumber.title.localized,
value: viewModel.flightNumber, titleFontSize: 17, valueFontSize: 24)
}()
These are then placed into a separate stack view:
private lazy var flightData: UIStackView = {
let stack = UIStackView(arrangedSubviews: [flightNumber, aircraftReg, dateTime, origin])
stack.axis = .horizontal
stack.distribution = .equalSpacing
stack.spacing = Constants.standardHorizontalStackSpacing
return stack
}()
The title labels have font size of 12 and the values have font size 17. Except for one which is customised to have font size 17 and 2 accordingly. I am trying to get all the elements to align along the same base line. I added the following constraint:
titleLabel.firstBaselineAnchor.constraint(equalTo: valueLabel.firstBaselineAnchor),
However the effect is as follows:
Most of the stack elements are aligning but as you can see the first element with the larger font is sitting higher than the second. This is driving me a little crazy. How can I fix it so that all elements in the stack align along the same baseline.
I think you're going to have trouble trying to use baselines across stack views.
One option - not sure if it will work for you - is to put all the labels in a single horizontal stack view, and use .setCustomSpacing() to adjust the spaces between labels.
As an example:
and with Debug -> View Debugging -> Show View Frames:
Here's the code I used to generate that:
class AlignedStackViewController: UIViewController {
let detailsStack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.alignment = .leading
v.distribution = .fill
v.spacing = 12
return v
}()
let dataStack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .firstBaseline
v.distribution = .fill
v.spacing = 5
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0.928, green: 0.973, blue: 1.0, alpha: 1.0)
let fdLabel = UILabel()
fdLabel.text = "FLIGHT DETAILS"
detailsStack.addArrangedSubview(fdLabel)
detailsStack.addArrangedSubview(dataStack)
detailsStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(detailsStack)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// position stack view at 40, 100
detailsStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
detailsStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
])
addLabelPair(with: "#", value: "VS0105", titleFontSize: 17, valueFontSize: 24)
addLabelPair(with: "Reg:", value: "GAAAA", titleFontSize: 12, valueFontSize: 17)
addLabelPair(with: "Departure:", value: "09 Sep 2020", titleFontSize: 12, valueFontSize: 17)
addLabelPair(with: "at", value: "08:34", titleFontSize: 12, valueFontSize: 17)
updateStackSpacing()
}
func updateStackSpacing() -> Void {
// make sure stack has been filled
guard dataStack.arrangedSubviews.count == 8 else {
return
}
// verbose for clarity
let flightNumberLabel = dataStack.arrangedSubviews[1]
let aircraftRegLabel = dataStack.arrangedSubviews[3]
dataStack.setCustomSpacing(12, after: flightNumberLabel)
dataStack.setCustomSpacing(12, after: aircraftRegLabel)
}
func addLabelPair(with title: String, value: String, titleFontSize: CGFloat, valueFontSize: CGFloat) -> Void {
let tLabel = UILabel()
let vLabel = UILabel()
tLabel.font = UIFont.systemFont(ofSize: titleFontSize)
vLabel.font = UIFont.systemFont(ofSize: valueFontSize)
tLabel.textColor = UIColor.black.withAlphaComponent(0.7)
vLabel.textColor = UIColor.black
tLabel.text = title
vLabel.text = value
dataStack.addArrangedSubview(tLabel)
dataStack.addArrangedSubview(vLabel)
}
}
I think, It seems that uiLabel has default padding
And the default padding value depends on the font size.
In spite of sizeToFit, the result is the same
I want to create a constraint that is 80 percent of the width of the uiview controller not a 100 percent which is what it is now. I tried using the var percent but its not working. The code should be centered on the y axis. So 10 percent on the left side and ride side are not covered by the constraint. I am trying to do this in a stack view.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//Image View
let percent = ((UIScreen.main.bounds.midY) + (UIScreen.main.bounds.maxX * 0.8))
//Text Label
let textLabel = UILabel()
textLabel.backgroundColor = UIColor.yellow
textLabel.widthAnchor.constraint(equalToConstant: self.view.frame.width * percent).isActive = true
textLabel.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
textLabel.text = "Hi World"
textLabel.textAlignment = .center
//Stack View
let stackView = UIStackView()
stackView.axis = NSLayoutConstraint.Axis.vertical
stackView.distribution = UIStackView.Distribution.equalSpacing
stackView.alignment = UIStackView.Alignment.center
stackView.spacing = 16.0
stackView.addArrangedSubview(textLabel)
stackView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(stackView)
//Constraints
stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
}
}
Set the widthAnchor of stackView with that of superView's width (in this case view controller's view) and set multiplier value as 0.80
stackView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.80).isActive = true
You can achieve this by setting 'widthAnchor' and 'centerXAnchor' like below. We need to always keep horizontal center of child view and super view same. Whereas width of child should be 80 percentage(here 0.8 indicates 80%) of super view.
stackView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.8, constant: 0).isActive = true
stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
I have two labels of dynamic height that i want centered in a UICollectionViewCell. I added a detailContainerView centered on the cell and added the two label using auto layout as shown below.
NSLayoutConstraint.activate([
detailContainerView.centerYAnchor.constraint(equalTo: centerYAnchor),
detailContainerView.leftAnchor.constraint(equalTo: centerXAnchor), // these labels are supposed to occupy the right half pf the cell
detailContainerView.rightAnchor.constraint(equalTo: rightAnchor),
detailContainerView.heightAnchor.constraint(lessThanOrEqualToConstant: 60)
])
Here are the labels:
let nameLabel: UILabel = {
let nl = UILabel()
nl.translatesAutoresizingMaskIntoConstraints = false
nl.textAlignment = .left
nl.numberOfLines = 0
nl.font = UIFont.actigage_systemFontOfSize(14)
nl.isUserInteractionEnabled = true
nl.textColor = .darkGray
nl.adjustsFontSizeToFitWidth = true
nl.minimumScaleFactor = 0.8
return nl
}()
let categoryLabel: UILabel = {
let nl = UILabel()
nl.translatesAutoresizingMaskIntoConstraints = false
nl.textAlignment = .left
nl.numberOfLines = 0
nl.adjustsFontSizeToFitWidth = true
nl.minimumScaleFactor = 0.8
nl.isUserInteractionEnabled = true
nl.font = UIFont.actigage_systemFontOfSize(12)
return nl
}()
nameLabel.setContentCompressionResistancePriority(751, for: .vertical)
nameLabel.setContentHuggingPriority(751, for: .vertical)
And here are their constraints:
NSLayoutConstraint.activate([
categoryLabel.topAnchor.constraint(equalTo: detailContainerView.topAnchor),
categoryLabel.leftAnchor.constraint(equalTo: logoImageView.rightAnchor, constant: 8),
categoryLabel.rightAnchor.constraint(equalTo: rightAnchor),
categoryLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 15)
])
NSLayoutConstraint.activate([
nameLabel.topAnchor.constraint(equalTo: categoryLabel.bottomAnchor),
nameLabel.leftAnchor.constraint(equalTo: logoImageView.rightAnchor, constant: 8),
nameLabel.rightAnchor.constraint(equalTo: rightAnchor),
nameLabel.bottomAnchor.constraint(equalTo: detailContainerView.bottomAnchor),
])
Side Note: we don't want the labels to be larger than the cell size.
I can't figure out how to remove the extra space from the labels when the text auto shrinks. TIA :)