Dynamic font resizing in collectionview [closed] - ios

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 5 years ago.
Improve this question
for my app I implemented a collectionview showing cards, that are rotated in a carousel-like animation. My problem is that the cards themselves resize correctly when swiped, but the fonts stay the same size or resize incorrectly. To be specific, the answers (bottommost 5 labels) are in a stackview.
Things I tried:
AutoLayout
Autoshrink
adjustsFontSizeToFitWidth
any number of different constraints
I attached a few screenshots below, where I colored the labels for better visibility.

To change the size of a view and have all of it's subviews / buttons / labels / etc scale with it - including label fonts, you are better off using CGAffineTransform for scaling.
Here is a simple example. It can be pasted into a Playground page to see the effect:
import UIKit
import PlaygroundSupport
class TestViewController : UIViewController {
let theStackView: UIStackView = {
let sv = UIStackView()
sv.translatesAutoresizingMaskIntoConstraints = false
sv.axis = .vertical
sv.distribution = .equalCentering
sv.alignment = .center
return sv
}()
let theContainerView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .green
return v
}()
let btn: UIButton = {
let b = UIButton()
b.setTitle("Tap to Scale", for: .normal)
b.backgroundColor = .red
b.translatesAutoresizingMaskIntoConstraints = false
return b
}()
// on button tap, scale the Container view by 50% both ways
// note that Container view's subviews also scale
func btnTapped(_ sender: Any) {
theContainerView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
}
override func viewDidLoad() {
super.viewDidLoad()
// add the button to self.view
self.view.addSubview(btn)
// button position
btn.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
btn.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 20.0).isActive = true
// add a target for the button tap
btn.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
// add our "Container" view
view.addSubview(theContainerView)
// add our Stack view to the Container view
theContainerView.addSubview(theStackView)
// add 5 labels to the Stack view
for i in 1...5 {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 20.0)
label.text = "This is Label \(i)"
label.backgroundColor = .cyan
label.translatesAutoresizingMaskIntoConstraints = false
theStackView.addArrangedSubview(label)
}
// pin Container view 20-pts from the bottom of the button, and 8-pts from left, right and bottom
theContainerView.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 20.0).isActive = true
theContainerView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0).isActive = true
theContainerView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -8.0).isActive = true
theContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8.0).isActive = true
// pin Stack view 8-pts from top, left, right and bottom of the Container view
theStackView.topAnchor.constraint(equalTo: theContainerView.topAnchor, constant: 8.0).isActive = true
theStackView.leftAnchor.constraint(equalTo: theContainerView.leftAnchor, constant: 8.0).isActive = true
theStackView.rightAnchor.constraint(equalTo: theContainerView.rightAnchor, constant: -8.0).isActive = true
theStackView.bottomAnchor.constraint(equalTo: theContainerView.bottomAnchor, constant: -8.0).isActive = true
}
}
let vc = TestViewController()
vc.view.backgroundColor = .yellow
PlaygroundPage.current.liveView = vc

Related

Swift - Update auto layout properties of UI components programmatically

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!

Align UIButton and UILabel text with different font sizes

I have a UIButton and a UILabel displayed inline. They have different size fonts, however I would like to align them so they appear on the same line.
At the moment the UILabel is slight above the baseline of the UIButton.
I was hoping to avoid manually setting a content offset as I want this to scale correctly where possible. I worry manual calculations may have unexpected side effects on changing font sizes etc.
I have created a playground that should show the 2 elements:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
lazy var nameButton = configure(UIButton(type: .system), using: {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.titleLabel?.font = .systemFont(ofSize: 18)
$0.setTitleColor(.darkGray, for: .normal)
$0.contentHorizontalAlignment = .leading
$0.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal)
$0.backgroundColor = .lightGray
$0.setTitle("This is a button", for: .normal)
})
lazy var publishedDateLabel = configure(UILabel(frame: .zero), using: {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.font = .systemFont(ofSize: 14)
$0.textColor = .darkGray
$0.setContentHuggingPriority(UILayoutPriority.defaultLow, for: .horizontal)
$0.backgroundColor = .lightGray
$0.text = "and this is a label"
})
override func loadView() {
let view = UIView()
view.backgroundColor = .white
[nameButton, publishedDateLabel].forEach(view.addSubview(_:))
NSLayoutConstraint.activate([
nameButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
nameButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
publishedDateLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
publishedDateLabel.leadingAnchor.constraint(equalTo: nameButton.trailingAnchor),
publishedDateLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8)
])
self.view = view
}
// setup helper method
func configure<T>(_ value: T, using closure: (inout T) throws -> Void) rethrows -> T {
var value = value
try closure(&value)
return value
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
I have tried making the label and button the same height by adding publishedDateLabel.heightAnchor.constraint(equalTo: nameButton.heightAnchor)
This didn't change the alignment however.
I also tried using publishedDateLabel.lastBaselineAnchor.constraint(equalTo: nameButton.lastBaselineAnchor)
to align the anchors however this aligned the top of the elements
How can align the bottom of the text in the button to the bottom of the text in the label?
Just comment out the heightAnchor use the lastBaselineAnchor:
NSLayoutConstraint.activate([
nameButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
nameButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
publishedDateLabel.lastBaselineAnchor.constraint(equalTo: nameButton.lastBaselineAnchor),
publishedDateLabel.leadingAnchor.constraint(equalTo: nameButton.trailingAnchor),
publishedDateLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8)
])

Extracting a childView and repositioning it inside of a new parentView

I’m trying to rip a view from a stackView that is embedded in a scrollView and then reposition said view in the same location but in another view at the same level in the view hierarchy as the scrollView.
The effect I’m trying to achieve is that I’m animating the removal of a view— where the view would be super imposed in another view, while the scrollView would scroll up and new view would be added to the stackView all while the view that was ripped fades out.
Unfortunately, achieving this effect remains elusive as the rippedView is position at (x: 0, y: 0). When I try force a new frame onto this view its tough because Im guessing the pixel perfect correct frame. Here’s a bit of the code from my viewController:
/*
I tried to make insertionView and imposeView have the same dimensions as the scrollView and
the stackView respectively as I thought if the rippedView’s original superView is the same
dimensions as it’s new superView, the rippedView would be positioned in the same place
without me needing to alter its frame.
*/
let insertionView = UIView(frame: scrollView.frame)
let imposeView = UIView(frame: stackView.frame)
rippedView.removeFromSuperview()
insertionView.addSubview(imposeView)
imposeView.addSubview(rippedView)
let newFrame = CGRect(x: 0, y: 450, width: rippedView.intrinsicContentSize.width, height:
rippedView.intrinsicContentSize.height)
rippedView.frame = newFrame
self.view.addSubview(insertionView)
Before removing rippedView, get it's actual frame:
let newFrame = self.view.convert(rippedView.bounds, from: rippedView)
The issue you are hitting is likely due to the stackView's arranged subviews having .translatesAutoresizingMaskIntoConstraints set to false. I believe this happens automatically when you add a view to a stackView, unless you specify otherwise.
A stackView's arranged subviews have coordinates relative to the stackView itself. So the first view will be at 0,0. Since you are adding a "container" view with the same frame as the stackView, you can use the same coordinate space... but you'll need to enable .translatesAutoresizingMaskIntoConstraints.
Try it like this:
#objc func btnTapped(_ sender: Any?) -> Void {
// get a reference to the 3rd arranged subview in the stack view
let rippedView = stackView.arrangedSubviews[2]
// local var holding the rippedView frame (as set by the stackView)
// get it before moving view from stackView
let r = rippedView.frame
// instantiate views
let insertionView = UIView(frame: scrollView.frame)
let imposeView = UIView(frame: stackView.frame)
// add imposeView to insertionView
insertionView.addSubview(imposeView)
// add insertionView to self.view
self.view.addSubview(insertionView)
// move rippedView from stackView to imposeView
imposeView.addSubview(rippedView)
// just to make it easy to see...
rippedView.backgroundColor = .green
// set to TRUE
rippedView.translatesAutoresizingMaskIntoConstraints = true
// set the frame
rippedView.frame = r
}
Here's a full class example that you can run directly (just assign it to a view controller):
class RipViewViewController: UIViewController {
let aButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
v.setTitle("Testing", for: .normal)
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .systemBlue
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.spacing = 8
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(aButton)
view.addSubview(scrollView)
scrollView.addSubview(stackView)
let g = view.safeAreaLayoutGuide
let sg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
aButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 16.0),
aButton.centerXAnchor.constraint(equalTo: g.centerXAnchor, constant: 0.0),
scrollView.topAnchor.constraint(equalTo: aButton.bottomAnchor, constant: 40.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: -40.0),
stackView.topAnchor.constraint(equalTo: sg.topAnchor, constant: 40.0),
stackView.leadingAnchor.constraint(equalTo: sg.leadingAnchor, constant: 20.0),
stackView.trailingAnchor.constraint(equalTo: sg.trailingAnchor, constant: 20.0),
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -40.0),
stackView.bottomAnchor.constraint(equalTo: sg.bottomAnchor, constant: 20.0),
])
for i in 1...5 {
let l = UILabel()
l.backgroundColor = .cyan
l.textAlignment = .center
l.text = "Label \(i)"
stackView.addArrangedSubview(l)
}
aButton.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
}
#objc func btnTapped(_ sender: Any?) -> Void {
// get a reference to the 3rd arranged subview in the stack view
let rippedView = stackView.arrangedSubviews[2]
// local var holding the rippedView frame (as set by the stackView)
// get it before moving view from stackView
let r = rippedView.frame
// instantiate views
let insertionView = UIView(frame: scrollView.frame)
let imposeView = UIView(frame: stackView.frame)
// add imposeView to insertionView
insertionView.addSubview(imposeView)
// add insertionView to self.view
self.view.addSubview(insertionView)
// move rippedView from stackView to imposeView
imposeView.addSubview(rippedView)
// just to make it easy to see...
rippedView.backgroundColor = .green
// set to TRUE
rippedView.translatesAutoresizingMaskIntoConstraints = true
// set the frame
rippedView.frame = r
}
}

Set button width to fit dynamic button title

I have my UI structured say Level 1(UP), Level 2(DOWN) with some controls
In level 1, I have a label L1
In level 2, I have a button and label L2
In level 2 my button may be removed in runtime and I wanted my label L2 to be aligned to leading edge as L1
I'm facing two problems here
When I set my button title programmatically, I want to set my button such that its width grows when text increases and reduces its width when there is less text content. This isn't happening. Please see below screens the constraints I've in place
When I removed my button from superview, I wanted my L2 label Leading to be aligned to L1 leading. So I created a constraint from L2.leading = L1.leading and prioirty is 999
In this case, the button gets reduces its size to almost 0 even if i have text in that. Please advice me setting this up
Problem #1:
use .horizontal UIStackview for the button and text. set its distribution to .fill. For the button set contentCompression resistance priority to .required for .horizontal & set contenHugging priority to .required for .horizontal. So the Button will always wrap the text no matter what.
Problem #2:
While placing inside a stackview, you don't have to remove the button from superview. Just hide it using isHidden.
Code Demonstration
class SampleVC: UIViewController {
private var didAddConstraint = false
// Basic Views
private let label: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
view.text = "Label"
return view
}()
private let topButton: UIButton = {
let view = UIButton()
view.translatesAutoresizingMaskIntoConstraints = false
view.setTitle("Button", for: .normal)
view.setTitleColor(.gray, for: .highlighted)
view.backgroundColor = .green
view.setContentHuggingPriority(.required, for: .horizontal)
view.setContentCompressionResistancePriority(.required, for: .horizontal)
return view
}()
private let rightLabel: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
view.numberOfLines = 0
view.text = "label"
view.backgroundColor = .red
return view
}()
private lazy var stackview: UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.axis = .horizontal
view.distribution = .fill
view.addArrangedSubview(topButton)
view.addArrangedSubview(rightLabel)
return view
}()
override func loadView() {
super.loadView()
view.addSubview(label)
view.addSubview(stackview)
view.setNeedsUpdateConstraints()
view.backgroundColor = .white
}
override func updateViewConstraints() {
super.updateViewConstraints()
if didAddConstraint == false {
didAddConstraint = true
// top label
label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16.0).isActive = true
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
label.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
// stackview
stackview.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16.0).isActive = true
stackview.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 8.0).isActive = true
stackview.rightAnchor.constraint(equalToSystemSpacingAfter: view.rightAnchor, multiplier: 16.0).isActive = true
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// TEST Code
// topButton.setTitle("TEST TEST TEST", for: .normal)
// topButton.isHidden = true
}
}

Arranging buttons programmatically with constraints

I have an array of buttons that I am iterating through and adding the buttons onto the view. Each button should be adjacent to the previous button, so I'm setting the leading constraint to the previous button's trailing. But the buttons end up layered on top of each other with only the top one displayed.
for k in 0 ..< buttons.count {
view.addSubview(buttons[k])
if k > 0 {
buttons[k].leadingAnchor.constraint(equalTo: buttons[k-1].trailingAnchor).isActive = true
}
}
Edit:
I don't know if this is part of the problem, but here's how I'm creating the buttons. I set each to (0,0) because I don't know where they'll end up. I assume the constraint would reposition them as needed (first time use programmatic constraints).
let size = CGRect(x: 0, y: 0, width: buttonWidth, height: buttonHeight)
let button: UIButton = UIButton(frame: size)
Here a simple playground that works with a UIStackView. You can play a bit and accommodate for your goal.
UIStackViews are very flexible components if you want avoid creating constraints manually.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController: UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let buttons = createButtons()
let stackView = createStackView(with: UILayoutConstraintAxis.vertical)
buttons.forEach { button in
stackView.addArrangedSubview(button)
}
view.addSubview(stackView)
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
self.view = view
}
func createStackView(with layout: UILayoutConstraintAxis) -> UIStackView {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = layout
stackView.distribution = .equalSpacing
stackView.spacing = 0
return stackView
}
func createButtons() -> [UIButton] {
var buttons = [UIButton]()
for x in 0..<5 {
let button = UIButton(type: .custom)
button.backgroundColor = .red
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 50).isActive = true
button.heightAnchor.constraint(equalToConstant: 100).isActive = true
button.setTitle("Title \(x)", for: .normal)
buttons.append(button)
}
return buttons
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
The key problem is you should use isActive to active constraint.
The following is example
var buttons: [UIButton] = []
for index in 0...5 {
let button = UIButton(frame: .zero)
button.setTitleColor(.black, for: .normal)
button.setTitle("button \(index)", for: .normal)
button.layer.borderColor = UIColor.gray.cgColor
button.layer.borderWidth = 1.0
button.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(button)
buttons.append(button)
}
for index in 0...5 {
let button = buttons[index]
if index == 0 {
button.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8.0).isActive = true
button.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 20.0).isActive = true
} else {
let preButton = buttons[index - 1]
button.leadingAnchor.constraint(equalTo: preButton.trailingAnchor, constant: 8.0).isActive = true
button.topAnchor.constraint(equalTo: preButton.topAnchor, constant: 0.0).isActive = true
}
}

Resources