swift how to set autolayout programmatically - swift

I have buttons inside a view which in potrait mode I want like this -
which is achieved by the following code -
//original potrait mode/////
import UIKit
class PotraitViewController: UIViewController {
override func viewDidLoad() {
let buttonred = UIButton()
buttonred.backgroundColor = UIColor.red
let buttonblue = UIButton()
buttonblue.backgroundColor = UIColor.blue
let landscapesmallview = UIView()
view.addSubview(landscapesmallview)
landscapesmallview.addSubview(buttonred)
landscapesmallview.addSubview(buttonblue)
buttonred.translatesAutoresizingMaskIntoConstraints = false
buttonblue.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buttonred.topAnchor.constraint(equalTo: view.topAnchor,constant: 200),
buttonred.centerXAnchor.constraint(equalTo: view.centerXAnchor),
buttonred.trailingAnchor.constraint(equalTo: view.trailingAnchor,constant:-20),
buttonred.widthAnchor.constraint(equalToConstant: 50),
//-------
buttonblue.topAnchor.constraint(equalTo: buttonred.bottomAnchor,constant: 40),
buttonblue.leadingAnchor.constraint(equalTo: buttonred.leadingAnchor),
buttonblue.trailingAnchor.constraint(equalTo:buttonred.trailingAnchor),
buttonblue.widthAnchor.constraint(equalTo: buttonred.widthAnchor)
])
}
}
and in landscape mode I want like this -
which is achieved by the following code -
// original lanscape mode/////
import UIKit
class LandscapeViewController: UIViewController {
override func viewDidLoad() {
let buttonred = UIButton()
buttonred.backgroundColor = UIColor.red
let buttonblue = UIButton()
buttonblue.backgroundColor = UIColor.blue
view.addSubview(buttonred)
view.addSubview(buttonblue)
buttonred.translatesAutoresizingMaskIntoConstraints = false
buttonblue.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buttonred.centerYAnchor.constraint(equalTo: view.centerYAnchor),
buttonred.leadingAnchor.constraint(equalTo: view.leadingAnchor,constant:40),
buttonred.trailingAnchor.constraint(equalTo: view.centerXAnchor,constant:-20),
buttonred.widthAnchor.constraint(equalToConstant: 50),
//-------
buttonblue.centerYAnchor.constraint(equalTo: buttonred.centerYAnchor),
buttonblue.leadingAnchor.constraint(equalTo: view.centerXAnchor,constant:40),
buttonblue.trailingAnchor.constraint(equalTo: view.trailingAnchor,constant:-20),
buttonblue.widthAnchor.constraint(equalTo:buttonred.widthAnchor)
])
}
}
So, I tried the following code to achieve by screen rotation i.e. two different layouts in potrait and landscape views programmatically with the help of the following code:-
import UIKit
class NewViewController: UIViewController {
override func viewDidLoad() {
let buttonredlandscape = UIButton()
buttonredlandscape.backgroundColor = UIColor.red
let buttonbluelandscape = UIButton()
buttonbluelandscape.backgroundColor = UIColor.blue
let buttonredportrait = UIButton()
buttonredportrait.backgroundColor = UIColor.red
let buttonblueportrait = UIButton()
buttonblueportrait.backgroundColor = UIColor.blue
let landscapesmallview = UIView()
let portraitsmallview = UIView()
landscapesmallview.backgroundColor = UIColor.gray
portraitsmallview.backgroundColor = UIColor.purple
landscapesmallview.frame = view.frame
portraitsmallview.frame = view.frame
view.addSubview(landscapesmallview)
view.addSubview(portraitsmallview)
landscapesmallview.addSubview(buttonredlandscape)
landscapesmallview.addSubview(buttonbluelandscape)
portraitsmallview.addSubview(buttonredportrait)
portraitsmallview.addSubview(buttonblueportrait)
buttonredlandscape.translatesAutoresizingMaskIntoConstraints = false
buttonbluelandscape.translatesAutoresizingMaskIntoConstraints = false
buttonredportrait.translatesAutoresizingMaskIntoConstraints = false
buttonblueportrait.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buttonredlandscape.centerYAnchor.constraint(equalTo:landscapesmallview.centerYAnchor),
buttonredlandscape.topAnchor.constraint(equalTo:landscapesmallview.topAnchor,constant:40),
buttonredlandscape.trailingAnchor.constraint(equalTo: landscapesmallview.centerXAnchor,constant:-20),
buttonredlandscape.heightAnchor.constraint(equalTo: landscapesmallview.heightAnchor,constant:50),
buttonbluelandscape.centerYAnchor.constraint(equalTo:buttonredlandscape.centerYAnchor),
buttonbluelandscape.leadingAnchor.constraint(equalTo: landscapesmallview.centerXAnchor,constant:40),
buttonbluelandscape.trailingAnchor.constraint(equalTo: landscapesmallview.trailingAnchor,constant:-20),
buttonbluelandscape.heightAnchor.constraint(equalTo: buttonredlandscape.heightAnchor),
buttonredportrait.topAnchor.constraint(equalTo: portraitsmallview.topAnchor,constant: 200),
buttonredportrait.centerXAnchor.constraint(equalTo: portraitsmallview.centerXAnchor),
buttonredportrait.trailingAnchor.constraint(equalTo: portraitsmallview.trailingAnchor,constant:-20),
buttonredportrait.widthAnchor.constraint(equalTo: buttonredportrait.widthAnchor),
buttonblueportrait.topAnchor.constraint(equalTo: buttonredportrait.bottomAnchor,constant: 40),
buttonblueportrait.leadingAnchor.constraint(equalTo: buttonredportrait.leadingAnchor),
buttonblueportrait.trailingAnchor.constraint(equalTo:buttonredportrait.trailingAnchor),
buttonblueportrait.widthAnchor.constraint(equalTo: buttonredportrait.widthAnchor)
])
//-------
func viewWillTransition(to size: CGSize, with coordinator: ) {
if UIDevice.current.orientation.isLandscape {
landscapesmallview.translatesAutoresizingMaskIntoConstraints = false
portraitsmallview.translatesAutoresizingMaskIntoConstraints = true
} else if UIDevice.current.orientation.isPortrait {
portraitsmallview.translatesAutoresizingMaskIntoConstraints = false
landscapesmallview.translatesAutoresizingMaskIntoConstraints = true
}
}
}
}
which in potrait mode shows -
and which in landscape mode shows -
How to achieve what I want programmatically i.e. topmost 2 buttons to rearrange themselves programmatically every-time the user rotates the device. Its not just the buttons. It can be labels, images, collectionview etc. or just anything. The upshot is that I want to achieve two different layouts in landscape and portrait modes programmatically irrespective of the device.
Points to be noted :-
i) I have tried used NSLayoutAnchor with "NSLayoutConstraint.activate" because apple recommends it, but if the code can be made shorter(and faster) with some other method like visual format etc. I'm okay with that as well/
ii) If possible, I do not want to use stackview or containerview, because there can be many more types of labels, buttons etc, but if there is no other way, then I will use it.
iii) Is my code DRY principle compliant ?
Also, guys, please, I do not deserve negative marks because, as far as I know, this has not been asked before. I request you not to give negative marks and encourage me.

There are various ways to do this. One approach:
declare two "constraint" arrays
one to hold the "narrow view" constraints
one to hold the "wide view" constraints
activate / deactivate the constraints as needed
Here is a complete example:
class ChangeLayoutViewController: UIViewController {
let redButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
v.setTitle("Red Button", for: [])
return v
}()
let blueButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
v.setTitle("Blue Button", for: [])
return v
}()
var narrowConstraints: [NSLayoutConstraint] = [NSLayoutConstraint]()
var wideConstraints: [NSLayoutConstraint] = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(redButton)
view.addSubview(blueButton)
let g = view.safeAreaLayoutGuide
var c: NSLayoutConstraint
// MARK: - narrow orientation
// constrain redButton above blueButton
// constrain redButton leading and trailing to safe-area (with 8-pts on each side)
c = redButton.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0)
narrowConstraints.append(c)
c = redButton.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0)
narrowConstraints.append(c)
// constrain blueButton leading and trailing to safe-area (with 8-pts on each side)
c = blueButton.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0)
narrowConstraints.append(c)
c = blueButton.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0)
narrowConstraints.append(c)
// constrain redButton top 40-pts from safe-area top
c = redButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0)
narrowConstraints.append(c)
// constrain blueButton top 20-pts from redButton bottom
c = blueButton.topAnchor.constraint(equalTo: redButton.bottomAnchor, constant: 20.0)
narrowConstraints.append(c)
// MARK: - wide orientation
// constrain redButton & blueButton side-by-side
// with equal widths and 8-pts between them
// constrain redButton leading 8-pts from safe-area leading
c = redButton.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0)
wideConstraints.append(c)
// constrain blueButton trailing 8-pts from safe-area trailing
c = blueButton.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0)
wideConstraints.append(c)
// constrain blueButton leading 8-pts from redButton trailing
c = blueButton.leadingAnchor.constraint(equalTo: redButton.trailingAnchor, constant: 8.0)
wideConstraints.append(c)
// constrain buttons to equal widths
c = blueButton.widthAnchor.constraint(equalTo: redButton.widthAnchor)
wideConstraints.append(c)
// constrain both buttons centerY to safe-area centerY
c = redButton.centerYAnchor.constraint(equalTo: g.centerYAnchor)
wideConstraints.append(c)
c = blueButton.centerYAnchor.constraint(equalTo: g.centerYAnchor)
wideConstraints.append(c)
// activate initial constraints based on view width:height ratio
changeConstraints(view.frame.width > view.frame.height)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
// change active set of constraints based on view width:height ratio
self.changeConstraints(size.width > size.height)
}
func changeConstraints(_ useWide: Bool) -> Void {
if useWide {
NSLayoutConstraint.deactivate(narrowConstraints)
NSLayoutConstraint.activate(wideConstraints)
} else {
NSLayoutConstraint.deactivate(wideConstraints)
NSLayoutConstraint.activate(narrowConstraints)
}
}
}
Results:

refer this this image for handling in device orientation
take both buttons in stack view and make the stack view in center vertically and center horizontally

Related

Swift, Scroll View, Stack View, programatical Button Creation and Auto Layout causes weird Behaviors / Errors

I have a simple iOS App, containing a Scroll View (scroll horizontally, but mainly as described here: Is it possible for UIStackView to scroll?). By default the Horizontal Stack is empty (spacing = Standard, Distribution is Fill, but also Equal Spacing does not improve this), and I add buttons programatically to it (for example purpose I add a random length string, to have various sizes), using:
filterStackView.translatesAutoresizingMaskIntoConstraints = false
let newButtonConfiguration = UIButton.Configuration.filled()
let newButton = UIButton(configuration: newButtonConfiguration)
newButton.translatesAutoresizingMaskIntoConstraints = false
newButton.setTitle("\(randomString(length: Int.random(in: 5..<25)))", for: .normal)
newButton.setImage(UIImage(systemName: "xmark.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .unspecified)), for: .normal)
newButton.addTarget(self, action: #selector(newButtonPressedAction), for: .touchUpInside)
filterStackView.addArrangedSubview(newButton)
I already tried adding to the button:
newButton.sizeToFit()
newButton.layoutIfNeeded()
or to the View Stack:
filterStackView.sizeToFit()
filterStackView.layoutIfNeeded()
Without any visible change. The weird things happening are:
After adding another one, sizes start changing in a funny way (if buttons are added from Story Board they really respect the width of the text - and Autolayout works fine):
And after playing with adding and removing them for a while:
And in the debug console I see constraints failing badly:
2022-11-01 14:05:48.450435+0100 TestAppSideScroll[20626:640959] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x600001b58c80 'UISV-canvas-connection' H:[UIButton:0x122408660'N6xoAsl']-(0)-| (active, names: '|':UIStackView:0x1505080c0 )>",
"<_UISystemBaselineConstraint:0x600001b586e0 'UISV-spacing' H:[UIButton:0x1223095d0'oHSryjPsjrZA']-(NSLayoutAnchorConstraintSpace(8))-[UIButton:0x122408660'N6xoAsl'] (active)>"
)
Will attempt to recover by breaking constraint
<_UISystemBaselineConstraint:0x600001b586e0 'UISV-spacing' H:[UIButton:0x1223095d0'oHSryjPsjrZA']-(NSLayoutAnchorConstraintSpace(8))-[UIButton:0x122408660'N6xoAsl'] (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
Which actually makes no sense, since the conflict seems to be the spacing constraint with the connection to the canvas constraint? But that I would expect to happen for every single button I add, if that would be the general problem - however there are no real constraints, except the spacing and the snapping of the scroll view.
Any help would be appreciated.
Yeah... something really weird going on there.
My guess would be that UIKit has some sort of algorithm that is trying to apply "readability" to the buttons. Clearly, though, that's not the goal here.
After some quick testing, one way to get around this is to add the button as a subview to a clear "container" view, and then add that container view to the stack view.
Some sample code:
class SampleViewController: UIViewController {
let filterStackViewA: UIStackView = {
let v = UIStackView()
v.spacing = 8
v.distribution = .fill
v.alignment = .center
return v
}()
let filterStackViewB: UIStackView = {
let v = UIStackView()
v.spacing = 8
v.distribution = .fill
v.alignment = .center
return v
}()
let scrollViewA: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemYellow
return v
}()
let scrollViewB: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemOrange
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
let addBtn = UIButton()
addBtn.setTitle("Add Button", for: [])
addBtn.setTitleColor(.white, for: .normal)
addBtn.setTitleColor(.lightGray, for: .highlighted)
addBtn.backgroundColor = .systemGreen
addBtn.layer.cornerRadius = 8
let labelA: UILabel = {
let v = UILabel()
v.textColor = .white
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.text = "Buttons added to \"Container\" views..."
return v
}()
let labelB: UILabel = {
let v = UILabel()
v.textColor = .white
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.text = "Buttons added directly to stack view..."
return v
}()
[addBtn, scrollViewA, scrollViewB, filterStackViewA, filterStackViewB, labelA, labelB].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(addBtn)
view.addSubview(labelA)
view.addSubview(scrollViewA)
scrollViewA.addSubview(filterStackViewA)
view.addSubview(labelB)
view.addSubview(scrollViewB)
scrollViewB.addSubview(filterStackViewB)
let g = view.safeAreaLayoutGuide
let cgA = scrollViewA.contentLayoutGuide
let fgA = scrollViewA.frameLayoutGuide
let cgB = scrollViewB.contentLayoutGuide
let fgB = scrollViewB.frameLayoutGuide
NSLayoutConstraint.activate([
addBtn.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
addBtn.widthAnchor.constraint(equalToConstant: 200.0),
addBtn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
labelA.topAnchor.constraint(equalTo: addBtn.bottomAnchor, constant: 40.0),
labelA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
labelA.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewA.topAnchor.constraint(equalTo: labelA.bottomAnchor, constant: 8.0),
scrollViewA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollViewA.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewA.heightAnchor.constraint(equalToConstant: 60.0),
filterStackViewA.topAnchor.constraint(equalTo: cgA.topAnchor, constant: 4.0),
filterStackViewA.leadingAnchor.constraint(equalTo: cgA.leadingAnchor, constant: 8.0),
filterStackViewA.trailingAnchor.constraint(equalTo: cgA.trailingAnchor, constant: -8.0),
filterStackViewA.bottomAnchor.constraint(equalTo: cgA.bottomAnchor, constant: -4.0),
filterStackViewA.heightAnchor.constraint(equalTo: fgA.heightAnchor, constant: -8.0),
labelB.topAnchor.constraint(equalTo: filterStackViewA.bottomAnchor, constant: 20.0),
labelB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
labelB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewB.topAnchor.constraint(equalTo: labelB.bottomAnchor, constant: 8.0),
scrollViewB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollViewB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewB.heightAnchor.constraint(equalToConstant: 60.0),
filterStackViewB.topAnchor.constraint(equalTo: cgB.topAnchor, constant: 4.0),
filterStackViewB.leadingAnchor.constraint(equalTo: cgB.leadingAnchor, constant: 8.0),
filterStackViewB.trailingAnchor.constraint(equalTo: cgB.trailingAnchor, constant: -8.0),
filterStackViewB.bottomAnchor.constraint(equalTo: cgB.bottomAnchor, constant: -4.0),
filterStackViewB.heightAnchor.constraint(equalTo: fgB.heightAnchor, constant: -8.0),
])
addBtn.addTarget(self, action: #selector(addButton), for: .touchUpInside)
}
#objc func addButton() {
// unwrap optional system image
guard let img = UIImage(systemName: "xmark.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .unspecified))
else { return }
var newButtonConfiguration = UIButton.Configuration.filled()
newButtonConfiguration.title = randomString(length: Int.random(in: 5..<15))
newButtonConfiguration.image = img
// let's add a new button to a "container" view
let newButtonA = UIButton(configuration: newButtonConfiguration)
newButtonA.translatesAutoresizingMaskIntoConstraints = false
let v = UIView()
v.addSubview(newButtonA)
NSLayoutConstraint.activate([
newButtonA.topAnchor.constraint(equalTo: v.topAnchor),
newButtonA.leadingAnchor.constraint(equalTo: v.leadingAnchor),
newButtonA.trailingAnchor.constraint(equalTo: v.trailingAnchor),
newButtonA.bottomAnchor.constraint(equalTo: v.bottomAnchor),
])
// add the container view to the stack view
filterStackViewA.addArrangedSubview(v)
// let's add a new button directly to the stack view
let newButtonB = UIButton(configuration: newButtonConfiguration)
filterStackViewB.addArrangedSubview(newButtonB)
newButtonA.addTarget(self, action: #selector(removeMe(_:)), for: .touchUpInside)
newButtonB.addTarget(self, action: #selector(removeMe(_:)), for: .touchUpInside)
// let's make sure the new button is visible
// note: this is just for example...
// if we rapidly tap and add buttons, this can easily "miss" the last button
// so don't expect this to be "production" code
DispatchQueue.main.async {
let sz = self.scrollViewA.contentSize
let rA = CGRect(x: sz.width - 1.0, y: 0.0, width: 1.0, height: 1.0)
self.scrollViewA.scrollRectToVisible(rA, animated: true)
let szB = self.scrollViewB.contentSize
let rB = CGRect(x: szB.width - 1.0, y: 0.0, width: 1.0, height: 1.0)
self.scrollViewB.scrollRectToVisible(rB, animated: true)
}
}
#objc func removeMe(_ sender: UIButton) {
// get a reference to the tapped button's superview
guard let sv = sender.superview else { return }
var v: UIView!
if sv is UIStackView {
// we tapped a button added directly to the stack view
v = sender
} else {
// we tapped a button that's in a "container" view
v = sv
}
guard let st = v.superview as? UIStackView,
let idx = st.arrangedSubviews.firstIndex(of: v),
filterStackViewA.arrangedSubviews.count > idx,
filterStackViewB.arrangedSubviews.count > idx
else {
print("something's not setup right, so return")
return
}
let bA = filterStackViewA.arrangedSubviews[idx]
let bB = filterStackViewB.arrangedSubviews[idx]
// let's animate the buttons away
bA.isHidden = true
bB.isHidden = true
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
}, completion: { _ in
bA.removeFromSuperview()
bB.removeFromSuperview()
})
}
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
It will look like this when running:
Each tap of "Add Button" will add a new button to each scrolling-stackView, with the top set using buttons in container views. Tapping any of those buttons will remove it from its stack view, along with its "twin" from the other one.

NSLayoutConstraints not cooperating with UIImageView

In my UIView subclass, I have one image view and three labels:
let imageView = UIImageView()
let firstLabel = UILabel()
let secondLabel = UILabel()
let thirdLabel = UILabel()
The image and texts are set by the view controller that uses the view.
I begin to set them up with:
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
addSubview(imageView)
firstLabel.translatesAutoresizingMaskIntoConstraints = false
firstLabel.textAlignment = .center
addSubview(firstLabel)
secondLabel.translatesAutoresizingMaskIntoConstraints = false
secondLabel.textAlignment = .center
addSubview(secondLabel)
thirdLabel.translatesAutoresizingMaskIntoConstraints = false
thirdLabel.textAlignment = .center
addSubview(thirdLabel)
I am trying to constrain these in such a way such that it looks like the following (rough drawing):
Specifically:
thirdLabel is in the center at the bottom
secondLabel is in the center directly above thirdLabel
firstLabel is in the center directly above secondLabel
The size of imageView will vary depending on the size of the view, however it must meet these criteria:
It is in the center directly above firstLabel
It reaches the top
It is a square
So if the height of the view was larger, only the image view would enlarge, the labels would NOT increase height and evenly space out. They would remain at the bottom. So visually, this would be good:
and this would be bad:
An example of what I've tried (one of MANY):
NSLayoutConstraint.activate([
thirdLabel.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
thirdLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
thirdLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
thirdLabel.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
secondLabel.centerXAnchor.constraint(equalTo: thirdLabel.centerXAnchor),
secondLabel.bottomAnchor.constraint(equalTo: thirdLabel.topAnchor),
secondLabel.leadingAnchor.constraint(equalTo: thirdLabel.leadingAnchor),
secondLabel.trailingAnchor.constraint(equalTo: thirdLabel.trailingAnchor),
firstLabel.centerXAnchor.constraint(equalTo: secondLabel.centerXAnchor),
firstLabel.bottomAnchor.constraint(equalTo: secondLabel.topAnchor),
firstLabel.leadingAnchor.constraint(equalTo: secondLabel.leadingAnchor),
firstLabel.trailingAnchor.constraint(equalTo: secondLabel.trailingAnchor),
imageView.centerXAnchor.constraint(equalTo: firstLabel.centerXAnchor),
imageView.bottomAnchor.constraint(equalTo: firstLabel.topAnchor),
imageView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor),
])
I've mixed and matched so many constraints but I cannot achieve the layout in the first image. Not only can I get it working with various heights, I can't even get it to work with ANY height. Sometimes the image view takes up the whole thing and I can't even see the labels (are they underneath the view? behind the image view?). Sometimes the height of the labels are increased. These things occur even though I have constraints that seemingly don't allow this to happen? No breaking of constraint messages appear in the console either.
I believe it may have something to do with sizing, because if I don't set an image (and set a background color for imageView so I can see where it is), it works perfectly. It's only when I actually assign an image to imageView.image do things act up. I've tried resizing the image beforehand, along with setting many variables and constraints not shown in the particular example given above.
Frustrating!
You need to set both Content Compression Resistance and Content Hugging priorities on your labels.
Here is an example custom view class (using mostly your code):
class AJPView: UIView {
let imageView = UIImageView()
let firstLabel = UILabel()
let secondLabel = UILabel()
let thirdLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
addSubview(imageView)
firstLabel.translatesAutoresizingMaskIntoConstraints = false
firstLabel.textAlignment = .center
addSubview(firstLabel)
secondLabel.translatesAutoresizingMaskIntoConstraints = false
secondLabel.textAlignment = .center
addSubview(secondLabel)
thirdLabel.translatesAutoresizingMaskIntoConstraints = false
thirdLabel.textAlignment = .center
addSubview(thirdLabel)
NSLayoutConstraint.activate([
thirdLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
thirdLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
thirdLabel.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
secondLabel.bottomAnchor.constraint(equalTo: thirdLabel.topAnchor),
secondLabel.leadingAnchor.constraint(equalTo: thirdLabel.leadingAnchor),
secondLabel.trailingAnchor.constraint(equalTo: thirdLabel.trailingAnchor),
firstLabel.bottomAnchor.constraint(equalTo: secondLabel.topAnchor),
firstLabel.leadingAnchor.constraint(equalTo: secondLabel.leadingAnchor),
firstLabel.trailingAnchor.constraint(equalTo: secondLabel.trailingAnchor),
imageView.centerXAnchor.constraint(equalTo: firstLabel.centerXAnchor),
imageView.bottomAnchor.constraint(equalTo: firstLabel.topAnchor),
imageView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor),
// you've given the labels leading and trailing constraints,
// so you don't need these
//thirdLabel.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
//secondLabel.centerXAnchor.constraint(equalTo: thirdLabel.centerXAnchor),
//firstLabel.centerXAnchor.constraint(equalTo: secondLabel.centerXAnchor),
])
// prevent labels from being compressed or stretched vertically
[firstLabel, secondLabel, thirdLabel].forEach {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.setContentHuggingPriority(.required, for: .vertical)
}
// let's give the subviews background colors
// so we can easily see the frames
let clrs: [UIColor] = [
.systemYellow,
.green,
.cyan,
.yellow
]
for (v, c) in zip([imageView, firstLabel, secondLabel, thirdLabel], clrs) {
v.backgroundColor = c
}
}
}
and a demo view controller:
class ViewController: UIViewController {
var heightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
let testView = AJPView()
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
let g = view.safeAreaLayoutGuide
heightConstraint = testView.heightAnchor.constraint(equalToConstant: 120.0)
NSLayoutConstraint.activate([
testView.widthAnchor.constraint(equalToConstant: 300.0),
testView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// activate height anchor
heightConstraint,
])
testView.firstLabel.text = "First"
testView.secondLabel.text = "Second"
testView.thirdLabel.text = "Third"
if let img = UIImage(named: "myImage") {
testView.imageView.image = img
} else {
if let img = UIImage(systemName: "person.circle.fill") {
testView.imageView.image = img
}
}
// so we can see the frame of the view
testView.layer.borderWidth = 1
testView.layer.borderColor = UIColor.red.cgColor
// add grow / shrink buttons
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.spacing = 20
stack.distribution = .fillEqually
["Taller", "Shorter"].forEach {
let b = UIButton(type: .system)
b.backgroundColor = .yellow
b.setTitle($0, for: [])
b.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
stack.addArrangedSubview(b)
}
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
])
}
#objc func btnTapped(_ sender: UIButton) -> Void {
var h = heightConstraint.constant
if sender.currentTitle == "Taller" {
h += 10
} else {
h -= 10
}
heightConstraint.constant = h
}
}
The output looks like this (the custom view is outlined in red):
you can tap the "Taller" / "Shorter" buttons to make the custom view grow or shrink (by 10-pts each tap) to see the changes:
Note that the view will eventually get too tall for the 1:1 ratio image view to fit horizontally:

Resize superview according to label's font size and image's aspect ratio

This is an auto-layout related question. I've containerView which has two subviews: imageView and label. I want to let the fontsize of the label determine the size of the containerView according to the aspect ratio of imageView.
When the font size increases, the containerView and the imageView should get bigger maintaining the aspect ratio and keeping the label centered with some padding as shown in the image below.
And I want to achieve it programmatically.
Any help will be much appreciated
You can accomplish this by:
constrain image view to all 4 sides of container
constrain label centered in container
constrain image view to 16:9 ratio
constrain image view's height to label's height + desired "padding"
Here's an example, including buttons to increase / decrease the font size:
class WalterViewController: UIViewController {
let theContainerView: UIView = {
let v = UIView()
v.backgroundColor = .blue
return v
}()
let theImageView: UIImageView = {
let v = UIImageView()
v.backgroundColor = .red
v.contentMode = .scaleToFill
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .yellow
v.textAlignment = .center
v.text = "TEST"
// content vertical hugging REQUIRED !!!
v.setContentHuggingPriority(.required, for: .vertical)
return v
}()
let btnUp: UIButton = {
let b = UIButton(type: .system)
b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
b.setTitle("Increase", for: .normal)
return b
}()
let btnDn: UIButton = {
let b = UIButton(type: .system)
b.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
b.setTitle("Decrease", for: .normal)
return b
}()
let btnStack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.spacing = 12
v.distribution = .fillEqually
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// we'll be using constraints
[theContainerView, theImageView, theLabel, btnUp, btnDn, btnStack].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
// add buttons to the stack
btnStack.addArrangedSubview(btnUp)
btnStack.addArrangedSubview(btnDn)
// add imageView and label to container
theContainerView.addSubview(theImageView)
theContainerView.addSubview(theLabel)
// add button stack and container view to view
view.addSubview(btnStack)
view.addSubview(theContainerView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// horizontal button stack 20-points from top, 40-points on each side
btnStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
btnStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
btnStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// container view centered in view safeArea
theContainerView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
theContainerView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// constrain image view to its superView (container view)
// 8-pts on all 4 sides
theImageView.topAnchor.constraint(equalTo: theContainerView.topAnchor, constant: 8.0),
theImageView.leadingAnchor.constraint(equalTo: theContainerView.leadingAnchor, constant: 8.0),
theImageView.trailingAnchor.constraint(equalTo: theContainerView.trailingAnchor, constant: -8.0),
theImageView.bottomAnchor.constraint(equalTo: theContainerView.bottomAnchor, constant: -8.0),
// label is centered in its superView (container view)
theLabel.centerXAnchor.constraint(equalTo: theContainerView.centerXAnchor),
theLabel.centerYAnchor.constraint(equalTo: theContainerView.centerYAnchor),
// constrain imageView to 16:9 ratio
theImageView.widthAnchor.constraint(equalTo: theImageView.heightAnchor, multiplier: 16.0 / 9.0),
// constrain imageView's height to label's height +40
// will result in 20-pts on top and bottom
theImageView.heightAnchor.constraint(equalTo: theLabel.heightAnchor, constant: 40.0),
])
// load an image
if let img = UIImage(named: "bkg640x360") {
theImageView.image = img
}
// add targets to buttons to increase / decrease the label's font size
btnUp.addTarget(self, action: #selector(increaseTapped(_:)), for: .touchUpInside)
btnDn.addTarget(self, action: #selector(decreaseTapped(_:)), for: .touchUpInside)
}
#objc func increaseTapped(_ sender: Any?) -> Void {
theLabel.font = theLabel.font.withSize(theLabel.font.pointSize + 1.0)
}
#objc func decreaseTapped(_ sender: Any?) -> Void {
theLabel.font = theLabel.font.withSize(theLabel.font.pointSize - 1.0)
}
}
How it looks on launch (container view is centered in root view):
and, after tapping Increase a bunch of times:

Button Constraints within a StackView (Swift Programmatically)

I'm new to setting up StackViews and Buttons programmatically. I am getting some strange behavior with my constraints I cannot figure out what I'm doing wrong. It feels like I'm missing something simple. Any help is greatly appreciated!
I am trying to add two buttons to a StackView to create a custom tab bar. However, when I add the constraints to the buttons they are showing up outside the bottom of StackView. It's like the top constraint of Earth image isn't working. Any ideas? See image and code below.
// View to put in the StackView
class ProfileBottomTabBarView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = .blue
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// Calculate the screen height
public var screenHeight: CGFloat {
return UIScreen.main.bounds.height
}
// StackView height set to a proporation of screen height
let stackViewHeight = screenHeight * 0.07
// Views to put in the StackView
let profileIconView = ProfileBottomTabBarView()
let actIconView = ActBottomTabBarView()
let achieveIconView = AchieveBottomTabBarView()
let growIconView = GrowBottomTabBarView()
// Buttons to put in the Views
let profileButton = UIButton(type: .system)
let actButton = UIButton(type: .system)
let achieveButton = UIButton(type: .system)
let growButton = UIButton(type: .system)
let profileButtonText = UIButton(type: .system)
let actButtonText = UIButton(type: .system)
let achieveButtonText = UIButton(type: .system)
let growButtonText = UIButton(type: .system)
// Stackview setup
lazy var stackView: UIStackView = {
let stackV = UIStackView(arrangedSubviews: [profileIconView, actIconView, achieveIconView, growIconView])
stackV.translatesAutoresizingMaskIntoConstraints = false
stackV.axis = .horizontal
stackV.spacing = 20
stackV.distribution = .fillEqually
return stackV
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// Add StackView
view.addSubview(stackView)
stackView.bottomAnchor.constraint(equalTo: view.safeBottomAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.safeLeadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.safeTrailingAnchor).isActive = true
// Set height of the bottom tab bar as a proportion of the screen height.
stackView.heightAnchor.constraint(equalToConstant: stackViewHeight).isActive = true
profileIconView.topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
profileIconView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
profileIconView.heightAnchor.constraint(equalToConstant: stackViewHeight).isActive = true
// Add Buttons to the View
profileIconView.addSubview(profileButton)
profileIconView.addSubview(profileButtonText)
profileButton.translatesAutoresizingMaskIntoConstraints = false
profileButtonText.translatesAutoresizingMaskIntoConstraints = false
// Profile Button with Earth Image Setup
profileButton.setImage(UIImage(named: "earthIcon"), for: .normal)
profileButton.imageView?.contentMode = .scaleAspectFit
profileButton.topAnchor.constraint(equalTo: profileIconView.topAnchor).isActive = true
profileButton.bottomAnchor.constraint(equalTo: profileButtonText.topAnchor).isActive = true
profileButton.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor).isActive = true
//Set height of icon to a proportion of the stackview height
let profileButtonHeight = stackViewHeight * 0.8
profileButton.heightAnchor.constraint(equalTo: profileIconView.heightAnchor, constant: profileButtonHeight).isActive = true
profileButton.widthAnchor.constraint(equalToConstant: profileButtonHeight).isActive = true
profileButton.imageView?.widthAnchor.constraint(equalToConstant: profileButtonHeight)
profileButton.imageView?.heightAnchor.constraint(equalToConstant: profileButtonHeight)
// Profile Text Button Setup
profileButtonText.setTitle("Profile", for: .normal)
profileButtonText.titleLabel?.font = UIFont.boldSystemFont(ofSize: 12)
profileButtonText.setTitleColor(.white, for: .normal)
profileButtonText.topAnchor.constraint(equalTo: profileButton.bottomAnchor).isActive = true
profileButtonText.bottomAnchor.constraint(equalTo: profileIconView.bottomAnchor).isActive = true
profileButtonText.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor).isActive = true
//Set height of icon to a proportion of the stackview height
let profileButtonTextHeight = stackViewHeight * 0.2
profileButton.heightAnchor.constraint(equalTo: profileIconView.heightAnchor, constant: profileButtonTextHeight).isActive = true
profileButtonText.widthAnchor.constraint(equalToConstant: 40).isActive = true
}
A few things wrong with your constraints...
You're calculating heights / widths and using them as constants, but those values may (almost certainly will) change based on view lifecycle.
Better to use only related constraints. For example:
// constrain profile image button top, centerX and width relative to the iconView
profileButton.topAnchor.constraint(equalTo: profileIconView.topAnchor),
profileButton.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor),
profileButton.widthAnchor.constraint(equalTo: profileIconView.widthAnchor, multiplier: 1.0),
// constrain profile text button bottom, centerX and width relative to the iconView
profileButtonText.centerXAnchor.constraint(equalTo: profileIconView.centerXAnchor),
profileButtonText.widthAnchor.constraint(equalTo: profileIconView.widthAnchor, multiplier: 1.0),
profileButtonText.bottomAnchor.constraint(equalTo: profileIconView.bottomAnchor),
// constrain bottom of image button to top of text button (with a padding of 4-pts, change to suit)
profileButton.bottomAnchor.constraint(equalTo: profileButtonText.topAnchor, constant: -4.0),
// constrain height of text button to 20% of height of iconView
profileButtonText.heightAnchor.constraint(equalTo: profileIconView.heightAnchor, multiplier: 0.2),
To make things easier on yourself, I'd suggest creating a BottomTabBarView that handles adding and constraining your buttons:
class BottomTabBarView: UIView {
var theImageButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.imageView?.contentMode = .scaleAspectFit
return v
}()
var theTextButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.titleLabel?.font = UIFont.boldSystemFont(ofSize: 12)
v.setTitleColor(.white, for: .normal)
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
convenience init(withImageName imageName: String, labelText: String, bkgColor: UIColor) {
self.init()
self.commonInit()
theImageButton.setImage(UIImage(named: imageName), for: .normal)
theTextButton.setTitle(labelText, for: .normal)
backgroundColor = bkgColor
}
func commonInit() -> Void {
self.translatesAutoresizingMaskIntoConstraints = false
addSubview(theImageButton)
addSubview(theTextButton)
NSLayoutConstraint.activate([
// constrain profile image button top, centerX and width of the iconView
theImageButton.topAnchor.constraint(equalTo: topAnchor),
theImageButton.centerXAnchor.constraint(equalTo: centerXAnchor),
theImageButton.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0),
// constrain profile text button bottom, centerX and width of the iconView
theTextButton.centerXAnchor.constraint(equalTo: centerXAnchor),
theTextButton.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.0),
theTextButton.bottomAnchor.constraint(equalTo: bottomAnchor),
// constrain bottom of image button to top of text button
theImageButton.bottomAnchor.constraint(equalTo: theTextButton.topAnchor, constant: -4.0),
// set text button height to 20% of view height (instead of using intrinsic height)
theTextButton.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.2),
])
}
}
Now you can create each view with a single line, as in:
profileIconView = BottomTabBarView(withImageName: "earthIcon", labelText: "Profile", bkgColor: .blue)
And your view controller class becomes much simpler / cleaner:
class BenViewController: UIViewController {
// Views to put in the StackView
var profileIconView = BottomTabBarView()
var actIconView = BottomTabBarView()
var achieveIconView = BottomTabBarView()
var growIconView = BottomTabBarView()
// Stackview setup
lazy var stackView: UIStackView = {
let stackV = UIStackView(arrangedSubviews: [profileIconView, actIconView, achieveIconView, growIconView])
stackV.translatesAutoresizingMaskIntoConstraints = false
stackV.axis = .horizontal
stackV.spacing = 20
stackV.distribution = .fillEqually
return stackV
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
profileIconView = BottomTabBarView(withImageName: "earthIcon", labelText: "Profile", bkgColor: .blue)
actIconView = BottomTabBarView(withImageName: "actIcon", labelText: "Action", bkgColor: .brown)
achieveIconView = BottomTabBarView(withImageName: "achieveIcon", labelText: "Achieve", bkgColor: .red)
growIconView = BottomTabBarView(withImageName: "growIcon", labelText: "Grow", bkgColor: .purple)
// Add StackView
view.addSubview(stackView)
NSLayoutConstraint.activate([
// constrain stackView to bottom, leading and trailing (to safeArea)
stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
// Set height of the stackView (the bottom tab bar) as a proportion of the view height (7%).
stackView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.07),
])
}
}

UIView resize to fit labels inside of it

I have a UIView that I'm appending into a stack view on my main page of my app
This is the class of my view:
class MyCustomView: UIView {
public let leftLabel: UILabel = UILabel(frame: .zero)
public let rightLabel: UILabel = UILabel(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(leftLabel)
addSubview(rightLabel)
leftLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
leftLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.4),
leftLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
leftLabel.topAnchor.constraint(equalTo: topAnchor),
leftLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
rightLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6),
rightLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
rightLabel.topAnchor.constraint(equalTo: topAnchor),
rightLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
])
leftLabel.text = "Short string"
rightLabel.text = "Short string too"
}
}
And I append to my main stack view with:
let myCustomView = MyCustomView(frame: .zero)
stackView.addArrangedSubview(myCustomView)
This loads in my label's correctly and resizes everything as I'd want.
However, in my main class, I am updating myCustomView.rightLabel.text = <New Way Longer Text That Takes 2 Lines Instead of One>
The text is updating properly, but my myCustomView size is not resizing, so part of the text is just being cut-off
I have tried following other answers on here, but none of them seem to work for me.
Am I missing something small in order to force the resize of the customView to fit the label inside of it?
Thank you in advance
Your code does not show that you set the .numberOfLines in the label(s) to 0 to allow for multi-line labels.
Adding only that, should allow your labels to grow in height and to expand your custom view. However... that will also make both labels expand to the size of the tallest label, resulting in the text of the "shorter" label being vertically centered (I added background colors to make it easy to see the frames / bounds of the views):
If you constrain the Bottom of your custom view to the Bottom of each label at greaterThanOrEqualTo you can keep the labels "top-aligned":
You can run this code directly in a Playground page to see the results:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyCustomView: UIView {
public let leftLabel: UILabel = UILabel(frame: .zero)
public let rightLabel: UILabel = UILabel(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(leftLabel)
addSubview(rightLabel)
// background colors so we can see the view frames
backgroundColor = .cyan
leftLabel.backgroundColor = .yellow
rightLabel.backgroundColor = .green
// we want multi-line labels
leftLabel.numberOfLines = 0
rightLabel.numberOfLines = 0
// use auto-layout
leftLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// constrain to top
leftLabel.topAnchor.constraint(equalTo: topAnchor),
// constrain to left
leftLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
// constrain width = 40%
leftLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.4),
// constrain to top
rightLabel.topAnchor.constraint(equalTo: topAnchor),
// constrain to right
rightLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
// constrain width = 60%
rightLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6),
// constrain bottom of view (self) to >= 0 from bottom of leftLabel
bottomAnchor.constraint(greaterThanOrEqualTo: leftLabel.bottomAnchor, constant: 0.0),
// constrain bottom of view (self) to >= 0 from bottom of rightLabel
bottomAnchor.constraint(greaterThanOrEqualTo: rightLabel.bottomAnchor, constant: 0.0),
])
leftLabel.text = "Short string"
rightLabel.text = "Short string too"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyViewController : UIViewController {
var theButton: UIButton = {
let b = UIButton()
b.setTitle("Tap Me", for: .normal)
b.translatesAutoresizingMaskIntoConstraints = false
b.backgroundColor = .red
return b
}()
var theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.spacing = 8
v.distribution = .equalSpacing
return v
}()
var myView = MyCustomView()
// on button tap, change the text in the label(s)
#objc func didTap(_ sender: Any?) -> Void {
myView.leftLabel.text = "Short string with\nA\nB\nC\nD\nE"
myView.rightLabel.text = "Short string too\nA\nB"
}
override func loadView() {
let view = UIView()
self.view = view
view.backgroundColor = .white
view.addSubview(theButton)
// constrain button to Top: 32 and centerX
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 32.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
view.addSubview(theStackView)
// constrain stack view to Top: 100 and Leading/Trailing" 0
// no Bottom or Height constraint
NSLayoutConstraint.activate([
theStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0),
theStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
theStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
])
theStackView.addArrangedSubview(myView)
// add an action for the button tap
theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()