Adding labels and textviews in a stack view programmatically in swift - swift

How can I do to have a title, followed by a few lines of text, followed by a title again and again few lines of text constrained in the middle of a view controller programmatically?
My goal is to have bolded for the titles, and it would be nice to have the textview lines incremented also.
My idea was to create 2 labels, and 2 textviews. And adding those to a textview in this order: label1, t1, label2, t2.
But it doesn't seem to work. I try to avoid defining the same textviews and labels many times. textviews add up if I copy its definition twice but not for labels (maybe it is view related?)
I tried with UIbuttons and it worked.
This is what I tried so far:
import UIKit
class HowToSetupProIGVC: UIViewController {
deinit {print("deinit")}
let textView: UITextView = {
let textView = UITextView()
textView.backgroundColor = .blue //bkgdColor
textView.textAlignment = .left
//textView.frame = CGRect(x: 5, y: 5, width: 5, height: 5)
textView.tintColor = .black
textView.translatesAutoresizingMaskIntoConstraints = false //enable autolayout
textView.heightAnchor.constraint(equalToConstant: 100).isActive = true
textView.widthAnchor.constraint(equalToConstant: 300).isActive = true
return textView
}()
let label: UILabel = {
let l = UILabel(frame:CGRect.zero)
//l.frame = CGRect(x: 5, y: 5, width: 5, height: 5)
l.backgroundColor = .green //bkgdColor
l.font = UIFont.preferredFont(forTextStyle: .headline)
l.translatesAutoresizingMaskIntoConstraints = false //enable autolayout
l.heightAnchor.constraint(equalToConstant: 22).isActive = true
l.widthAnchor.constraint(equalToConstant: 300).isActive = true
return l
}()
override func viewDidLoad() {
super.viewDidLoad()
self.modalUI(arrowButton: false)
self.view.backgroundColor = bkgdColor
customStackHTSProIG ()
}
}
extension HowToSetupProIGVC {
func customStackHTSProIG () {
let label1 = label
let label2 = label
let t1 = textView
let t2 = textView
label1.text = "Title1:"
label2.text = "title2:"
t1.text = """
1. On your profile tap menu
2. Tap settings
3. Tap accounts
4. Tap set up professional account
"""
t2.text = """
1. On your profile tap "Edit profile"
2. Link your created page to your account
"""
//StackView
let stackHTS = UIStackView()
stackHTS.axis = NSLayoutConstraint.Axis.vertical
stackHTS.distribution = .fillEqually
stackHTS.alignment = .center
stackHTS.spacing = 5
stackHTS.backgroundColor = .red
//Add StackView + elements
stackHTS.addArrangedSubview(label1)
stackHTS.addArrangedSubview(t1)
stackHTS.addArrangedSubview(label2)
stackHTS.addArrangedSubview(t2)
self.view.addSubview(stackHTS)
//Constraints StackView
stackHTS.translatesAutoresizingMaskIntoConstraints = false
stackHTS.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
stackHTS.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
//stackHTS.heightAnchor.constraint(equalToConstant: 88).isActive = true
}
}

UILabel & UITextView are both UIKit classes written in Objective-C. They are reference types, NOT value types.
When you write following -
let label1 = label
let label2 = label
let t1 = textView
let t2 = textView
Both label1 & label2 are pointing to the one & same instance of UILabel. So is the case for t1 & t2 as well.
When you add them like this -
//Add StackView + elements
stackHTS.addArrangedSubview(label1)
stackHTS.addArrangedSubview(t1)
stackHTS.addArrangedSubview(label2)
stackHTS.addArrangedSubview(t2)
You expect 2 labels and 2 textViews to be added to the StackView. You are adding only 1 label and 1 textView though.
You expect to see all of following -
label1.text = "Title1:"
label2.text = "title2:"
t1.text = """
1. On your profile tap menu
2. Tap settings
3. Tap accounts
4. Tap set up professional account
"""
t2.text = """
1. On your profile tap "Edit profile"
2. Link your created page to your account
"""
However you are only seeing following -
label2.text = "title2:"
t2.text = """
1. On your profile tap "Edit profile"
2. Link your created page to your account
"""
Solutions -
Create two separate instances of UITextView & UILabel like you have already done for the first two and Add these new instances to stack view as well.
Use one UILabel and remove everything else. Use NSAttributedString API to stylize your text as you want for different sections / paragraphs and assign it to UILabel.attributedText.

Related

sendSubviewToBack is not working when an UIView is added to a custom UILabel

I am iterating values and add them to a container UIView.
for value in bla {
// Create a new label
let labelHashtag = UILabelBadge()
labelHashtag.backgroundColor = .white
labelHashtag.frame.size.width = labelHashtag.intrinsicContentSize.width + tagPadding
labelHashtag.frame.size.height = tagHeight
labelHashtag.layer.cornerRadius = labelHashtag.layer.frame.size.height / 2
labelHashtag.topInset = 2
labelHashtag.bottomInset = 2
labelHashtag.rightInset = 6
labelHashtag.leftInset = 6
labelHashtag.textAlignment = .center
// !!! Here I am trying to add an UIView and send it back !!!
let bla = UIView()
bla.frame = labelHashtag.bounds
bla.backgroundColor = .red
labelHashtag.addSubview(bla)
labelHashtag.sendSubviewToBack(bla)
Container.addSubview(labelHashtag)
}
It also does not work when I use insertSubview at: 0. It will stay on the top of my UILabel.
How can I add an UIView and send it back?
When you add subview on UILabel means that UILabel is the root and all subviews will on the front of root view.
From your question, there are two approaches to achieve that.
First one, create a root UIView and add your UILabel and UIView into
for value in bla {
let rootUIView = UIView()
// Create a new label
let labelHashtag = UILabelBadge()
labelHashtag.backgroundColor = .white
labelHashtag.frame.size.width = labelHashtag.intrinsicContentSize.width + tagPadding
labelHashtag.frame.size.height = tagHeight
labelHashtag.layer.cornerRadius = labelHashtag.layer.frame.size.height / 2
labelHashtag.topInset = 2
labelHashtag.bottomInset = 2
labelHashtag.rightInset = 6
labelHashtag.leftInset = 6
labelHashtag.textAlignment = .center
// !!! Here I am trying to add an UIView and send it back !!!
let backgroundView = UIView()
backgroundView.frame = labelHashtag.bounds
backgroundView.backgroundColor = .red
rootUIView.addSubview(labelHashtag)
rootUIView.addSubview(backgroundView)
rootUIView.sendSubviewToBack(backgroundView)
Container.addSubview(rootUIView)
}
Second one, make your UIView as a root and add your custom UILabel into it
for value in bla {
// Create a new label
let labelHashtag = UILabelBadge()
labelHashtag.backgroundColor = .white
labelHashtag.frame.size.width = labelHashtag.intrinsicContentSize.width + tagPadding
labelHashtag.frame.size.height = tagHeight
labelHashtag.layer.cornerRadius = labelHashtag.layer.frame.size.height / 2
labelHashtag.topInset = 2
labelHashtag.bottomInset = 2
labelHashtag.rightInset = 6
labelHashtag.leftInset = 6
labelHashtag.textAlignment = .center
// !!! Here I am trying to add an UIView and send it back !!!
let backgroundView = UIView()
backgroundView.frame = labelHashtag.bounds
backgroundView.backgroundColor = .red
backgroundView.addSubview(labelHashtag)
Container.addSubview(backgroundView)
}

Why doesn't my UILabel in a nested view receive touch events / How can I test the Responder Chain?

I have found lots of similar questions about not receiving touch events and I understand that in some cases, writing a custom hitTest function may be required - but I also read that the responder chain will traverse views and viewControllers that are in the hierarchy - and I don't understand why a custom hitTest would be required for my implementation.
I'm looking for an explanation and/or a link to a document that explains how to test the responder chain. This problem is occurring in Xcode 10.2.1.
My scenario (I am not using Storyboard):
I have a mainViewController, that provides a full screen view with an ImageView and a few Labels. I have attached TapGestureRecognizers to the ImageView and one of the labels - and they both work properly.
When I tap the label, I add a child viewController and it's view as a subview to the mainViewController. The view is constrained to cover only the right-half of the screen.
The child viewController contains a vertical stack view that contains 3 arrangedSubviews.
Each arrangedSubview contains a Label and a horizontal StackView.
The horizontal stackView's each contain a View with a Label as a subview.
The Label in the subview sets it's isUserInteractionEnabled flag to True and adds a TapGestureRecognizer.
These are the only objects in the child ViewController that have 'isUserInteractionEnabled' set.
The Label's are nested fairly deep, but since this is otherwise a direct parent/child hierarchy (as opposed to the 2 views belonging to a NavigationController), I would expect the Label's to be in the normal responder chain and function properly. Do the Stack View's change that behavior? Do I need to explicitly set the 'isUserInteractionEnabled' value to False on some of the views? Is there way I can add logging to the ResponderChain so I can see which views it checked and find out where it is being blocked?
After reading this StackOverflow post I tried adding my gesture recognizers in viewDidLayoutSubviews() instead of what's shown below - but they still do not receive tap events.
Thank you in advance to any who can offer advice or help.
Here is the code for the label that is not responding to my tap events and the tap event it should call:
func makeColorItem(colorName:String, bgColor:UIColor, fgColor:UIColor) -> UIView {
let colorNumber:Int = colorLabelDict.count
let colorView:UIView = {
let v = UIView()
v.tag = 700 + colorNumber
v.backgroundColor = .clear
v.contentMode = .center
return v
}()
self.view.addSubview(colorView)
let tapColorGR:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapColor))
let colorChoice: UILabel = {
let l = UILabel()
l.tag = 700 + colorNumber
l.isUserInteractionEnabled = true
l.addGestureRecognizer(tapColorGR)
l.text = colorName
l.textAlignment = .center
l.textColor = fgColor
l.backgroundColor = bgColor
l.font = UIFont.systemFont(ofSize: 24, weight: .bold)
l.layer.borderColor = fgColor.cgColor
l.layer.borderWidth = 1
l.layer.cornerRadius = 20
l.layer.masksToBounds = true
l.adjustsFontSizeToFitWidth = true
l.translatesAutoresizingMaskIntoConstraints = false
l.widthAnchor.constraint(equalToConstant: 100)
return l
}()
colorView.addSubview(colorChoice)
colorChoice.centerXAnchor.constraint(equalTo: colorView.centerXAnchor).isActive = true
colorChoice.centerYAnchor.constraint(equalTo: colorView.centerYAnchor).isActive = true
colorChoice.heightAnchor.constraint(equalToConstant: 50).isActive = true
colorChoice.widthAnchor.constraint(equalToConstant: 100).isActive = true
colorLabelDict[colorNumber] = colorChoice
return colorView
}
#objc func tapColor(sender:UITapGestureRecognizer) {
print("A Color was tapped...with tag:\(sender.view?.tag ?? -1)")
if let cn = sender.view?.tag {
colorNumber = cn
let v = colorLabelDict[cn]
if let l = (v?.subviews.first as? UILabel) {
print("The \(l.text) label was tapped.")
}
}
}
It looks like the main reason you're not getting a tap recognized is because you are adding a UILabel as a subview of a UIView, but you're not giving that UIView any constraints. So the view ends up with a width and height of Zero, and the label exists outside the bounds of the view.
Without seeing all of your code, it doesn't look like you need the extra view holding the label.
Take a look at this... it will add a vertical stack view to the main view - centered X and Y - and add "colorChoice" labels to the stack view:
class TestViewController: UIViewController {
let stack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 4
return v
}()
var colorLabelDict: [Int: UIView] = [:]
override func viewDidLoad() {
super.viewDidLoad()
let v1 = makeColorLabel(colorName: "red", bgColor: .red, fgColor: .white)
let v2 = makeColorLabel(colorName: "green", bgColor: .green, fgColor: .black)
let v3 = makeColorLabel(colorName: "blue", bgColor: .blue, fgColor: .white)
[v1, v2, v3].forEach {
stack.addArrangedSubview($0)
}
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
func makeColorLabel(colorName:String, bgColor:UIColor, fgColor:UIColor) -> UILabel {
let colorNumber:Int = colorLabelDict.count
// create tap gesture recognizer
let tapColorGR:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapColor))
let colorChoice: UILabel = {
let l = UILabel()
l.tag = 700 + colorNumber
l.addGestureRecognizer(tapColorGR)
l.text = colorName
l.textAlignment = .center
l.textColor = fgColor
l.backgroundColor = bgColor
l.font = UIFont.systemFont(ofSize: 24, weight: .bold)
l.layer.borderColor = fgColor.cgColor
l.layer.borderWidth = 1
l.layer.cornerRadius = 20
l.layer.masksToBounds = true
l.adjustsFontSizeToFitWidth = true
l.translatesAutoresizingMaskIntoConstraints = false
// default .isUserInteractionEnabled for UILabel is false, so enable it
l.isUserInteractionEnabled = true
return l
}()
NSLayoutConstraint.activate([
// label height: 50, width: 100
colorChoice.heightAnchor.constraint(equalToConstant: 50),
colorChoice.widthAnchor.constraint(equalToConstant: 100),
])
// assign reference to this label in colorLabelDict dictionary
colorLabelDict[colorNumber] = colorChoice
// return newly created label
return colorChoice
}
#objc func tapColor(sender:UITapGestureRecognizer) {
print("A Color was tapped...with tag:\(sender.view?.tag ?? -1)")
// unwrap the view that was tapped, make sure it's a UILabel
guard let tappedView = sender.view as? UILabel else {
return
}
let cn = tappedView.tag
let colorNumber = cn
print("The \(tappedView.text ?? "No text") label was tapped.")
}
}
Result of running that:
Those are 3 UILabels, and tapping each will trigger the tapColor() func, printing this to the debug console:
A Color was tapped...with tag:700
The red label was tapped.
A Color was tapped...with tag:701
The green label was tapped.
A Color was tapped...with tag:702
The blue label was tapped.

How to setup constraints with SnapKit for UITextField and UILabel?

I have a [[TextField need max available width----]offset(10.0)][Label]]. I want to setup TextFeild pin to left and shrink all available space without label trimming and setup label to pin right and get minimal fit size.
lazy var textField: UITextField = {
var textField = UITextField()
textField.placeholder = "Placeholder"
textField.delegate = self
textField.borderStyle = UITextField.BorderStyle.none
textField.keyboardType = UIKeyboardType.numberPad
textField.returnKeyType = UIReturnKeyType.done
textField.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return textField
}()
lazy var measureLabel: UILabel = {
var label = UILabel()
label.numberOfLines = 1
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}()
measureLabel.snp.makeConstraints { (make) in
make.right.equalTo(self.snp.right)
make.centerY.equalToSuperview()
}
textField.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.right.equalTo(self.measureLabel.snp.left).offset(-10.0)
make.centerY.equalToSuperview()
}
You need
label.setContentHuggingPriority(.required, for: .horizontal)
label.setContentCompressionResistancePriority(.required, for: .horizontal)
Also you can completely remove these 2 lines as by default textfield's ContentHuggingPriority && ContentCompressionResistancePriority is lower than the default for label , plus the textfield has no intrinsic size
Implement the a demo below.
label can automatically increase height with attributes below. (swift 5)
label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
It can make the height of super view synchronize at the same time, when you set
the height of super view the same as label.
label = UILabel()
viewContainer.addSubview(label)
label.backgroundColor = UIColor.white
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.text = "hello world, today is a new day. Have a nice day. hello world, today is a new day. Have a nice day. hello world, today is a new day. Have a nice day. hello world, today is a new day. Have a nice day. hello world, today is a new day. Have a nice day. hello world, today is a new day. Have a nice day."
self.addSubview(label)
label.snp.makeConstraints { (make) in
let superView = viewContainer!
make.left.equalTo(superView).offset(10)
make.right.equalTo(superView).offset(-10)
make.centerY.equalTo(superView)
}
viewContainer.snp.makeConstraints { (make) in
make.centerY.equalTo(self)
make.centerX.equalTo(self)
make.left.equalTo(self).offset(10)
make.right.equalTo(self).offset(-10)
make.height.equalTo(label).offset(100)
}
download code: https://github.com/zgpeace/SnapkitDemo/tree/dynamicHeightLabel

shift bar buttons to left side in toolbar with swift

I have a toolbar that is placed at the bottom (correctly). The problem is the button (on a UIBarButtonItem) sits in the center of the entire toolbar.
How can I position this button to the side, but still (vertically) centered with the image?
It'd be best with a margin.
So it'd look like
--------------
|. X
|. label
--------------
The code is:
let customButton: UIButton = UIButton(type: .custom)
customButton.setImage(UIImage(named: "start"), for: .normal)
customButton.setTitle("Start", for: .normal)
customButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 12)
customButton.setTitleColor(UIColor.white, for: .normal)
customButton.sizeToFit()
customButton.centerLabelVerticallyWithPadding(spacing: 5)
customButton.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(startButtonAction(_:))))
iconBar.items = [UIBarButtonItem(customView: customButton)]
Where iconBar - the toolbar is defined as:
let iconBar: UIToolbar =
{
let view = UIToolbar()
view.translatesAutoresizingMaskIntoConstraints = false
view.barTintColor = UIColor.black
return view
}()
Also I'm using an extension to center the UIButton, then I can add an image (also centered) above it. Here's the extension:
extension UIButton
{
func centerLabelVerticallyWithPadding(spacing:CGFloat)
{
// update positioning of image and title
let imageSize = self.imageView!.frame.size
self.titleEdgeInsets = UIEdgeInsets(top:0,
left:-imageSize.width,
bottom:-(imageSize.height + spacing),
right:0)
let titleSize = self.titleLabel!.frame.size
self.imageEdgeInsets = UIEdgeInsets(top:-(titleSize.height + spacing),
left:0,
bottom: 0,
right:-titleSize.width)
// reset contentInset, so intrinsicContentSize() is still accurate
let trueContentSize = self.titleLabel!.frame.union(self.imageView!.frame).size
let oldContentSize = self.intrinsicContentSize
let heightDelta = trueContentSize.height - oldContentSize.height
let widthDelta = trueContentSize.width - oldContentSize.width
self.contentEdgeInsets = UIEdgeInsets(top:heightDelta/2.0,
left:widthDelta/2.0,
bottom:heightDelta/2.0,
right:widthDelta/2.0)
}
}
Are you looking for the 'flexible space bar button item' or 'fixed space bar button item'? then you can add a little bit of space to the left and right inside the toolbar and define it's size.
It will look something like this:
let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
spacer.width = 10

how to align loop of labels horizontally with different width using autolayout Programatically

I want to align set of labels next to each other using AutoLayout constraint programmatically.
NOTE:And each label must have different width.Like i need to assign in array [50,40,70,30,20,10]
I find some relevant questions but not useful in my case
for var i=0;i<7;i++{
let DataLabel = UILabel()
DataLabel.translatesAutoresizingMaskIntoConstraints = false
DataLabel.text = NamingLabel[i]
DataLabel.backgroundColor = UIColor.lightGrayColor()
Self.view.addSubview(DataLabel)
}
If the constraint is in Visual Format language means will be more useful.
Thanks in Advance.
You need the following VFL strings for you labels:
To place each label at 40pt from the top each label needs: "V:|-40-[*labelName*]". Change the 40 to whatever suits you best.
To place the labels next to each other and set their widths you need this single VFL string: "H:|-[label0(50)][label1(40)][label2(70)][label3(30)][label4(20)][label5(10)][label6(30)]"
To be able to use Visual Format Language you have create a dictionary that contains all labels. The values of that dictionary are the UILabels and the keys are Strings that you use in the VFL strings to define which label a constraint should be added to. In this case I use the keys "label0", "label1", "label2" etc.
Here is an example how you could add the constraints in the loop:
let labelWidths = [50, 40, 70, 30, 20, 10, 30]
var labels = [String: UILabel]()
var allConstraints = [NSLayoutConstraint]()
var horizontalConstraintsString = "H:|-"
for i in 0..<7 {
let labelName = "label\(i)"
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
// set text, textColor etc.
view.addSubview(label)
labels[labelName] = label
// add vertical constraints to label
let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|-40-[\(labelName)]", options: [], metrics: nil, views: labels)
allConstraints.appendContentsOf(verticalConstraints)
// add label to horizontal VFL string
horizontalConstraintsString += "[\(labelName)(\(labelWidths[i]))]"
}
// get horizontal contraints from horizontal VFL string
let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(horizontalConstraintsString, options: [], metrics: nil, views: labels)
allConstraints.appendContentsOf(horizontalConstraints)
NSLayoutConstraint.activateConstraints(allConstraints)
UPDATE:
As you you are writing in your comment that it still does not work for you, here is the full ViewController class from my example project. So just create a new project in Xcode and paste the following code into the ViewController class:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.lightGrayColor()
let labelWidths = [50, 40, 70, 30, 20, 10, 30]
var labels = [String: UILabel]()
var allConstraints = [NSLayoutConstraint]()
var horizontalConstraintsString = "H:|-"
for i in 0..<7 {
let labelName = "label\(i)"
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "\(i)"
label.layer.cornerRadius = 6
label.clipsToBounds = true
label.backgroundColor = UIColor.grayColor()
view.addSubview(label)
labels[labelName] = label
// add vertical constraints to label
let verticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|-40-[\(labelName)]", options: [], metrics: nil, views: labels)
allConstraints.appendContentsOf(verticalConstraints)
// add label to horizontal VFL string
horizontalConstraintsString += "[\(labelName)(\(labelWidths[i]))]"
}
// get horizontal contraints from horizontal VFL string
let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(horizontalConstraintsString, options: [], metrics: nil, views: labels)
allConstraints.appendContentsOf(horizontalConstraints)
NSLayoutConstraint.activateConstraints(allConstraints)
}
}
I added a background color and a corner radius to the labels so you can see where one label ends and the next begins. When you run this project you should see the following: