UIStackView setting layoutMargins in code breaks alignment - ios

I'm trying to add layoutMargins to some elements in a UIStackView I'm creating in code.
I've tried the same thing in IB and it works perfectly, but when I add layoutMargins and I set isLayoutMarginsRelativeArrangement = true then my stack view's alignment breaks.
The code of my stack view is the following:
#objc lazy var buttonsStackView: UIStackView = {
let stack = UIStackView(arrangedSubviews: [doneButton, separatorView, giftButton])
stack.layoutMargins = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4)
stack.isLayoutMarginsRelativeArrangement = true
stack.axis = .horizontal
stack.frame = CGRect(x: 0, y: 0, width: 150, height: 44)
stack.spacing = 4
stack.distribution = .equalCentering
stack.alignment = .center
let bgView = UIView(frame: stack.bounds)
stackViewBackground = bgView
bgView.backgroundColor = ColorManager.shared.grayColor
bgView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
stack.insertSubview(bgView, at: 0)
separatorView.heightAnchor.constraint(equalTo: stack.heightAnchor, multiplier: 0.8).isActive = true
return stack
}()
SeparatorView only has a 1pt width constraint, while the two buttons are left unconstrained to keep theirintrinsicContentSize.
Here's how my stackView looks when isLayoutMarginsRelativeArrangement is false:
But obviously, the left and right margins are needed, so when setting isLayoutMarginsRelativeArrangement to true, my stackview's alignment breaks:
Unfortunately, I cannot use IB for this particular view and I need to initialise it from code. Any idea on how to fix this is greatly appreciated. Thank you!

Here is an example of making that a custom view, with the separator centered horizontally, and the buttons centered in each "side":
protocol DoneGiftDelegate: class {
func doneButtonTapped()
func giftButtonTapped()
}
class DoneGiftView: UIView {
weak var delegate: DoneGiftDelegate?
let doneButton: UIButton = {
let v = UIButton(type: .system)
v.setTitle("Done", for: [])
v.tintColor = .white
return v
}()
let giftButton: UIButton = {
let v = UIButton(type: .system)
v.setImage(UIImage(systemName: "gift.fill"), for: [])
v.tintColor = .white
return v
}()
let separatorView: UIView = {
let v = UIView()
v.backgroundColor = .white
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .lightGray // ColorManager.shared.grayColor
[doneButton, separatorView, giftButton].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
// width and height constraints: 150 x 44
// set Priority to 999 so it can be overriden by controller if desired
let widthConstraint = widthAnchor.constraint(equalToConstant: 150.0)
widthConstraint.priority = UILayoutPriority(rawValue: 999)
let heightConstraint = heightAnchor.constraint(equalToConstant: 44.0)
heightConstraint.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
// doneButton Leading to Leading
doneButton.leadingAnchor.constraint(equalTo: leadingAnchor),
// separator Leading to doneButton Trailing
separatorView.leadingAnchor.constraint(equalTo: doneButton.trailingAnchor),
// giftButton Leading to separator Trailing
giftButton.leadingAnchor.constraint(equalTo: separatorView.trailingAnchor),
// giftButton Trailing to Trailing
giftButton.trailingAnchor.constraint(equalTo: trailingAnchor),
// all centered vertically
doneButton.centerYAnchor.constraint(equalTo: centerYAnchor),
separatorView.centerYAnchor.constraint(equalTo: centerYAnchor),
giftButton.centerYAnchor.constraint(equalTo: centerYAnchor),
// doneButton Height = Height
doneButton.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0),
// separator Height = 80% of Height
separatorView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.8),
// giftButton Height = Height
giftButton.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1.0),
// separator Width = 1
separatorView.widthAnchor.constraint(equalToConstant: 1.0),
// doneButton Width = giftButton Width
doneButton.widthAnchor.constraint(equalTo: giftButton.widthAnchor, multiplier: 1.0),
// self Width and Height
widthConstraint,
heightConstraint,
])
// add target actions for buttons
doneButton.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
giftButton.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#objc func buttonTapped(_ sender: UIButton) -> Void {
if sender == doneButton {
delegate?.doneButtonTapped()
} else {
delegate?.giftButtonTapped()
}
}
}
class DemoViewController: UIViewController, DoneGiftDelegate {
let doneGiftView: DoneGiftView = DoneGiftView()
let testView: UIView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
doneGiftView.translatesAutoresizingMaskIntoConstraints = false
testView.translatesAutoresizingMaskIntoConstraints = false
testView.addSubview(doneGiftView)
view.addSubview(testView)
// so we can see the view frame
testView.backgroundColor = .cyan
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// testView Top: 100
// Leading / Trailing with 20-pts "padding"
// Height: 80
testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
testView.heightAnchor.constraint(equalToConstant: 80.0),
// doneGiftView Trailing to testView Trailing
doneGiftView.trailingAnchor.constraint(equalTo: testView.trailingAnchor),
// doneGiftView centered vertically in testView
doneGiftView.centerYAnchor.constraint(equalTo: testView.centerYAnchor),
])
doneGiftView.delegate = self
}
func doneButtonTapped() {
print("Done button tapped!")
// do what we want when Done button tapped
}
func giftButtonTapped() {
print("Gift button tapped")
// do what we want when Gift button tapped
}
}
Example result:

Related

Change the size of my component dynamically based on the amount of text in my label

I have a simple component, a label and an imageview.
They are put in horizontal position. I need to center that and if my label gets more text, the component will change too and the position of my image will change. I have to do this programmatically and I’m really new to programmatically constraints.
For more explanation this is an example:
And this is my code right now:
class PVProgressIndicator: UIView {
let progressText: UILabel = {
let progressText = UILabel()
progressText.font = UIFont.systemFont(ofSize: 11)
progressText.text = ""
progressText.numberOfLines = 0
progressText.textAlignment = .right
progressText.translatesAutoresizingMaskIntoConstraints = false
progressText.sizeToFit()
progressText.layoutIfNeeded()
return progressText
}()
let iconView: UIImageView = {
let iconView = UIImageView()
iconView.translatesAutoresizingMaskIntoConstraints = false
let image = UIImage(named: "feedback_success")!
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
let resizedImage = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
}
iconView.image = resizedImage
iconView.sizeToFit()
iconView.layoutIfNeeded()
return iconView
}()
let iconView2: UIImageView = {
let iconView2 = UIImageView()
iconView2.translatesAutoresizingMaskIntoConstraints = false
let image = UIImage(named: "feedback_error")!
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
let resizedImage = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
}
iconView2.image = resizedImage
iconView2.sizeToFit()
iconView2.layoutIfNeeded()
return iconView2
}()
private func setupView() {
let overlayView = UIView()
overlayView.translatesAutoresizingMaskIntoConstraints = false
addSubview(overlayView)
overlayView.addSubview(iconView)
overlayView.addSubview(iconView2)
NSLayoutConstraint.activate([
iconView.centerXAnchor.constraint(equalTo: iconView2.centerXAnchor),
iconView.centerYAnchor.constraint(equalTo: iconView.centerYAnchor),
progressIndicator.centerXAnchor.constraint(equalTo: iconView2.centerXAnchor),
progressIndicator.centerYAnchor.constraint(equalTo: iconView2.centerYAnchor),
iconView2.topAnchor.constraint(equalTo: self.topAnchor, constant: 20),
iconView2.widthAnchor.constraint(equalToConstant: 10),
iconView2.heightAnchor.constraint(equalToConstant: 10),
iconView2.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: -10)
])
addSubview(progressText)
NSLayoutConstraint.activate([
progressText.leadingAnchor.constraint(equalTo: overlayView.trailingAnchor, constant: 15),
progressText.centerYAnchor.constraint(equalTo: overlayView.centerYAnchor)
])
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Also, this is how I set this in a parent view:
private func setupProgressBar() {
self.codeValidationTextField.addSubview(progressBar)
progressBar.translatesAutoresizingMaskIntoConstraints = false
progressBar.leadingAnchor.constraint(equalTo: codeValidationTextField.leadingAnchor, constant: 10).isActive = true
progressBar.bottomAnchor.constraint(equalTo: codeValidationTextField.bottomAnchor, constant: -33).isActive = true
progressBar.widthAnchor.constraint(equalTo: codeValidationTextField.widthAnchor, multiplier: 0.5)
progressBar.progressIndicator.isHidden = false
progressBar.iconView.isHidden = true
progressBar.progressText.isHidden = false
}
It's not entirely clear what you are trying to do...
your code references a progressIndicator when setting constraints, but there is no progressIndicator identified.
there is no reason to be "scaling" your images... the image view will do that for you.
you're adding 2 "icon" image views, but the layout doesn't make much sense.
there is no need to use a overlayView -- we can add the subviews to the custom view itself (and, the code you've shown doesn't setup that overlayView correctly, so no idea if it is supposed to be doing anything).
Note that it helps to include comments in your code, telling yourself what you expect to happen.
And, a tip: during development, give your UI elements contrasting background colors to make it easy to see the framing.
So, let's simplify this and start with a single image view and label:
class PVProgressIndicator: UIView {
let progressText: UILabel = {
let progressText = UILabel()
progressText.font = UIFont.systemFont(ofSize: 11)
progressText.text = ""
progressText.numberOfLines = 0
progressText.textAlignment = .right
progressText.translatesAutoresizingMaskIntoConstraints = false
// no need for any of this
//progressText.sizeToFit()
//progressText.layoutIfNeeded()
return progressText
}()
let iconView: UIImageView = {
let iconView = UIImageView()
iconView.translatesAutoresizingMaskIntoConstraints = false
if let image = UIImage(named: "feedback_success") {
iconView.image = image
} else {
// could not load image, so set background color
iconView.backgroundColor = .systemRed
}
// no need for any of this
//let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
//let resizedImage = renderer.image { _ in
// image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
//}
//iconView.image = resizedImage
//iconView.sizeToFit()
//iconView.layoutIfNeeded()
return iconView
}()
private func setupView() {
// no need for this... add subviews to self
//let overlayView = UIView()
//overlayView.translatesAutoresizingMaskIntoConstraints = false
//addSubview(overlayView)
self.addSubview(iconView)
self.addSubview(progressText)
NSLayoutConstraint.activate([
// 20-points on top and bottom of iconView
iconView.topAnchor.constraint(equalTo: self.topAnchor, constant: 20.0),
iconView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20.0),
// align iconView to leading-edge of self
iconView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
// iconView 16 x 16
iconView.widthAnchor.constraint(equalToConstant: 16),
iconView.heightAnchor.constraint(equalToConstant: 16),
// progressText centered vertically
progressText.centerYAnchor.constraint(equalTo: self.centerYAnchor),
// progressText 15-points from iconView trailing edge
progressText.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 15.0),
// align progressText to trailing-edge of self
progressText.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0.0),
])
// during development, use some background colors so we can see the framing
progressText.backgroundColor = .cyan
self.backgroundColor = .systemGreen
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupView()
}
}
And an example view controller showing it. Each tap anywhere in the view will cycle through several "example" progress strings:
class PVTestViewController: UIViewController {
let progressBar = PVProgressIndicator()
let sampleStrings: [String] = [
"Short",
"Medium Length",
"A Longer Sample String",
"This one should be long enough that it will word wrap onto more than one line.",
"Let's make this sample string\nuse\n4 lines\nof text.",
"If the progress text might be very, very long... so long that it could wrap onto more than four lines (like this), then some additional constraints will need to be set on the elements in the progressBar view.",
]
var sampleIDX: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
progressBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(progressBar)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain progressBar Top 20-points from top of safe area
progressBar.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// centered horizontally
progressBar.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// let's set the max width to 75% of safe area width
progressBar.widthAnchor.constraint(lessThanOrEqualTo: g.widthAnchor, multiplier: 0.75),
// progressBar sets it's own height
])
updateProgress()
// instruction label
let v = UILabel()
v.text = "Tap anywhere to cycle through the sample progress strings."
v.numberOfLines = 0
v.textAlignment = .center
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
func updateProgress() {
progressBar.progressText.text = sampleStrings[sampleIDX % sampleStrings.count]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
sampleIDX += 1
updateProgress()
}
}
When you start, it will look like this:
Each tap cycles to the next sample string...

Multi-line UILabel in TableView header [duplicate]

I am currently using a UIViewController and adding a UITableView to the view. With this tableView I am adding a UIView called containerView to its tableHeaderView. I set the height of the container view and then adding a second UIView to its subview, that is pinned to the bottom of the containerView.
When I add it to the header view the cells are being overlapped. What's odd though is that if I don't add the subview to the container view the headerView is not being overlapped by the cells, it is only occurring when I am adding the second view as a subview to the container view.
class ViewController: UIViewController {
private var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.alpha = 0.7
view.backgroundColor = .red
return view
}()
private var bottomView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .blue
return view
}()
private(set) lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
containerView.addSubview(bottomView)
tableView.tableHeaderView = containerView
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
containerView.topAnchor.constraint(equalTo: tableView.topAnchor),
containerView.heightAnchor.constraint(equalToConstant: 214),
containerView.widthAnchor.constraint(equalToConstant: view.frame.size.width),
bottomView.topAnchor.constraint(equalTo: containerView.bottomAnchor),
bottomView.heightAnchor.constraint(equalToConstant: 114),
bottomView.widthAnchor.constraint(equalToConstant: view.frame.size.width),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.contentInset = UIEdgeInsets(top: -view.safeAreaInsets.top, left: 0, bottom: 0, right: 0)
tableView.tableHeaderView?.autoresizingMask = []
tableView.tableHeaderView?.layoutIfNeeded()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
}
}
The reason your "blue view" is overlapping the cells is because you are constraining its Top to the red view's Bottom, but you're not updating the header view size.
One good approach is to create a UIView subclass to use as your header view. Setup all of its content with proper auto-layout constraints.
Then, in the controller's viewDidLayoutSubviews(), we use .systemLayoutSizeFitting(...) to determine the header view's height and update its frame:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// update table header size
guard let headerView = tableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
var frame = headerView.frame
// avoids infinite loop!
if height != frame.height {
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
Here is a complete example...
First, our custom view class:
class SampleHeaderView: UIView {
let redView: UIView = {
let v = UIView()
v.backgroundColor = .systemRed
return v
}()
let blueView: UIView = {
let v = UIView()
v.backgroundColor = .systemBlue
return v
}()
let redTopLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .yellow
v.numberOfLines = 0
return v
}()
let redBottomLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .green
v.numberOfLines = 0
return v
}()
let multiLineLabel: UILabel = {
let v = UILabel()
v.backgroundColor = .cyan
v.numberOfLines = 0
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// all views will use auto-layout
[redView, blueView, redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// prevent label vertical compression
[redTopLabel, redBottomLabel, multiLineLabel].forEach { v in
v.setContentCompressionResistancePriority(.required, for: .vertical)
}
// add top and bottom labels to red view
redView.addSubview(redTopLabel)
redView.addSubview(redBottomLabel)
// add multi-line label to blue view
blueView.addSubview(multiLineLabel)
// add red and blue views to self
addSubview(redView)
addSubview(blueView)
// the following constraints need to have less-than required to avoid
// auto-layout warnings
// blue view bottom to self
let c1 = blueView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
// labels trailing contraints
let c2 = redTopLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
let c3 = redBottomLabel.trailingAnchor.constraint(equalTo: redView.trailingAnchor, constant: -8.0)
let c4 = multiLineLabel.trailingAnchor.constraint(equalTo: blueView.trailingAnchor, constant: -8.0)
[c1, c2, c3, c4].forEach { c in
c.priority = .required - 1
}
NSLayoutConstraint.activate([
// red view top to self
redView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
// leading / trailing to self
redView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
redView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// blue view top to red view bottom
blueView.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 0.0),
// leading / trailing to self
blueView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
blueView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
// top and bottom labels, constrained in red view
// with a little "padding"
redTopLabel.topAnchor.constraint(equalTo: redView.topAnchor, constant: 8.0),
redTopLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),
redBottomLabel.topAnchor.constraint(equalTo: redTopLabel.bottomAnchor, constant: 8.0),
redBottomLabel.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 8.0),
redBottomLabel.bottomAnchor.constraint(equalTo: redView.bottomAnchor, constant: -8.0),
// multi-line label, constrained in blue view
// with a little "padding"
multiLineLabel.topAnchor.constraint(equalTo: blueView.topAnchor, constant: 8.0),
multiLineLabel.leadingAnchor.constraint(equalTo: blueView.leadingAnchor, constant: 8.0),
multiLineLabel.bottomAnchor.constraint(equalTo: blueView.bottomAnchor, constant: -8.0),
// the less-than-required priority constraints
c1, c2, c3, c4,
])
}
}
and a sample controller:
class TableHeaderViewController: UIViewController {
var sampleHeaderView = SampleHeaderView()
private(set) lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
sampleHeaderView.redTopLabel.text = "The Red Top Label"
sampleHeaderView.redBottomLabel.text = "The Red Bottom Label, with enough text that is should wrap."
sampleHeaderView.multiLineLabel.text = "This text is for the Label in the Blue View. It is also long enough that it will require word-wrapping. Note that the header updates itself when the frame changes, such as on device rotation."
tableView.tableHeaderView = sampleHeaderView
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// update table header size
guard let headerView = tableView.tableHeaderView else { return }
let height = headerView.systemLayoutSizeFitting(CGSize(width: tableView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
var frame = headerView.frame
// avoids infinite loop!
if height != frame.height {
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
}
}
extension TableHeaderViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
c.textLabel?.text = "\(indexPath)"
return c
}
}
Output:
and rotated:

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()
})
}
}

Cannot move UILabel position programatically

I have a file for the View section of app, where i have all the labels and images that i intent to use, this is what i have in my DetailViewTableCell class, which inherits from UIView.
class DetailViewTableCell: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
}
Now i move to my DetailViewController class, here i try and add the label, the label is added but it appears always at top left corner at 0,0 coordinate, when i try and add constraints for position, i always get error, now i can try
detailMain.detailName.frame.origin.x = 30
but i get error:
Constraint items must each be a view or layout guide.
In any case i do not wish to use this approach but more something like this
NSLayoutConstraint(item: detailMain.detailName, attribute: .leading, relatedBy: .equal, toItem: detailMain.detailName.superview, attribute: .leading, multiplier: 1, constant: 20).isActive = true
but i get the same above error, my over all code is this:
self.view.addSubview(detailMain.detailName)
detailMain.detailName.translatesAutoresizingMaskIntoConstraints = false
detailMain.detailName.heightAnchor.constraint(equalToConstant: 25).isActive = true
detailMain.detailName.widthAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true
detailMain.detailName.font = UIFont(name: "Rubik-Medium", size: 30)
detailMain.detailName.backgroundColor = UIColor.white
detailMain.detailName.textColor = UIColor.black
Which works perfectly fine but the moment i try and constraints, the error come up, this is how the app shows up with out constraints and name at top most left corner
////////UPDATE
So here is my new DetailViewTableCell,
import UIKit
class DetailViewTableCell: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
[detailMainImage, detailName, detailType, detailHeart].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
NSLayoutConstraint.activate([
// constrain main image to all 4 sides
detailMainImage.topAnchor.constraint(equalTo: topAnchor),
detailMainImage.leadingAnchor.constraint(equalTo: leadingAnchor),
detailMainImage.trailingAnchor.constraint(equalTo: trailingAnchor),
detailMainImage.bottomAnchor.constraint(equalTo: bottomAnchor),
// activate the height contraint
// constrain detailType label
// 30-pts from Leading
// 12-pts from Top
detailType.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailType.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
// constrain detailName label
// 30-pts from Leading
// 12-pts from Bottom
detailName.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
// constrain detailHeart image
// 12-pts from Trailing
// 12-pts from Bottom
// width: 24 height: equal to width (1:1 square)
detailHeart.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
detailHeart.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
detailHeart.widthAnchor.constraint(equalToConstant: 24),
detailHeart.heightAnchor.constraint(equalTo: detailHeart.widthAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and this 2 lines is what i add to my Detail view controller in viewDidLoad
let v = DetailViewTableCell()
detailTableView.tableHeaderView = v
Also i add this function
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = detailTableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
detailTableView.tableHeaderView = headerView
detailTableView.layoutIfNeeded()
}
}
then in my viewForHeaderInSection inbuilt function i add this
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
tableView.rowHeight = 80
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
headerView.heightAnchor.constraint(equalToConstant: 400).isActive = true
detailMain.detailMainImage.translatesAutoresizingMaskIntoConstraints = false
detailMain.detailMainImage.heightAnchor.constraint(equalToConstant: 400).isActive = true
detailMain.detailMainImage.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
detailMain.detailMainImage.image = UIImage(named: restaurant.image)
detailMain.detailMainImage.contentMode = .scaleAspectFill
detailMain.detailMainImage.clipsToBounds = true
headerView.addSubview(detailMain.detailMainImage)
//Add the name
detailMain.detailName.text = restaurant.name
headerView.addSubview(detailMain.detailName)
return headerView
}
but still same position , is there any thing i add to add or remove from my code
You didn't show where you *want the labels, but this should get you going...
In your "header view" class:
add your elements: detailMainImage, detailName, etc...
set their properties and constraints as desired
You can get auto-layout to use the constraints you've setup in the header view to automatically determine its height:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = tableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
}
So, here's a complete example:
class TestHeaderTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// instantiate the header view
let v = DetailTableHeaderView()
// set it as the tableHeaderView
tableView.tableHeaderView = v
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = tableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
}
}
class DetailTableHeaderView: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .white
// give the heart image view a background color so we can see its frame
detailHeart.backgroundColor = .red
if let img = UIImage(named: "teacup") {
detailMainImage.image = img
}
// give labels some text so we can see them
detailType.text = "Detail Type"
detailName.text = "Detail Name"
// setup fonts for labels as desired
//detailName.font = UIFont(name: "Rubik-Medium", size: 30)
// I don't have "Rubik" so this is with the system font
detailName.font = UIFont.systemFont(ofSize: 30, weight: .bold)
detailName.backgroundColor = UIColor.white
detailName.textColor = UIColor.black
detailType.textColor = .white
[detailMainImage, detailName, detailType, detailHeart].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
// give main image a height
// set its Priority to 999 to prevent layout constraint conflict warnings
let mainImageHeightAnchor = detailMainImage.heightAnchor.constraint(equalToConstant: 300.0)
mainImageHeightAnchor.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
// constrain main image to all 4 sides
detailMainImage.topAnchor.constraint(equalTo: topAnchor),
detailMainImage.leadingAnchor.constraint(equalTo: leadingAnchor),
detailMainImage.trailingAnchor.constraint(equalTo: trailingAnchor),
detailMainImage.bottomAnchor.constraint(equalTo: bottomAnchor),
// activate the height contraint
mainImageHeightAnchor,
// constrain detailType label
// 30-pts from Leading
// 12-pts from Top
detailType.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailType.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
// constrain detailName label
// 30-pts from Leading
// 12-pts from Bottom
detailName.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
// constrain detailHeart image
// 12-pts from Trailing
// 12-pts from Bottom
// width: 24 height: equal to width (1:1 square)
detailHeart.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
detailHeart.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
detailHeart.widthAnchor.constraint(equalToConstant: 24),
detailHeart.heightAnchor.constraint(equalTo: detailHeart.widthAnchor),
])
}
}
For this example, I just set the background color of the "heart" image view to red, and I clipped the teacup out of your image:
And this is the result:
Edit - to use the custom view as a Section header view...
Use the same DetailTableHeaderView class from above, but change the table view controller as follows:
class TestSectionHeaderTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.sectionHeaderHeight = UITableView.automaticDimension
tableView.estimatedSectionHeaderHeight = 300
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let v = DetailTableHeaderView()
// for example implementation...
if let img = UIImage(named: "teacup") {
v.detailMainImage.image = img
}
v.detailName.text = "Testing the Name"
// for your implementation...
//if let img = UIImage(named: restaurant.image) {
// v.detailMainImage.image = img
//}
//v.detailName.text = restaurant.name
return v
}
return nil;
}
}

expand UIView height bottom to left and vice versa

I have a view that has a button on top and a textview on bottom as subviews, Im trying to have the button either expand and show the textview or collapse and hide it (like a show/hide)
I used the solution here and it kind of helped, except the subview (text view) was still showing and overlapping with other views so it only hid the main UIView.
here I initialized the height constraint to 25 to leave space for the button:
heightConstraint = detailView.heightAnchor.constraint(equalToConstant: 25)
the action function that'll expand/collapse the view based on the height constraint
#objc func expandViewPressed(sender: UIButton) {
if isAnimating { return }
let shouldCollapse = detailView.frame.height > 25
animateView(isCollapsed: shouldCollapse)
}
the animation function
private func animateView(isCollapsed: Bool) {
heightConstraint[enter image description here][1].isActive = isCollapsed
isAnimating = true
UIView.animate(withDuration: 1, animations: {
self.detailText.isHidden = isCollapsed
self.view.layoutIfNeeded()
}) { (_) in
self.isAnimating = false
}
}
—
expanded view
Sherbini .. please make sure you have added proper constraints to your subviews .. make sure not to add height constraint on any of your subview ..
also make sure you have added view.clipsToBounds == true
Hope it will work for you ..
There are various ways to approach this.
One method is to use two "bottom" constraints:
one from the bottom of the button to the bottom of detailView
one from the bottom of detailText to the bottom of detailView
Then set the constraint Priority of each based on whether the view should be "collapsed" or "expanded."
Here is a full implementation for you to try:
class ExpandViewController: UIViewController {
let myButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Collapse", for: [])
v.backgroundColor = .red
return v
}()
let detailText: UITextView = {
let v = UITextView()
v.translatesAutoresizingMaskIntoConstraints = false
v.text = "This is text in the text view."
return v
}()
let detailView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
v.clipsToBounds = true
return v
}()
var isAnimating: Bool = false
var collapsedConstraint: NSLayoutConstraint!
var expandedConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0)
detailView.addSubview(myButton)
detailView.addSubview(detailText)
view.addSubview(detailView)
let g = view.safeAreaLayoutGuide
// when collapsed, we want button bottom to constrain detailView bottom
collapsedConstraint = myButton.bottomAnchor.constraint(equalTo: detailView.bottomAnchor, constant: -12.0)
// when expanded, we want textView bottom to constrain detailView bottom
expandedConstraint = detailText.bottomAnchor.constraint(equalTo: detailView.bottomAnchor, constant: -12.0)
// we'll start in Expanded state
expandedConstraint.priority = .defaultHigh
collapsedConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
// constrain detailView Top / Leading / Trailing
detailView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
detailView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
detailView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// no Height or Bottom constraint for detailView
// constrain button Top / Center / Width
myButton.topAnchor.constraint(equalTo: detailView.topAnchor, constant: 12.0),
myButton.centerXAnchor.constraint(equalTo: detailView.centerXAnchor),
myButton.widthAnchor.constraint(equalToConstant: 200.0),
// constrain detailText Top / Leading / Trailing
detailText.topAnchor.constraint(equalTo: myButton.bottomAnchor, constant: 12.0),
detailText.leadingAnchor.constraint(equalTo: detailView.leadingAnchor, constant: 12.0),
detailText.trailingAnchor.constraint(equalTo: detailView.trailingAnchor, constant: -12.0),
// constrain detailText's Height
detailText.heightAnchor.constraint(equalToConstant: 200.0),
expandedConstraint,
collapsedConstraint,
])
myButton.addTarget(self, action: #selector(self.expandViewPressed(sender:)), for: .touchUpInside)
}
#objc func expandViewPressed(sender: UIButton) {
if isAnimating { return }
animateView()
}
private func animateView() {
isAnimating = true
// if it's expanded
if expandedConstraint.priority == .defaultHigh {
expandedConstraint.priority = .defaultLow
collapsedConstraint.priority = .defaultHigh
} else {
collapsedConstraint.priority = .defaultLow
expandedConstraint.priority = .defaultHigh
detailText.isHidden = false
}
UIView.animate(withDuration: 1, animations: {
self.view.layoutIfNeeded()
}) { (_) in
self.detailText.isHidden = self.expandedConstraint.priority == .defaultLow
self.isAnimating = false
self.myButton.setTitle(self.detailText.isHidden ? "Expand" : "Collapse", for: [])
}
}
}

Resources