View has ambigous layout - ios

Programmatically I'm trying to create a stack view with 5 buttons using auto layout. When i run the project then it run's without showing any error but it is not showing the button stack.
On the other-hand in Debug View Hierarchy, it is showing "View has ambigous layout." What I'm missing here.
class TestView: UIView {
var stackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
initComponents()
}
func initComponents() {
self.autoresizesSubviews = false
stackView.autoresizesSubviews = false
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .equalSpacing
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.contentMode = .scaleToFill
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
stackView.topAnchor.constraint(equalTo: self.topAnchor),
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
])
for i in 0...4 {
let button = UIButton()
button.titleLabel?.text = "\(i)"
button.translatesAutoresizingMaskIntoConstraints = false
stackView.addSubview(button)
button.heightAnchor.constraint(equalTo: stackView.heightAnchor, multiplier: 1).isActive = true
button.widthAnchor.constraint(equalTo: button.heightAnchor, multiplier: 1).isActive = true
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}}
In ViewController.swift file
let frame = CGRect(x: 0, y: 0, width: 200, height: 30)
let test = TestView.init(frame: frame)
self.view.addSubview(test)

I had to change a few things to make this work:
Use button.setTitle(_:for:) to set the title of the buttons.
Set a color for your text with button.setTitleColor(_:for:).
Set a background color for your buttons.
Add the buttons to the stackView with stackView.addArrangedSubview(button).
Also, you might want to move the frame down a bit. It's in the upper left corner which puts it over top of the status bar and behind the notch of the iPhone X.
for i in 0...4 {
let button = UIButton()
button.setTitle("\(i)", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
// Set these colors to whatever you like
button.setTitleColor(.black, for: .normal)
button.backgroundColor = .red
stackView.addArrangedSubview(button)
button.heightAnchor.constraint(equalTo: stackView.heightAnchor, multiplier: 1).isActive = true
button.widthAnchor.constraint(equalTo: button.heightAnchor, multiplier: 1).isActive = true
}

Related

UIStackView setting layoutMargins in code breaks alignment

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:

Setting background color for customView has no effect

I have a customView which I display by after a button-tap. My problem is that setting .backgroundColor has no effect and it is just a clear background.
CustomView:
let wishWishView: WishStackView = {
let v = WishStackView()
v.backgroundColor = .cyan
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
AddViewButton:
#objc func addWishButtonTapped(){
self.view.addSubview(self.wishWishView)
wishWishView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
wishWishView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
}
WishWishView is just a simple UIView with a StackView inside of it.
My guess would be that it is a UIStackView, which does not support having a background color.
You can read more about why this happens, and possible workarounds here: https://stackoverflow.com/a/34868367/3992237
Your code is Totally wrong, first you set your view like this:
let wishWishView: UIView = {
let v = UIView()
v.backgroundColor = .cyan
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
now set your stackView, in my example I put in 2 label in vertical axis:
let stackView: UIStackView = {
let label1 = UILabel()
label1.text = "Label 1"
label1.textColor = .red
label1.font = UIFont.systemFont(ofSize: 16)
let label2 = UILabel()
label2.text = "Label 2"
label2.textColor = .black
label2.font = UIFont.systemFont(ofSize: 16)
let sV = UIStackView(arrangedSubviews: [label1, label2])
sV.axis = .vertical
sV.distribution = .fillEqually
sV.translatesAutoresizingMaskIntoConstraints = false
return sV
}()
after that set your button:
let buttonAddView: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Add View", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .red
button.addTarget(self, action: #selector(addWishButtonTapped), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
in viewDidLoad add button and set constraints
view.addSubview(buttonAddView)
buttonAddView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
buttonAddView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20).isActive = true
buttonAddView.heightAnchor.constraint(equalToConstant: 50).isActive = true
buttonAddView.widthAnchor.constraint(equalToConstant: 120).isActive = true
now write the function that add the view with stack view inside when the button is tapped with right constraints, in your function you call only the position of the view but not the size of it. Write your function like this:
#objc func addWishButtonTapped(){
view.addSubview(self.wishWishView)
wishWishView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
wishWishView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
wishWishView.heightAnchor.constraint(equalToConstant: 100).isActive = true
wishWishView.widthAnchor.constraint(equalToConstant: 200).isActive = true
wishWishView.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: wishWishView.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: wishWishView.bottomAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: wishWishView.leadingAnchor, constant: 10).isActive = true
stackView.trailingAnchor.constraint(equalTo: wishWishView.trailingAnchor, constant: -10).isActive = true
}
Now simply change background of the wishWishView to automatically set stackView background, and that's it...

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

Constraints failing to add padding

I'm working on a custom control that will eventually be a button. I've successfully built out the design in the interface builder, but when I'm trying to programmatically build the same view, it's not working as expected. I'm attempting to force some padding by setting constants on the constraints that join the labels to their superviews, however, that's not working as expected.
The top button is what I'm expecting (and what I've created in interface builder, do ignore the font differences). The bottom is what I'm getting. There are no errors/warnings being thrown but it's not resizing the superview.
class RoundedButton3: UIView {
var stringLabel: UILabel
var numberLabel: UILabel
var leftBackground: UIView
var rightBackground: UIView
var stackView: UIStackView
override init(frame: CGRect) {
stringLabel = UILabel()
numberLabel = UILabel()
leftBackground = UIView()
rightBackground = UIView()
stackView = UIStackView()
super.init(frame: frame)
self.contentMode = .scaleToFill
self.autoresizingMask = [.flexibleBottomMargin, .flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin]
self.translatesAutoresizingMaskIntoConstraints = false
stringLabel.translatesAutoresizingMaskIntoConstraints = false
numberLabel.translatesAutoresizingMaskIntoConstraints = false
leftBackground.translatesAutoresizingMaskIntoConstraints = false
rightBackground.translatesAutoresizingMaskIntoConstraints = false
stackView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(stackView)
leftBackground.addSubview(stringLabel)
rightBackground.addSubview(numberLabel)
stackView.addArrangedSubview(leftBackground)
stackView.addArrangedSubview(rightBackground)
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.distribution = .fill
stringLabel.setContentHuggingPriority(UILayoutPriorityRequired, for: .horizontal)
stringLabel.setContentHuggingPriority(UILayoutPriorityRequired, for: .vertical)
stringLabel.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .horizontal)
stringLabel.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .vertical)
numberLabel.setContentHuggingPriority(UILayoutPriorityRequired, for: .horizontal)
numberLabel.setContentHuggingPriority(UILayoutPriorityRequired, for: .vertical)
numberLabel.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .horizontal)
numberLabel.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .vertical)
self.topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
self.rightAnchor.constraint(equalTo: stackView.rightAnchor).isActive = true
self.leftAnchor.constraint(equalTo: stackView.leftAnchor).isActive = true
leftBackground.leftAnchor.constraint(equalTo: stringLabel.leftAnchor, constant: 4).isActive = true
leftBackground.rightAnchor.constraint(equalTo: stringLabel.rightAnchor, constant: 4).isActive = true
leftBackground.topAnchor.constraint(equalTo: stringLabel.topAnchor, constant: 4).isActive = true
leftBackground.bottomAnchor.constraint(equalTo: stringLabel.bottomAnchor, constant: 4).isActive = true
rightBackground.leftAnchor.constraint(equalTo: numberLabel.leftAnchor, constant: 4).isActive = true
rightBackground.rightAnchor.constraint(equalTo: numberLabel.rightAnchor, constant: 4).isActive = true
rightBackground.topAnchor.constraint(equalTo: numberLabel.topAnchor, constant: 4).isActive = true
rightBackground.bottomAnchor.constraint(equalTo: numberLabel.bottomAnchor, constant: 4).isActive = true
stringLabel.text = "RESERVE"
numberLabel.text = "10"
stringLabel.textColor = AppColors.WSF_GREEN
numberLabel.textColor = UIColor.white
rightBackground.backgroundColor = AppColors.WSF_GREEN
leftBackground.backgroundColor = UIColor.white
self.layer.borderColor = AppColors.WSF_GREEN.cgColor
self.layer.borderWidth = 3
self.layer.cornerRadius = 4
self.layer.masksToBounds = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
To define padding you could use the layoutMargins property of the containing view. You can refer to this answer: Adding padding to an UIView.

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