UILabel doesn't always show up in its superview - ios

I'm trying to create a stack view with nested stack views of views to create a grid. Every cell in a grid should have a label. The problem I have is that label is showing only in the very first view as in this screenshot:
Code of my main stack view is straightforward:
for _ in 1...5 {
let view = WeekView()
addArrangedSubview(view)
}
And this is the code of a nested stack view.
// Function called in init(frame:)
func configureView() {
spacing = 8
for _ in 1...7 {
let view = UIView()
let label = UILabel()
view.addSubview(label)
addArrangedSubview(view)
label.text = "hi"
label.textAlignment = .center
view.widthAnchor.constraint(equalToConstant: 40).isActive = true
view.translatesAutoresizingMaskIntoConstraints = false
label.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
label.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
label.translatesAutoresizingMaskIntoConstraints = false
}
}
Any suggestions will be much appriciated.

You are not setting the position for the label. You could add the horizontal and vertical constraints, but you can also simply do:
label.translatesAutoresizingMaskIntoConstraints = true
label.frame = view.bounds
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
instead of
label.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
label.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
label.translatesAutoresizingMaskIntoConstraints = false

The problem might be that you haven't set
label.translatesAutoresizingMaskIntoConstraints = false
Please try to set that directly after you create the UI element:
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
...
view.widthAnchor.constraint(equalToConstant: 40).isActive = true
view.heightAnchor.constraint(equalToConstant: 40).isActive = true
Yet, in your case I would highly recommend using a UICollectionView. You get a lot of things for free if you use it (including smooth horizontal scrolling).
The image looks like you are building some kind of calendar app. If you use a UICollectionView you also get a very flexible API for spacing and overall layout.

Related

Center stack view elements and not fill them

I am using a UIStackView as UITableView's BackGroundView property so when there was an error getting the collection that populates the tableView I can call a function that displays this stack view containing views that show a warning message and a retry button.
I tested doing a similar behaviour in an empty UIViewController so I could center the stackView and its children. The solution worked when I pinned the stack view to the superView's trailing and leading, centered it vertically and set it's top anchor to be greater or equal to the superView's top anchor and similarly it's bottom anchor is greater or equal to the superView's bottom anchor. I have also set the alignment to center and distribution to fill and all seemed to work properly.
Here are some screenshots:
I used this code in a UITableView's extension, but could only reproduce this behaviour. Are there any errors on this code?
func show(error: Bool, withMessage message : String? = nil, andRetryAction retry: (() -> Void)? = nil){
if error{
let iconLabel = UILabel()
iconLabel.GMDIcon = .gmdErrorOutline
iconLabel.textAlignment = .center
iconLabel.numberOfLines = 0
iconLabel.font = iconLabel.font.withSize(50)
iconLabel.textColor = Constants.Colors.ErrorColor
iconLabel.backgroundColor = .blue
let messageLabel = UILabel()
messageLabel.text = message ?? "Ocorreu um erro"
messageLabel.textColor = Constants.Colors.ErrorColor
messageLabel.numberOfLines = 0
messageLabel.textAlignment = .center
messageLabel.font = UIFont(name: "TrebuchetMS", size: 20)
messageLabel.backgroundColor = .green
var views: [UIView] = [iconLabel, messageLabel]
if let retry = retry{
let button = RaisedButton(title: "Tentar novamente")
button.pulseColor = Constants.Colors.PrimaryTextColor
button.backgroundColor = Constants.Colors.PrimaryColor
button.titleColor = .white
button.actionHandle(controlEvents: .touchUpInside, ForAction: retry)
button.contentEdgeInsets = UIEdgeInsetsMake(10,10,10,10)
views.append(button)
}
}else{
self.backgroundView = nil
}
}
let stack = UIStackView()
stack.spacing = 10
stack.axis = .vertical
stack.alignment = .center
stack.distribution = .fill
stack.translatesAutoresizingMaskIntoConstraints = false
for view in views{
view.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(view)
}
if self.tableFooterView == nil{
tableFooterView = UIView()
}
self.backgroundView = stack;
if #available(iOS 11, *) {
let guide = self.safeAreaLayoutGuide
stack.topAnchor.constraint(greaterThanOrEqualTo: guide.topAnchor).isActive = true
stack.bottomAnchor.constraint(greaterThanOrEqualTo: guide.bottomAnchor).isActive = true
stack.leadingAnchor.constraint(equalTo: guide.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: guide.trailingAnchor).isActive = true
stack.centerYAnchor.constraint(equalTo: guide.centerYAnchor).isActive = true
} else {
stack.topAnchor.constraint(greaterThanOrEqualTo: self.topAnchor).isActive = true
stack.bottomAnchor.constraint(greaterThanOrEqualTo: self.bottomAnchor).isActive = true
stack.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
stack.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
}
A stack view knows its height if its elements all have an intrinsic size (ie its the sum of their individual heights + the inter item spacing). In this case because you have a 2 y position constraints, you are implying a height, so your constraints are unsatisfiable. The only y axis constraint you need is center vertically. get rid of the top and bottom constraints. The system will then use the intrinsic size to compute the height of the stack view and center it vertically in the background view. Leave your x axis constraints as is.

UIStackView container view height based on subviews

Here is my simple example. I have 1 vertical stack view with 1 subview. I want that subviews height to be based on the intrinsic height of the label within it, so that I can maintain a dynamic height for the entire stack view. How can this be done? Thanks
I think you did it right. But here is the keys:
Don't set height for stackView.
Set label top, bottom, left, trailing constraint to view.
Run. It should be okay on simulator.
If you found label's height seems not wrapping (neither both on storyboard or simulator), then change label's Vertical Content Hugging Priority to 750.
Try this code:
class DyanmicTextLabelViewController: UIViewController {
private var didAddConstraint = false
private let label: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
view.setContentHuggingPriority(.required, for: .vertical)
view.setContentCompressionResistancePriority(.required, for: .vertical)
view.text = "Layout anchors let you create constraints in an easy-to-read, compact format. They expose a number of methods for creating different types of constraints, as shown in Listing 13-1."
view.numberOfLines = 0
return view
}()
private lazy var container: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(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(container)
return view
}()
override func loadView() {
super.loadView()
view.addSubview(stackview)
view.setNeedsUpdateConstraints()
view.backgroundColor = .white
}
override func updateViewConstraints() {
super.updateViewConstraints()
if didAddConstraint == false {
didAddConstraint = true
// stackview constraints
stackview.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
let topAnchor = stackview.topAnchor.constraint(equalTo: view.topAnchor)
topAnchor.constant = 20
topAnchor.isActive = true
stackview.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
// label constraint
// example for giving label a left padding
let labelLeft = label.leftAnchor.constraint(equalTo: container.leftAnchor)
labelLeft.constant = 16.0
labelLeft.isActive = true
label.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
label.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
}
}
}
The important part here is the initialization of stackview, label & constraint set on label
label initialization
private let label: UILabel = {
let view = UILabel()
view.translatesAutoresizingMaskIntoConstraints = false
view.setContentHuggingPriority(.required, for: .vertical)
view.setContentCompressionResistancePriority(.required, for: .vertical)
view.text = "Layout anchors let you create constraints in an easy-to-read, compact format. They expose a number of methods for creating different types of constraints, as shown in Listing 13-1."
view.numberOfLines = 0
return view
}()
stackview initialization
private lazy var stackview : UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.axis = .horizontal
view.distribution = .fill
view.addArrangedSubview(container)
return view
}()
label constraint
// label constraint
// example for giving label a left padding
let labelLeft = label.leftAnchor.constraint(equalTo: container.leftAnchor)
labelLeft.constant = 16.0
labelLeft.isActive = true
label.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
label.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true
This settings could be easily translated to storyboard.

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

Swift UITapGesture on view in a titleView not working

I have a UINavigationItem and I set it's titleView to a UIView which has a UILabel and UIImageView embedded. I'm attempting to add a UITapGestureRecognizer to the view but it doesn't seem to work. Any solutions? Also, adding a gestureRecognizer to the whole navigationBar isn't an option as I have a rightBarButtonItem and want to make use of the back button.
Here is my code:
func configureTitleView() {
guard let profile = profile else {
// Pop navController
return
}
let titleView = UIView()
titleView.frame = CGRect(x: 0, y: 0, width: 100, height: 40)
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
titleView.addSubview(containerView)
let profileImageView = UIImageView()
profileImageView.translatesAutoresizingMaskIntoConstraints = false
profileImageView.contentMode = .scaleAspectFill
profileImageView.clipsToBounds = true
let imageURL = URL(string: profile!.firstProfilePicture!)
profileImageView.sd_setImage(with: imageURL)
containerView.addSubview(profileImageView)
profileImageView.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = true
profileImageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor).isActive = true
profileImageView.widthAnchor.constraint(equalToConstant: 36).isActive = true
profileImageView.heightAnchor.constraint(equalToConstant: 36).isActive = true
profileImageView.layer.cornerRadius = 36 / 2
let nameLabel = UILabel()
containerView.addSubview(nameLabel)
nameLabel.text = profile!.displayName!
nameLabel.textColor = .white
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.leftAnchor.constraint(equalTo: profileImageView.rightAnchor, constant: 8).isActive = true
nameLabel.centerYAnchor.constraint(equalTo: profileImageView.centerYAnchor).isActive = true
nameLabel.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true
nameLabel.heightAnchor.constraint(equalTo: profileImageView.heightAnchor).isActive = true
containerView.centerXAnchor.constraint(equalTo: titleView.centerXAnchor).isActive = true
containerView.centerYAnchor.constraint(equalTo: titleView.centerYAnchor).isActive = true
self.navigationItem.titleView = titleView
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.openProfile))
tapGesture.numberOfTapsRequired = 1
titleView.addGestureRecognizer(tapGesture)
titleView.isUserInteractionEnabled = true
}
Beginning with iOS 11, views added to toolbars as UIBarButtonItem using UIBarButtonItem(customView:) are now laid out using auto layout. This includes title views added to a UINavigationBar through the navigationItem.titleView property of a UIViewController. You should add sizing constraints on your titleView. For example:
titleView.widthAnchor.constraint(equalToConstant: 100).isActive = true
titleView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
Otherwise, auto layout will use the intrinsic content size of your title view which is CGSize.zero. Gestures are masked to the bounds of the view they are attached to even if the sub views of that view are not. Because the bounds of titleView without constraints is CGRect.zero it will never fire. Add constraints and it works as expected.
For more information see the WWDC 2017 session Updating your app for iOS 11.
You do not need to add explicit height and width constant constraint to custom view.
Just add subviews to custom view, add width and height anchor.
let customView = UIView()
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
customView.addSubview(button)
[
button.widthAnchor.constraint(equalTo: customView.widthAnchor),
button.heightAnchor.constraint(equalTo: customView.heightAnchor),
].forEach({$0.isActive = true})
navigationItem.titleView = customView

Setting constant width on subview of UIStackView when axis is vertical

I have a UIScrollView that contains a UIStackView, and I add views to it and if the UIStackView needs more space than the screen has then it will scroll thanks to the UIScrollView.
I am able to set constant heights on the views, but I also need to set a specific width on them, so that they have a specific width and are also centered in the stack view.
Something like this, except the widthAnchor does not work.
import UIKit
class ViewController: UIViewController {
let scrollView: UIScrollView = {
let sv = UIScrollView()
sv.translatesAutoresizingMaskIntoConstraints = false
sv.backgroundColor = .gray
return sv
}()
let stackView: UIStackView = {
let sv = UIStackView()
sv.translatesAutoresizingMaskIntoConstraints = false
sv.axis = .vertical
return sv
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
scrollView.addSubview(stackView)
stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true
stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
let view1 = UIView()
view1.backgroundColor = .red
let view2 = UIView()
view2.backgroundColor = .blue
let view3 = UIView()
view3.backgroundColor = .green
let view4 = UIView()
view4.backgroundColor = .purple
stackView.addArrangedSubview(view1)
stackView.addArrangedSubview(view2)
stackView.addArrangedSubview(view3)
stackView.addArrangedSubview(view4)
view1.heightAnchor.constraint(equalToConstant: 200).isActive = true
view2.heightAnchor.constraint(equalToConstant: 300).isActive = true
view3.heightAnchor.constraint(equalToConstant: 420).isActive = true
view4.heightAnchor.constraint(equalToConstant: 100).isActive = true
// This does not work.
// view1.widthAnchor.constraint(equalToConstant: 40).isActive = true
}
}
The alignment property on a UIStackView determines how its layout works perpendicular to its axis. By default, a UIStackView has an alignment of fill. In constraint terms, fill is like adding a constraint to (in this case) the left and right edges of the stack view for each arranged subview. These implicit constraints are likely causing your problem. Solution: set stackView.alignment = either leading, center, or trailing depending on your desired effect.

Resources