I have 10 UILabels with different sizes, and I want to arrange them in 3 rows, and all rows have leading alignment, and if the last item of each row can't fit in remaining space of parent view, it have to move to the next line. How can I do that by using UIStackview?
You can try to do something like that:
Your ViewController
for item in array {
let someView = VPView(frame: CGRect(x: 0, y: 0, width: 70, height: 20))
someView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(someView)
}
UIView Class:
class VPView: UIView {
let myLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.addLabel()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func addLabels() {
//Mark: - Styles
let labelH: CGFloat = 15
let labelW: CGFloat = 70
//MARK: - My Label
self.myLabel.frame = CGRect(x: 0, y: 0, width: labelW, height: labelH)
self.myLabel.backgroundColor = UIColor.blue
self.myLabel.textAlignment = NSTextAlignment.center
self.myLabel.numberOfLines = 0
self.addSubview(self.myLabel)
self.myLabel.translatesAutoresizingMaskIntoConstraints = false
self.myLabel.heightAnchor.constraint(equalToConstant: labelH).isActive = true
self.myLabel.widthAnchor.constraint(equalToConstant: labelW).isActive = true
self.myLabel.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
self.myLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
}
}
Related
I am trying to animate a multi-line label inside a UIView. In the container view, the width of the label is relative to the bounds. When the container view is animated, the label jumps to the final state and then the container resizes. How can I instead animate the right side of the text to be continuously pinned to the right edge of the container view as it grows larger?
class ViewController: UIViewController {
var container: ContainerView = ContainerView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(container)
container.frame = CGRect(x: 0, y: 0, width: 150, height: 150)
container.center = view.center
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut) {
self.container.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
self.container.center = self.view.center
self.container.layoutIfNeeded()
}
}
}
}
class ContainerView: UIView {
let label: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.text = "foo bar foo bar foo bar foo bar foo bar foo bar foo foo bar foo bar foo bar foo bar foo bar foo bar foo"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .purple
addSubview(label)
}
override func layoutSubviews() {
super.layoutSubviews()
let size = label.sizeThatFits(CGSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude))
label.frame = CGRect(x: 0, y: 0, width: self.bounds.width, height: size.height)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
As you've seen, when we change the width of a label UIKit re-calculates the word wrapping immediately.
When we do something like this:
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut) {
self.container.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
self.container.center = self.view.center
self.container.layoutIfNeeded()
}
UIKit sets the width and then animates it. So, as soon as the animation starts, the word wrapping gets set to the "destination" width.
One way to animate the word wrap changes would be to create an animation loop, using small point-size changes.
That works-ish, with two problems:
Using a UILabel, we get vertical shifting (because the text is vertically centered in a label), and
If we make the incremental size changes small, it's smooth but slow. If we make the incremental changes large, it's quick but "jerky."
To solve the first problem, we can use a UITextView, subclassed to work like a top-aligned UILabel. Here's an example:
class MyTextViewLabel: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() -> Void {
isScrollEnabled = false
isEditable = false
isSelectable = false
textContainerInset = UIEdgeInsets.zero
textContainer.lineFragmentPadding = 0
}
}
Not much we can do about the second problem, other than experiment with the width-increment value.
Here's a complete example to look at and play with (using the above MyTextViewLabel class). Note that I'm also using auto-layout / constraints instead of explicit frames:
class MyContainerView: UIView {
let label: MyTextViewLabel = {
let label = MyTextViewLabel()
label.text = "Let's use some readable text for this example. It will make the wrapping changes look more natural than using a bunch of repeating three-character \"words.\""
// let's set the font to the default UILabel font
label.font = .systemFont(ofSize: 17.0)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
clipsToBounds = true
backgroundColor = .purple
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// let's inset the "label" by 4-points so we can see the purple view frame
label.topAnchor.constraint(equalTo: topAnchor, constant: 4.0),
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4.0),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4.0),
// if we want the bottom text to be "clipped"
// don't set the bottom anchor
//label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4.0),
])
label.backgroundColor = .yellow
}
}
class LabelWrapAnimVC: UIViewController {
// for this example
let startWidth: CGFloat = 150.0
let targetWidth: CGFloat = 200.0
// number of points to increment in each loop
// play with this value...
// 1-point produces a very smooth result, but the total animation time will be slow
// 5-points seems "reasonable" (looks smoother on device than on simulator)
let loopIncrement: CGFloat = 5.0
// total amount of time for the animation
let loopTotalDuration: TimeInterval = 2.0
// each loop anim duration - will be calculated
var loopDuration: TimeInterval = 0
let container: MyContainerView = MyContainerView()
var cWidth: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(container)
container.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
cWidth = container.widthAnchor.constraint(equalToConstant: startWidth)
NSLayoutConstraint.activate([
container.centerXAnchor.constraint(equalTo: g.centerXAnchor),
container.centerYAnchor.constraint(equalTo: g.centerYAnchor),
container.heightAnchor.constraint(equalTo: container.widthAnchor),
cWidth,
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
doAnim()
}
func animLoop() {
cWidth.constant += loopIncrement
// in case we go over the target width
cWidth.constant = min(cWidth.constant, targetWidth)
UIView.animate(withDuration: loopDuration, animations: {
self.view.layoutIfNeeded()
}, completion: { _ in
if self.cWidth.constant < self.targetWidth {
self.animLoop()
} else {
// maybe do something when animation is done
}
})
}
func doAnim() {
// reset width to original
cWidth.constant = startWidth
// calculate loop duration based on size difference
let numPoints: CGFloat = targetWidth - startWidth
let numLoops: CGFloat = numPoints / loopIncrement
loopDuration = loopTotalDuration / numLoops
DispatchQueue.main.async {
self.animLoop()
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
doAnim()
}
}
I don't know if this will be suitable for your target usage, but it's at least worth a look.
this is the code where i am configuring the cell.
cell.configure(image: models[indexPath.item].image, tagText: models[indexPath.item].tag, priceIcon: models[indexPath.item].priceIcon, value: models[indexPath.item].price)
and this is my code for cell.
//
// ImageCollectionViewCell.swift
// tr0ve-iOSApp
//
//
//
// ImageCollectionViewCell.swift
// MyCollectionView
//
import UIKit
class ImageCollectionViewCell: UICollectionViewCell {
static let identifier = "ImageCollectionViewCell"
let itemView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(itemView)
contentView.clipsToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
itemView.frame = contentView.bounds
}
override func prepareForReuse() {
super.prepareForReuse()
itemView.subviews.forEach { view in
view.removeFromSuperview()
}
}
func configure (image: UIImage?, tagText: String, priceIcon: UIImage, value: Float){
let imageView = UIImageView(image: image)
let priceTagView = UIView(frame: CGRect(x: 5, y: contentView.frame.size.height-50, width: 60, height: 20))
priceTagView.backgroundColor = .black
let valueLabel = UILabel(frame: CGRect(x: 20, y: 5, width: 35, height: 10))
valueLabel.textColor = .white
valueLabel.text = String(value)
valueLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
let priceIconView = UIImageView(frame: CGRect(x: 5, y: 5, width: 10, height: 10))
priceIconView.image = priceIcon
priceTagView.addSubview(priceIconView)
priceTagView.addSubview(valueLabel)
let tag = UILabel(frame: CGRect(x: 100, y: contentView.frame.size.height-50, width: 20, height: 20))
tag.text = tagText
tag.textColor = .white
tag.textAlignment = .center
if tagText == "UC" {
tag.backgroundColor = .green
}
else {
tag.backgroundColor = .blue
}
itemView.addSubview(imageView)
itemView.addSubview(priceTagView)
itemView.addSubview(tag)
}
}
[in this image i want to add subview in cell's bottom, and in tried it using image's frame and by contentView's frame. but it is doing good for some cells and bad for the other ones. and when i scroll everyhing meshes up.1
I set custom view to titleView of UINavigationBar.
When I just set the view with frame, it works fine.
However if I add any subview to a view that I set to a title view, it just does not appear.
A view added to titleView
class NavigationBarSearchBar: UIView {
let view: UIView = {
let v = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 20))
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupSubviews() {
addSubview( view )
[
view.widthAnchor.constraint(equalToConstant: 20),
view.heightAnchor.constraint(equalToConstant: 20),
view.centerYAnchor.constraint(equalTo: centerYAnchor),
view.centerXAnchor.constraint(equalTo: centerXAnchor)
].forEach { $0.isActive = true }
}
}
Declaration on View Controller
private lazy var searchBar: UIView = {
let sb = NavigationBarSearchBar(frame: CGRect(x: 0, y: 0, width: CGFloat.greatestFiniteMagnitude, height: 40))
return sb
}()
Set the custom view to nav bar
navigationItem.titleView = searchBar
lazy var searchBar:UISearchBar = UISearchBar(frame: CGRectMake(0, 0, 200, 20))
searchBar.sizeToFit()
navigationItem.titleView = searchBar
Create a view with Frame and add to navigationItem
var viewTitle = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 30))
viewTitle.backgroundColor = UIColor.red
navigationItem?.titleView = viewTitle
I want to create custom UITextField with error label on bottom of it. I want the label to be multiline, I tried numberOfLines = 0. But it is not working.
Here is my snippet for the class
public class MyTextField: UITextField {
private let helperTextLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12.0, weight: UIFont.Weight.regular)
label.textColor = helperTextColor
label.numberOfLines = 0
return label
}()
public override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(errorTextLabel)
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addSubview(errorTextLabel)
}
public override func layoutSubviews() {
super.layoutSubviews()
errorTextLabel.frame = CGRect(x: bounds.minX, y: bounds.maxY - 20, width: bounds.width, height: 20)
}
public override var intrinsicContentSize: CGSize {
return CGSize(width: 240.0, height: 68.0)
}
public override func sizeThatFits(_ size: CGSize) -> CGSize {
return intrinsicContentSize
}
}
I think the root cause is because I set height to 20, but how can I set the height dynamically based on the errorTextLabel.text value?
You are giving your label a fixed size.
Not using AutoLayout and giving the textfield and label room to expand it's size when needed.
Personally, if creating this particular control, I would create a UIView with a textfield and a label inside a UIStackView. That way if the label is hidden when there is no error the stackview will automatically adjust the height for you. Then when you unhide it, the view will expand to fit both controls.
A basic example:
//: Playground - noun: a place where people can play
import UIKit
import PlaygroundSupport
class LabelledTextView: UIView {
private let label = UILabel()
private let textfield = UITextField()
private let stackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true
stackView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
stackView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
stackView.alignment = .leading
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.addArrangedSubview(textfield)
stackView.addArrangedSubview(label)
textfield.placeholder = "Please enter some text"
label.numberOfLines = 0
label.text = "Text did not pass validation, Text did not pass validation, Text did not pass validation, Text did not pass validation"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
let labelledTextView = LabelledTextView(frame: CGRect(x: 50, y: 300, width: 300, height: 60))
let vc = UIViewController()
vc.view.addSubview(labelledTextView)
labelledTextView.translatesAutoresizingMaskIntoConstraints = false
labelledTextView.topAnchor.constraint(equalTo: vc.view.topAnchor).isActive = true
labelledTextView.leftAnchor.constraint(equalTo: vc.view.leftAnchor).isActive = true
labelledTextView.widthAnchor.constraint(equalToConstant: 300).isActive = true
labelledTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true
PlaygroundPage.current.liveView = vc.view
You need to use sizeToFit():
errorTextLabel.sizeToFit()
I've been struggling with this for almost an entire day now and can't find a solution. I'm trying to create a UICollectionView within a view controller that has cells just with a single label in each of them. However, I can't seem to add the label as a subview to the cell no matter what I do. Here is the code I have in my main view controller:
var collectionView: UICollectionView!
func configureCollectionView()
{
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: 50, height: 20)
let y = interestsUnderline.frame.origin.y + 5
let width = interestsUnderline.frame.width
let frame = CGRect(x: 40, y: y, width: width, height: 30)
collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
collectionView?.dataSource = self
collectionView?.delegate = self
collectionView?.register(InterestsCell.self, forCellWithReuseIdentifier: "InterestsCell")
collectionView?.showsHorizontalScrollIndicator = false
view.addSubview(collectionView!)
}
I call configureCollectionView() from viewDidLayoutSubviews. This is the code I have in my collection view cell.
class InterestsCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
let label: UILabel = {
let label = UILabel()
label.text = "Hello"
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
func setupViews() {
contentView.backgroundColor = .red
contentView.addSubview(label)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Whenever I dequeue the cell, I get the correct size of the cell and the correct color. But when I go to set constraints on the label in my cell's setupViews function, the view controller will just not even open up altogether. If I don't set the constraints, the label is not there. I'm not sure what to do at this point and I would appreciate some help. Thank you!
I took a look at your above snippet and what I found is you haven't set the Frame you need to set the Frame of your UILabel
let label: UILabel = {
let label = UILabel()
label.frame = CGRect(x: 40, y: 100, width: self.view.frame.width, height: 30)
label.text = "Hello"
label.textColor = UIColor.red
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
Hope this help you
Since your project uses auto layout constraints you have to set them for the label:
func setupViews() {
contentView.backgroundColor = .red
contentView.addSubview(label)
label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
}
Inside your InterestsCell class add the following code:
override func layoutSubviews() {
super.layoutSubviews()
setupViews()
}
instead of:
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
Hope this would work