Swift - Update auto layout properties of UI components programmatically - ios

To design and create my UI, I always use auto layouts and do it programmatically instead of using storyboard.
In every view class of mine, I have a method called
private func setupView(frame:CGRect) {
/* START CONTAINER VIEW */
containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
addSubview(containerView)
containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: frame.width * (13 / IPHONE8_SCREEN_WIDTH)).isActive = true
containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -frame.width * (13 / IPHONE8_SCREEN_WIDTH)).isActive = true
containerView.topAnchor.constraint(equalTo: topAnchor, constant: frame.height * (26 / IPHONE8_SCREEN_HEIGHT)).isActive = true
containerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
containerView.backgroundColor = UIColor.white
/* END CONTAINER VIEW */
...
}
to initialize the components. Now let's say, in the method above, I initialize 10 UI components which are properly displayed when I run my code. However, depending on some variables, I have another function that is being called
private func addNextRoundInformation() {
..
nextRoundLabel = UILabel()
nextRoundLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(nextRoundLabel)
nextRoundLabel.leadingAnchor.constraint(equalTo: currentRoundLabel.leadingAnchor).isActive = true
nextRoundLabel.widthAnchor.constraint(equalTo:currentRoundLabel.widthAnchor).isActive = true
nextRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
}
which should place a new label between some others which were already initialized.
Of course, when putting the new label between some particular ones, I also update the auto layout constraints of the of the bottom label like
private func updateNumberOfWinnersLabelConstraint() {
numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: nextRoundLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: nextRoundLabelValue.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
}
The topAnchor of each label depends on the bottom anchor of the previous one.
With this approach, I can't see nextRoundLabel at all. It only appears, if I initialize it in the private func setupView(frame:CGRect) {}
Why?

You could do this with "Top-to-Bottom" constraints, but it would be rather complex.
You would need to essentially create a "Linked List" to track each view, the views above and below it, and its constraints.
So, to "insert" a new view after the 3rd view, you would need to:
deactivate the inter-view constraints
insert the new view into the linked list
re-create and activate the new constraints
Putting your views in a UIStackView turns that process into a single line of code:
stackView.insertArrangedSubview(newView, at: 3)
Here's a quick example:
class ViewController: UIViewController {
let testView: SampleView = SampleView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
let infoLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.numberOfLines = 0
v.text = "Tap to Insert \"New Label\"\nafter \"Label 3\""
return v
}()
let btn: UIButton = {
let v = UIButton()
v.setTitle("Insert", for: [])
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .systemBlue
return v
}()
[infoLabel, btn, testView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
infoLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.75),
infoLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
btn.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 20.0),
btn.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.75),
btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testView.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 40.0),
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
testView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
btn.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
}
#objc func btnTapped(_ sender: Any?) {
testView.insertNew()
}
}
class SampleView: UIView {
var containerView: UIView!
var stackView: UIStackView!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
backgroundColor = .red
setupView(frame: .zero)
}
private func setupView(frame:CGRect) {
/* START CONTAINER VIEW */
containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
addSubview(containerView)
stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(stackView)
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
containerView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
stackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0),
stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8.0),
])
containerView.backgroundColor = UIColor.white
/* END CONTAINER VIEW */
// add 10 labels to the stack view
for i in 1...10 {
let v = UILabel()
v.text = "Label \(i)"
v.backgroundColor = .green
stackView.addArrangedSubview(v)
}
}
func insertNew() {
let v = UILabel()
v.text = "New Label"
v.backgroundColor = .cyan
v.translatesAutoresizingMaskIntoConstraints = false
// we're adding a label after 3rd label
stackView.insertArrangedSubview(v, at: 3)
}
}
It starts looking like this:
after tapping the "Insert" button, it looks like this:
Edit
To explain why your current approach isn't working...
Starting with this layout:
each label's Top is constrained to the previous label's Bottom (with constant: spacing).
Those constraints are indicated by the blue arrows.
You then want to "insert" Next Round Label between Round Ends In and Winners Per Round:
Your code:
adds the label
adds a constraint from the Top of Next Round Label to the Bottom of Round Ends In
adds a constraint from the Top of Winners Per Round to the Bottom of Next Round Label
but... Winners Per Round already has a .topAnchor connected to Round Ends In, so it now has two top anchors.
The conflicting constraints are shown in red:
As I said, I think your description of what you're trying to do would lend itself to using stack views, and would make "inserting" views so much easier.
But, if you need to stick with your current "Top-to-Bottom" constraints approach, you have several options.
One - remove all the labels, then re-add and re-constrain them, including the one you're inserting.
Two - track the constraints (using an array or custom object properties) so you can deactivate the conflicting constraint(s).
Three - use some code along the lines of
let theConstraint = containerView.constraints.first(where: {($0.secondItem as? UILabel) == roundEndsInLabel})
to "find" the constraint that needs to be deactivated.

I found a working solution:
First, I declared two helper variables
/* START HELPER VARIABLES */
var numberOfWinnersLabelTopConstraint:NSLayoutConstraint?
var numberOfWinnersLabelValueTopConstraint:NSLayoutConstraint?
/* END HELPER VARIABLES */
then I made some minor adjustments in my setupView function:
private func setupView(frame:CGRect) {
...
/* START NUMBER OF WINNERS PER ROUND LABEL */
numberOfWinnersPerRoundLabel = UILabel()
numberOfWinnersPerRoundLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(numberOfWinnersPerRoundLabel)
numberOfWinnersPerRoundLabel.leadingAnchor.constraint(equalTo: currentRoundLabel.leadingAnchor).isActive = true
numberOfWinnersPerRoundLabel.widthAnchor.constraint(equalTo:currentRoundLabel.widthAnchor).isActive = true
// numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true // Replacing with helper variable
numberOfWinnersLabelTopConstraint = numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT))
numberOfWinnersLabelTopConstraint?.isActive = true
numberOfWinnersPerRoundLabel.text = NSLocalizedString(NUMBER_OF_WINNERS_PER_ROUND_TEXT, comment: "")
numberOfWinnersPerRoundLabel.textColor = .darkGray
numberOfWinnersPerRoundLabel.adjustsFontSizeToFitWidth = true
numberOfWinnersPerRoundLabel.font = .systemFont(ofSize: 12)
/* END NUMBER OF WINNERS PER ROUND LABEL */
/* START NUMBER OF WINNERS PER ROUND LABEL VALUE */
numberOfWinnersPerRoundLabelValue = UILabel()
numberOfWinnersPerRoundLabelValue.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(numberOfWinnersPerRoundLabelValue)
numberOfWinnersPerRoundLabelValue.leadingAnchor.constraint(equalTo: currentRoundLabelValue.leadingAnchor).isActive = true
numberOfWinnersPerRoundLabelValue.widthAnchor.constraint(equalTo:currentRoundLabelValue.widthAnchor).isActive = true
// numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: numberOfWinnersPerRoundLabel.topAnchor).isActive = true // replacing with helper variable
numberOfWinnersLabelValueTopConstraint = numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: numberOfWinnersPerRoundLabel.topAnchor)
numberOfWinnersLabelValueTopConstraint?.isActive = true
numberOfWinnersPerRoundLabelValue.textColor = .black
/* END NUMBER OF WINNERS PER ROUND LABEL VALUE */
By introducing the helper variables, I could easily deactivate the topConstraint when adding the nextRoundLabel
private func addNextRoundInformation() {
nextRoundLabel = UILabel()
nextRoundLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(nextRoundLabel)
nextRoundLabel.leadingAnchor.constraint(equalTo: currentRoundLabel.leadingAnchor).isActive = true
nextRoundLabel.widthAnchor.constraint(equalTo:currentRoundLabel.widthAnchor).isActive = true
nextRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
nextRoundLabel.text = "Next round starts in"
nextRoundLabel.textColor = .darkGray
nextRoundLabel.font = .systemFont(ofSize: 12)
nextRoundLabelValue = UILabel()
nextRoundLabelValue.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(nextRoundLabelValue)
nextRoundLabelValue.leadingAnchor.constraint(equalTo: currentRoundLabelValue.leadingAnchor).isActive = true
nextRoundLabelValue.widthAnchor.constraint(equalTo:currentRoundLabelValue.widthAnchor).isActive = true
nextRoundLabelValue.topAnchor.constraint(equalTo:nextRoundLabel.topAnchor).isActive = true
nextRoundLabelValue.textColor = .black
nextRoundLabelValue.text = "Next round label value"
nextRoundLabelValue.font = .systemFont(ofSize: 14)
}
private func updateNumberOfWinnersLabelConstraint() {
numberOfWinnersLabelTopConstraint?.isActive = false // Deactivate previous constraint
numberOfWinnersLabelValueTopConstraint?.isActive = false
numberOfWinnersLabelTopConstraint = numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: nextRoundLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT))
numberOfWinnersLabelTopConstraint?.isActive = true
numberOfWinnersLabelValueTopConstraint = numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: nextRoundLabelValue.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT))
numberOfWinnersLabelValueTopConstraint?.isActive = true
}
Basically, I only had to update the topConstraints of the numberOfWinnersPerRoundLabel and numberOfWinnersPerRoundLabelValue
since everything else would be the same. No changes needed for currentRoundLabel.
I tested it and it worked!

Related

UIScrollView – Adjust User Scroll Amount Akin to Scrubbing?

I have UIScrollview that the user scrolls up and down.
Is there a way to adjust how much the user's drag of the finger results in the final scroll amount?
I was looking at UIScrollview delegate methods, but haven't found a place to alter that.
scrollViewDidScroll(_:) seems too late since this is AFTER the event.
iOS users are very familiar with using scrolling views, so changing the "scroll speed" may be confusing. However, it's your app :)
Give this a try...
When the user Begins dragging, we'll grab the .contentOffset.y as a "starting point." In scrollViewDidScroll, we'll get the difference between the new .contentOffset.y and the startingY ... multiply that by the speed factor ... and then change the .contentOffset.y.
Note that manually setting .contentOffset.y triggers scrollViewDidScroll, so we'll also need to set a bool flag to prevent recursion:
class SlowScrollVC: UIViewController, UIScrollViewDelegate {
// scrollSpeed --- example values
// 1.0 == normal
// 1.5 == fast
// 0.5 == slow
var scrollSpeed: CGFloat = 0.5
var startingOffsetY: CGFloat = 0
var bManualOffset: Bool = false
let scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
// add a bunch of labels and buttons so we have something to scroll
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 40
for i in 1...20 {
let v = UILabel()
v.text = "Label \(i)"
v.textAlignment = .center
v.backgroundColor = .cyan
stack.addArrangedSubview(v)
let b = UIButton()
b.setTitle("Button \(i)", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.backgroundColor = .systemBlue
b.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
stack.addArrangedSubview(b)
}
stack.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stack)
view.addSubview(scrollView)
// so we can see the scroll view frame
scrollView.backgroundColor = .yellow
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
stack.topAnchor.constraint(equalTo: cg.topAnchor, constant: 8.0),
stack.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 8.0),
stack.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -8.0),
stack.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -8.0),
stack.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -16.0),
])
scrollView.delegate = self
// you may also want to adjust .decelerationRate
// try various values to see the result
//scrollView.decelerationRate = UIScrollView.DecelerationRate(rawValue: 0.99)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
startingOffsetY = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !bManualOffset {
// get the difference between previous offset.y and new offset.y
let diff = scrollView.contentOffset.y - startingOffsetY
// adjust by scroll-speed factor
let newY = startingOffsetY + diff * scrollSpeed
// prevent recursion
bManualOffset = true
// set adjusted offset.y
scrollView.contentOffset.y = newY
// update start Y
startingOffsetY = newY
}
bManualOffset = false
}
#objc func btnTap(_ sender: UIButton) {
// just to confirm we tapped a button
print("Tap:", sender.currentTitle)
}
}

Change default StackView animation

Forgive me if explanation is not excellent. Basically, the video below shows the standard animation for hiding labels in a stack view. Notice it looks like the labels "slide" and "collapse together".
I still want to hide the labels, but want an animation where the alpha changes but the labels don't "slide". Instead, the labels change alpha and stay in place. Is this possible with stack views?
This is the code I have to animate:
UIView.animate(withDuration: 0.5) {
if self.isExpanded {
self.topLabel.alpha = 1.0
self.bottomLabel.alpha = 1.0
self.topLabel.isHidden = false
self.bottomLabel.isHidden = false
} else {
self.topLabel.alpha = 0.0
self.bottomLabel.alpha = 0.0
self.topLabel.isHidden = true
self.bottomLabel.isHidden = true
}
}
Update 1
It seems that even without a stack view, if I animate the height constraint, you get this "squeeze" effect. Example:
UIView.animate(withDuration: 3.0) {
self.heightConstraint.constant = 20
self.view.layoutIfNeeded()
}
Here are a couple options:
Set .contentMode = .top on the labels. I've never found Apple docs that clearly describe using .contentMode with UILabel, but it works and should work.
Embed the label in a UIView, constrained to the top, with Content Compression Resistance Priority set to .required, less-than-required priority for the bottom constraint, and .clipsToBounds = true on the view.
Example 1 - content mode:
class StackAnimVC: UIViewController {
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
let topLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
let botLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
let headerLabel = UILabel()
let threeLabel = UILabel()
let footerLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// label setup
let colors: [UIColor] = [
.systemYellow,
.cyan,
UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
UIColor(white: 0.9, alpha: 1.0),
]
for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
v.backgroundColor = c
v.font = .systemFont(ofSize: 24.0, weight: .light)
stackView.addArrangedSubview(v)
}
headerLabel.text = "Header"
threeLabel.text = "Three"
footerLabel.text = "Footer"
topLabel.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
botLabel.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
// we want 8-pts "padding" under the "collapsible" labels
stackView.setCustomSpacing(8.0, after: topLabel)
stackView.setCustomSpacing(8.0, after: botLabel)
// let's add a label and a Switch to toggle the labels .contentMode
let promptView = UIView()
let hStack = UIStackView()
hStack.spacing = 8
let prompt = UILabel()
prompt.text = "Content Mode Top:"
prompt.textAlignment = .right
let sw = UISwitch()
sw.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
hStack.addArrangedSubview(prompt)
hStack.addArrangedSubview(sw)
hStack.translatesAutoresizingMaskIntoConstraints = false
promptView.addSubview(hStack)
// add an Animate button
let btn = UIButton(type: .system)
btn.setTitle("Animate", for: [])
btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
let g = view.safeAreaLayoutGuide
// add elements to view and give them all the same Leading and Trailing constraints
[promptView, stackView, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
NSLayoutConstraint.activate([
promptView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stackView.topAnchor.constraint(equalTo: promptView.bottomAnchor, constant: 0.0),
// center the hStack in the promptView
hStack.centerXAnchor.constraint(equalTo: promptView.centerXAnchor),
hStack.centerYAnchor.constraint(equalTo: promptView.centerYAnchor),
promptView.heightAnchor.constraint(equalTo: hStack.heightAnchor, constant: 16.0),
// put button near bottom
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
}
#objc func switchChanged(_ sender: UISwitch) {
[topLabel, botLabel].forEach { v in
v.contentMode = sender.isOn ? .top : .left
}
}
#objc func btnTap(_ sender: UIButton) {
UIView.animate(withDuration: 0.5) {
// toggle hidden and alpha on stack view labels
self.topLabel.alpha = self.topLabel.isHidden ? 1.0 : 0.0
self.botLabel.alpha = self.botLabel.isHidden ? 1.0 : 0.0
self.topLabel.isHidden.toggle()
self.botLabel.isHidden.toggle()
}
}
}
Example 2 - label embedded in a UIView:
class TopAlignedLabelView: UIView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
self.addSubview(label)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
])
// we need bottom anchor to have
// less-than-required Priority
let c = label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
c.priority = .required - 1
c.isActive = true
// don't allow label to be compressed
label.setContentCompressionResistancePriority(.required, for: .vertical)
// we need to clip the label
self.clipsToBounds = true
}
}
class StackAnimVC: UIViewController {
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
let topLabel: TopAlignedLabelView = {
let v = TopAlignedLabelView()
return v
}()
let botLabel: TopAlignedLabelView = {
let v = TopAlignedLabelView()
return v
}()
let headerLabel = UILabel()
let threeLabel = UILabel()
let footerLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// label setup
let colors: [UIColor] = [
.systemYellow,
.cyan,
UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
UIColor(white: 0.9, alpha: 1.0),
]
for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
v.backgroundColor = c
if let vv = v as? UILabel {
vv.font = .systemFont(ofSize: 24.0, weight: .light)
}
if let vv = v as? TopAlignedLabelView {
vv.label.font = .systemFont(ofSize: 24.0, weight: .light)
}
stackView.addArrangedSubview(v)
}
headerLabel.text = "Header"
threeLabel.text = "Three"
footerLabel.text = "Footer"
topLabel.label.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
botLabel.label.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
// we want 8-pts "padding" under the "collapsible" labels
stackView.setCustomSpacing(8.0, after: topLabel)
stackView.setCustomSpacing(8.0, after: botLabel)
// add an Animate button
let btn = UIButton(type: .system)
btn.setTitle("Animate", for: [])
btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
let g = view.safeAreaLayoutGuide
// add elements to view and give them all the same Leading and Trailing constraints
[stackView, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// put button near bottom
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
}
#objc func btnTap(_ sender: UIButton) {
UIView.animate(withDuration: 0.5) {
// toggle hidden and alpha on stack view labels
self.topLabel.alpha = self.topLabel.isHidden ? 1.0 : 0.0
self.botLabel.alpha = self.botLabel.isHidden ? 1.0 : 0.0
self.topLabel.isHidden.toggle()
self.botLabel.isHidden.toggle()
}
}
}
Edit
If your goal is to have the Brown label "slide up and cover" both the Blue and Pink labels, with neither of those labels compressing or moving, take a similar approach:
use standard UILabel instead of the TopAlignedLabelView
embed the Blue and Pink labels in their own stack view
embed that stack view in a "container" view
constrain that stack view to be "top-aligned" like we did with the label in the TopAlignedLabelView
The arranged subviews of the "outer" stack view will now be:
Yellow label
"container" view
Brown label
Gray label
and to animate we'll toggle the .alpha and .isHidden on the "container" view instead of the Blue and Pink labels.
I edited the controller class -- give it a try and see if that's the effect you're after.
If it is, I strongly suggest you try to make those changes yourself... if you run into problems, use this example code as a guide:
class StackAnimVC: UIViewController {
let outerStackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 0
return v
}()
// create an "inner" stack view
// this will hold topLabel and botLabel
let innerStackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
return v
}()
// container for the inner stack view
let innerStackContainer: UIView = {
let v = UIView()
v.clipsToBounds = true
return v
}()
// we can use standard UILabels instead of custom views
let topLabel = UILabel()
let botLabel = UILabel()
let headerLabel = UILabel()
let threeLabel = UILabel()
let footerLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// label setup
let colors: [UIColor] = [
.systemYellow,
.cyan,
UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
UIColor(white: 0.9, alpha: 1.0),
]
for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
v.backgroundColor = c
v.font = .systemFont(ofSize: 24.0, weight: .light)
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
// add top and bottom labels to inner stack view
innerStackView.addArrangedSubview(topLabel)
innerStackView.addArrangedSubview(botLabel)
// add inner stack view to container
innerStackView.translatesAutoresizingMaskIntoConstraints = false
innerStackContainer.addSubview(innerStackView)
// constraints for inner stack view
// bottom constraint must be less-than-required
// so it doesn't compress when the container compresses
let isvBottom: NSLayoutConstraint = innerStackView.bottomAnchor.constraint(equalTo: innerStackContainer.bottomAnchor, constant: -8.0)
isvBottom.priority = .defaultHigh
NSLayoutConstraint.activate([
innerStackView.topAnchor.constraint(equalTo: innerStackContainer.topAnchor, constant: 0.0),
innerStackView.leadingAnchor.constraint(equalTo: innerStackContainer.leadingAnchor, constant: 0.0),
innerStackView.trailingAnchor.constraint(equalTo: innerStackContainer.trailingAnchor, constant: 0.0),
isvBottom,
])
topLabel.numberOfLines = 0
botLabel.numberOfLines = 0
topLabel.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
botLabel.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
headerLabel.text = "Header"
threeLabel.text = "Three"
footerLabel.text = "Footer"
// add views to outer stack view
[headerLabel, innerStackContainer, threeLabel, footerLabel].forEach { v in
outerStackView.addArrangedSubview(v)
}
// add an Animate button
let btn = UIButton(type: .system)
btn.setTitle("Animate", for: [])
btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
let g = view.safeAreaLayoutGuide
// add elements to view and give them all the same Leading and Trailing constraints
[outerStackView, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
NSLayoutConstraint.activate([
outerStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// put button near bottom
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
])
}
#objc func btnTap(_ sender: UIButton) {
UIView.animate(withDuration: 0.5) {
// toggle hidden and alpha on inner stack container
self.innerStackContainer.alpha = self.innerStackContainer.isHidden ? 1.0 : 0.0
self.innerStackContainer.isHidden.toggle()
}
}
}
Edit 2
A quick explanation of why this works...
Consider a typical UILabel as a subview of a UIView. We constrain the label to the view on all 4 sides with a little "padding":
aLabel.topAnchor.constraint(equalTo: aView.topAnchor, constant: 8.0),
aLabel.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 8.0),
aLabel.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: -8.0),
aLabel.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: -8.0),
Now we can constrain the view's Top / Leading / Trailing -- but not Bottom or Height -- and the label's intrinsic Height will control the Height of the view.
Pretty basic.
But, if we want to "animate it out of existence," changing the Height of the view will also change the Height of the label, resulting in a "squeeze" effect. We'll also get auto-layout complaints, because the constraints cannot be satisfied.
So, we need to change the .priority of the label's Bottom constraint to allow it to remain at its intrinsic Height, while its superview's Height changes.
Each of these 4 examples uses the same Top / Leading / Trailing constraints... the only difference is what we do with the Bottom constraint:
For Example 1, we don't set any Bottom constraint. So, we never even see its superview and animating the Height of its superview has no effect on the label.
For Example 2, we set the "normal" Bottom constraint, and we see the "squeezing" effect.
For Example 3, we give the label's Bottom constraint .priority = .defaultHigh. The label still controls the Height of its superview... until we activate the superview's Height constraint (of zero). The superview collapses, but we've given auto-layout permission to break the Bottom constraint.
Example 4 is the same as 3, but we've also set .clipsToBounds = true on the container view so the label Height remains constant, but no longer extends outside its superview.
All of that also applies to views in a stack view when setting .isHidden on an arranged subview.
Here's the code that generates that example, if you want to inspect it and play around with the variations:
class DemoVC: UIViewController {
var containerViews: [UIView] = []
var heightConstraints: [NSLayoutConstraint] = []
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
// create 4 container views, each with a label as a subview
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue, .systemYellow,
]
colors.forEach { bkgColor in
let thisContainer = UIView()
thisContainer.translatesAutoresizingMaskIntoConstraints = false
let thisLabel = UILabel()
thisLabel.translatesAutoresizingMaskIntoConstraints = false
thisContainer.backgroundColor = bkgColor
thisLabel.backgroundColor = UIColor(red: 0.75, green: 0.9, blue: 1.0, alpha: 1.0)
thisLabel.numberOfLines = 0
//thisLabel.font = .systemFont(ofSize: 20.0, weight: .light)
thisLabel.font = .systemFont(ofSize: 12.0, weight: .light)
thisLabel.text = "We want to animate compressing the \"container\" view vertically, without it squeezing or moving this label."
// add label to container view
thisContainer.addSubview(thisLabel)
// add container view to array
containerViews.append(thisContainer)
// add container view to view
view.addSubview(thisContainer)
NSLayoutConstraint.activate([
// each example gets the label constrained
// Top / Leading / Trailing to its container view
thisLabel.topAnchor.constraint(equalTo: thisContainer.topAnchor, constant: 8.0),
thisLabel.leadingAnchor.constraint(equalTo: thisContainer.leadingAnchor, constant: 8.0),
thisLabel.trailingAnchor.constraint(equalTo: thisContainer.trailingAnchor, constant: -8.0),
// we'll be using different bottom constraints for the examples,
// so don't set it here
//thisLabel.bottomAnchor.constraint(equalTo: thisContainer.bottomAnchor, constant: -8.0),
// each container view gets constrained to the top
thisContainer.topAnchor.constraint(equalTo: g.topAnchor, constant: 60.0),
])
// setup the container view height constraints, but don't activate them
let hc = thisContainer.heightAnchor.constraint(equalToConstant: 0.0)
// add the constraint to the constraints array
heightConstraints.append(hc)
}
// couple vars to reuse
var prevContainer: UIView!
var aContainer: UIView!
var itsLabel: UIView!
var bc: NSLayoutConstraint!
// -------------------------------------------------------------------
// first example
// we don't add a bottom constraint for the label
// that means we'll never see its container view
// and changing its height constraint won't do anything to the label
// -------------------------------------------------------------------
// second example
aContainer = containerViews[1]
itsLabel = aContainer.subviews.first
// we'll add a "standard" bottom constraint
// so now we see its container view
bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
bc.isActive = true
// -------------------------------------------------------------------
// third example
aContainer = containerViews[2]
itsLabel = aContainer.subviews.first
// add the same bottom constraint, but give it a
// less-than-required Priority so it won't "squeeze"
bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
bc.priority = .defaultHigh
bc.isActive = true
// -------------------------------------------------------------------
// fourth example
aContainer = containerViews[3]
itsLabel = aContainer.subviews.first
// same less-than-required Priority bottom constraint,
bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
bc.priority = .defaultHigh
bc.isActive = true
// we'll also set clipsToBounds on the container view
// so it will "hide / reveal" the label
aContainer.clipsToBounds = true
// now we need to layout the views
// constrain first example leading
aContainer = containerViews[0]
aContainer.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0).isActive = true
prevContainer = aContainer
for i in 1..<containerViews.count {
aContainer = containerViews[i]
aContainer.leadingAnchor.constraint(equalTo: prevContainer.trailingAnchor, constant: 8.0).isActive = true
aContainer.widthAnchor.constraint(equalTo: prevContainer.widthAnchor).isActive = true
prevContainer = aContainer
}
// constrain last example trailing
prevContainer.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0).isActive = true
// and, let's add labels above the 4 examples
for (i, v) in containerViews.enumerated() {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Example \(i + 1)"
label.font = .systemFont(ofSize: 14.0, weight: .light)
view.addSubview(label)
NSLayoutConstraint.activate([
label.bottomAnchor.constraint(equalTo: v.topAnchor, constant: -4.0),
label.centerXAnchor.constraint(equalTo: v.centerXAnchor),
])
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
heightConstraints.forEach { c in
c.isActive = !c.isActive
}
UIView.animate(withDuration: 1.0, animations: {
self.view.layoutIfNeeded()
})
}
}

I cannot figure out what causes layout constraint warnings in my message bubble cell

I've been trying to solve this problem for a few days now and I haven't been able to understand what is wrong with my constraints, and warnings keep popping up in the console.
Here are my UI elements:
let messageText : UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.backgroundColor = .clear
label.clipsToBounds = false
label.font = UIFont.systemFont(ofSize: 17)
return label
}()
private let messageCard : UIView = {
let card = UIView()
card.translatesAutoresizingMaskIntoConstraints = false
card.layer.cornerRadius = 16
card.layer.masksToBounds = true
card.clipsToBounds = false
return card
}()
private let avatar : CachedImageView = {
var imageView = CachedImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.heightAnchor.constraint(equalToConstant: 15).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 15).isActive = true
imageView.isAvatar = true
return imageView
}()
private let stackView : UIStackView = {
let stView = UIStackView()
stView.translatesAutoresizingMaskIntoConstraints = false
stView.backgroundColor = .clear
stView.axis = .vertical
return stView
}()
Here is my initialiser for the cell:
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.clipsToBounds = false
stackView.addArrangedSubview(messageText)
stackView.addArrangedSubview(avatar)
addSubview(messageCard)
addSubview(stackView)
let constraints = [
stackView.topAnchor.constraint(equalTo: topAnchor, constant: bubbleMargin + bubblePadding),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -(bubbleMargin + bubblePadding)),
stackView.widthAnchor.constraint(lessThanOrEqualToConstant: 300),
stackView.widthAnchor.constraint(greaterThanOrEqualToConstant: 30),
//stackView.heightAnchor.constraint(greaterThanOrEqualToConstant: 10), // ну хз
messageCard.topAnchor.constraint(equalTo: stackView.topAnchor, constant: -bubblePadding),
messageCard.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: bubblePadding),
messageCard.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: -bubblePadding),
messageCard.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: bubblePadding),
/*messageText.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 20),
messageText.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -20),*/
/*avatar.heightAnchor.constraint(equalToConstant: 15),
avatar.widthAnchor.constraint(equalToConstant: 15),
avatar.centerXAnchor.constraint(equalTo: stackView.centerXAnchor),
avatar.centerYAnchor.constraint(equalTo: stackView.centerYAnchor),*/
]
leftConstr = stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: bubbleMargin + bubblePadding)
rightConstr = stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -(bubbleMargin + bubblePadding))
leftConstr.isActive = true
for c in constraints {
c.isActive = true
}
}
Here is the method that fills the message bubble:
func setMessage() {
messageText.text = messageWrapper.message.text
messageText.textColor = .white
messageCard.backgroundColor = messageWrapper.message.out == 1 ? .darkGray : .lightGray
leftConstr.isActive = messageWrapper.message.out == 0
rightConstr.isActive = messageWrapper.message.out == 1
avatar.isHidden = messageWrapper.message.out == 1
if messageWrapper.message.out == 0 {
if let photo = messageWrapper.group?.photo50 {
avatar.setSource(url: photo)
}
if let photo = messageWrapper.profile?.photo100 {
avatar.setSource(url: photo)
}
}
}
I've also noticed that the warnings really only appear when I start scrolling, but the warning suggests that there is something wrong with a constraints that the setMessage() method doesn't ever touch.
screenshot of the warning, since stack overflow really wasn't having it as a code snippet
Looks like your stack view with needs to be <= 300 points, but the stack view width must be equal to message cell width - 13 - 13. Since your message cell width is 414, and 414 - 13 - 13 = 388, and 388 is not <= 300, you get the error.
You should either remove the stack view's width constraint, or change either the leading or trailing constraint to be more flexible.
There are many solutions. For example, you could remove these two lines:
messageCard.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: -bubblePadding),
messageCard.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: bubblePadding),
and instead center the stack view within messageCard.
But ultimately you need to not give it conflicting constraints.

Swift textview growing direction

so i have a textview inside a uiview. And my question is how to make my textivew grow in up direction when textview goes to the next line as well as my uiview.
var textheightcontraint : NSLayoutConstraint!
var viewheightconstraint : NSLayoutConstraint!
func setup4(){
view.addSubview(colorview)
colorview.topAnchor.constraint(equalTo: customtableview.bottomAnchor).isActive = true
colorview.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
colorview.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
viewheightconstraint = colorview.heightAnchor.constraint(equalToConstant: 44)
viewheightconstraint.isActive = true
colorview.backgroundColor = UIColor.lightGray
colorview.addSubview(customtextview2)
customtextview2.backgroundColor = .white
customtextview2.leftAnchor.constraint(equalTo: colorview.leftAnchor).isActive = true
customtextview2.rightAnchor.constraint(equalTo: colorview.rightAnchor, constant: -20).isActive = true
customtextview2.bottomAnchor.constraint(equalTo: colorview.bottomAnchor).isActive = true
textheightcontraint = customtextview2.heightAnchor.constraint(equalToConstant: 39)
textheightcontraint.isActive = true
customtextview2.delegate = self
}
func setuptextview(){
let fixedWidth = customtextview2.frame.size.width
let newSize = customtextview2.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
self.textheightcontraint.constant = newSize.height
self.viewheightconstraint.constant = newSize.height
self.view.layoutIfNeeded()
}
func textViewDidChange(_ textView: UITextView) {
setuptextview()
}
If I understand your question, you want the text view (and the view it's contained in) to keep its bottom at the same position, and expand upward as you type?
To do that, disable scrolling in the text view, and set up your constraints so the bottom of the containing view is constrained to a y-position:
//
// ViewController.swift
//
// Created by Don Mag on 8/9/18.
//
import UIKit
class ViewController: UIViewController {
var theContainingView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var theTextView: UITextView = {
let v = UITextView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .yellow
theContainingView.backgroundColor = .cyan
theContainingView.addSubview(theTextView)
view.addSubview(theContainingView)
NSLayoutConstraint.activate([
theTextView.topAnchor.constraint(equalTo: theContainingView.topAnchor, constant: 8.0),
theTextView.bottomAnchor.constraint(equalTo: theContainingView.bottomAnchor, constant: -8.0),
theTextView.leadingAnchor.constraint(equalTo: theContainingView.leadingAnchor, constant: 8.0),
theTextView.trailingAnchor.constraint(equalTo: theContainingView.trailingAnchor, constant: -8.0),
theContainingView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40.0),
theContainingView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40.0),
// Constrain the Bottom of the containing view. As the textView grows (or shrinks) with input,
// the TOP of the view will move up or down
theContainingView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: 300.0),
])
theTextView.isScrollEnabled = false
theTextView.text = "This is the starting text."
}
}
Results:

Autolayout: Center two views with Constraint Inequalities

I want to programmatically center two or more views within a container using only constraint inequalities. It would look something like this:
I have achieved this behavior using another view to group both labels and setting its centerXAnchor equal to the superview's centerXAnchor. I'm also aware that it's possible to achieve the same goal using two spacer views. The problem is that my application will have quite a lot of views, so I'd like to use as few subviews as possible in order to save resources
I have tried using constraint inequalities (first label's leading anchor >= view's leading anchor and second label's trailing anchor <= view's trailing anchor) to center these two views but the result I get is this.
code:
class TestView: UIView {
let firstLabel = UILabel()
let secondLabel = UILabel()
init() {
super.init(frame: .zero)
self.layer.borderColor = UIColor.gray.cgColor
self.layer.borderWidth = 1
setupSubviewsConstraints()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupSubviewsConstraints() {
self.addSubview(firstLabel)
self.addSubview(secondLabel)
firstLabel.layer.borderColor = UIColor.yellow.cgColor
firstLabel.layer.borderWidth = 1
firstLabel.text = "first"
firstLabel.translatesAutoresizingMaskIntoConstraints = false
firstLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 5).isActive = true
firstLabel.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor, constant: 5).isActive = true
firstLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -5).isActive = true
secondLabel.layer.borderColor = UIColor.blue.cgColor
secondLabel.layer.borderWidth = 1
secondLabel.text = "second"
secondLabel.translatesAutoresizingMaskIntoConstraints = false
secondLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 5).isActive = true
secondLabel.leadingAnchor.constraint(equalTo: firstLabel.trailingAnchor, constant: 5).isActive = true
secondLabel.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -5).isActive = true
secondLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -5).isActive = true
}

Resources