Multi-line UILabel causing extra padding - ios

I have a collectionView cell that has a label and an imageView. The imageView is 8 points from the top of the cell, the top of the label is 8 points from the bottom of the imageView, and the bottom of the label is 8 points from the bottom of the cell. The label wraps when it gets -10 point away from the right edge of the cell.
The text that goes into the label can span several lines. I use the below function inside the collectionView's sizeForItem to calculate the label's height:
func estimatedLabelHeight(text: String, width: CGFloat, font: UIFont) -> CGFloat {
let size = CGSize(width: width, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union([.usesLineFragmentOrigin, .usesFontLeading])
let attributes = [NSAttributedStringKey.font: font]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
The cell expands correctly but the label has extra padding that is added to it and I cannot figure out how to get rid of it.
That is a multi line label size 22 that correctly wraps. I took a picture of it inside the 3D inspector. As you can see there is quite an extra bit of padding on the top and bottom above and below the label text. The label's 8 point spacing below the imageView is correct but the extra padding makes it look like 24 points of spacing.
The odd thing is even when I reduced the label's size to 16 the extra padding was still there.
How can I remove this padding?
The collectionView's sizeForItem where the estimatedLabelHeight function is called:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let profileImageViewHeight: CGFloat = 50 // imageView height is set to 50 inside the cell
let padding: CGFloat = 24 // 8 x 8 x 8 the vertical padding inside the cell between the titleLabel, the imageView, and the cell
// estimatedLabelHeight is called here
let titleLabelHeight = estimatedLabelHeight(text: "some very very long text...", width: view.frame.width - 20, font: UIFont.systemFont(ofSize: 22))
let totalHeightOfCell = titleLabelHeight + profileImageViewHeight + padding
return CGSize(width: view.frame.width, height: totalHeightOfCell)
}
The cell:
class MyCell: UICollectionViewCell{
let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 22)
label.textColor = UIColor.black
label.textAlignment = .left
label.sizeToFit()
label.numberOfLines = 0
return label
}()
let profileImageView: UIImageView = {
// created it...
}
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(profileImageView)
addSubview(titleLabel)
profileImageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8).isActive = true
profileImageView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10).isActive = true
profileImageView.widthAnchor.constraint(equalToConstant: 50).isActive = true
profileImageView.heightAnchor.constraint(equalToConstant: 50).isActive = true
titleLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 8).isActive = true
titleLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10).isActive = true
titleLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
titleLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8).isActive = true
// I tried to set the label's height using the below but the same padding issue occured
// let titleLabelHeight = estimatedLabelHeight(text: titleLabel.text!, width: self.frame.width - 20, font: UIFont.systemFont(ofSize: 22))
// titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: titleLabelHeight).isActive = true
}
}

The problem was due to my own carelessness. #CSmith tip of measuring up the height in sizeForItem and the cell's frame is what helped me narrow it down.
Inside the project inside the collectionView cell I had the the imageView's bottom anchor set to 8 instead of -8 and inside the collectionView's sizeForItem I had the total height of 24. There was a conflict because since I had a 8 inside the cell it should've been 16 inside the collectionView and that mismatch was somehow stretching out the label. Once I corrected it and changed the imageView's bottom anchor -8 everything matched up and the label's padding issue was resolved.

Related

Content of UITableViewCell gets clipped upon expanding the cell

I want to implement expandable cell with UILabel that would grow when user taps it. I set the constraint properly and modify the numberOfLines upon expanding so the size would be calculated correctly.
However, the cell grows in size properly but its content gets clipped off. When I start scrolling the content magically shows up. I have followed few tutorials and I have no idea where my mistake could lie. Please see the code below and GIF
Edit: Of course, I am returning the UITableView.automaticDimension as the height of the row
// Label configuration inside cell
private lazy var label: UILabel = {
let l = UILabel()
l.font = .systemFont(ofSize: 14, weight: .regular)
l.numberOfLines = 3
l.lineBreakMode = .byTruncatingTail
return l
}()
// Modifying this value should correctly resize the label
var isExpanded: Bool = false {
didSet {
label.numberOfLines = isExpanded ? 0 : 3
setNeedsLayout()
}
}
// Setting up constraints. I'm using SnapKit for making the constraints
func setupView() {
contentView.addSubview(label)
label.snp.makeConstraints { make in
make.center.equalToSuperview()
make.left.equalToSuperview().offset(15)
make.top.equalToSuperview().offset(4).priority(.high)
}
}
And this is the code inside view controller that manages the tableView
func didChangeInfoExpanded(at path: IndexPath) {
DispatchQueue.main.async {
guard let cell = self.tableView.cellForRow(at: path) as? InfoTableCell else {
return
}
cell.isExpanded.toggle()
cell.layoutIfNeeded()
UIView.transition(with: self.tableView, duration: 0.3, options: .transitionCrossDissolve, animations: {
self.tableView.beginUpdates()
self.tableView.endUpdates()
}, completion: nil)
/*
I have also tried reloading the row but it's made a glitchy animation and the content was still clipped
self.tableView.reloadRows(at: [path], with: .automatic)
*/
}
}
A common issue is that when we set the number of lines from Zero to 3, the text of the label does not smoothly animate to 3 lines... it "snaps" to 3 lines, and then the bottom of the label frame, and the cell height, animates. Not a great visual effect.
Here's the best result I've gotten for this type of expand / collapse cell...
To the cell's contentView we add:
hiddenLabel ... a UILabel that will be hidden
container ... a UIView to hold the visible label
Then we add to the container view:
visibleLabel ... a UILabel
Both labels get the same text.
We constrain the hiddenLabel to all 4 sides of the content view (using layout margins guide). When we change hiddenLabel's number of lines, that will determine the height of the cell.
We also constrain container to all 4 sides of the content view. When the content view changes height, that will change the height of the container.
Inside the container, we constrain visibleLabel only to Top / Leading / Trailing... so when it has number of lines set to Zero, it will extend outside the bounds of the container (but we won't see that, because container has .clipsToBounds = true).
This gives us a smooth expand/collapse animation, with the text in the label being "revealed" / "covered".
So, the cell class looks like this:
class ExpandCell: UITableViewCell {
static let cellID: String = "expandCell"
let container = UIView()
let visibleLabel = UILabel()
let hiddenLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
[hiddenLabel, visibleLabel, container].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
contentView.addSubview(hiddenLabel)
contentView.addSubview(container)
container.addSubview(visibleLabel)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
// constrain hiddenLabel Top / Leading / Trailing to contentView
hiddenLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
hiddenLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
hiddenLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
// use less than or equal for bottom constraint to avoid auto-layout warnings
hiddenLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
// constrain container Top / Leading / Trailing / Bottom to contentView
container.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
container.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
container.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
container.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
// constrain theLabel Top / Leading / Trailing to container
visibleLabel.topAnchor.constraint(equalTo: container.topAnchor, constant: 0.0),
visibleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0.0),
visibleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0.0),
// NO bottom constraint for theLabel
])
// prevent theLabel from being visible outside the container
container.clipsToBounds = true
// label properties
[hiddenLabel, visibleLabel].forEach {
$0.font = .systemFont(ofSize: 14, weight: .regular)
$0.numberOfLines = 3
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.setContentHuggingPriority(.required, for: .vertical)
$0.contentMode = .top
}
// hide the hidden label
hiddenLabel.isHidden = true
// during development, so we can easily see frames
//visibleLabel.backgroundColor = .cyan
}
func setText(_ str: String, expanded: Bool) -> Void {
hiddenLabel.text = str
visibleLabel.text = str
hiddenLabel.numberOfLines = expanded ? 0 : 3
visibleLabel.numberOfLines = hiddenLabel.numberOfLines
}
func toggleExpanded() -> Bool {
visibleLabel.numberOfLines = 0
hiddenLabel.numberOfLines = hiddenLabel.numberOfLines == 0 ? 3 : 0
return hiddenLabel.numberOfLines == 0
}
}
In cellForRowAt we set it up (for example):
if indexPath.row == 1 {
let c = tableView.dequeueReusableCell(withIdentifier: ExpandCell.cellID, for: indexPath) as! ExpandCell
// set both hidden and visible label text
c.setText(detailString)
c.selectionStyle = .none
return c
}
Then, in didSelectRowAt we can toggle the expanded / collapsed state with animation:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let c = tableView.cellForRow(at: indexPath) as? ExpandCell {
tableView.performBatchUpdates({
c.visibleLabel.numberOfLines = 0
c.toggleExpanded()
}, completion: { _ in
// we need to update the number of lines for the visible label
// so we get the ellipses when we're showing the collapsed state
c.visibleLabel.numberOfLines = c.hiddenLabel.numberOfLines
})
}
}
Result:
For now I'll just modify the contentOffset a bit to simulate scrolling, but I'm curious why that issue happens.
self.tableView.beginUpdates()
self.tableView.endUpdates()
self.tableView.contentOffset.y += 0.2
0.1 did not work, 0.2 was the smallest value that caused the content to appear. Hooray UIKit

Swift - How to set custom view by text length

Here is my code,
func bannerNotification(text: String){
let container = UIView()
let image = UIImageView()
let label = UILabel()
container.frame = CGRect(x: 0, y:0, width: self.view.frame.size.width, height: 100)
container.backgroundColor = .blue
image.frame = CGRect(x: 15, y: 50, width: 30, height: 30)
image.image = UIImage(named: "passport")
label.frame = CGRect(x: image.bounds.maxX + 35, y: 50, width: container.frame.size.width - 100, height: 50)
label.backgroundColor = .red
label.numberOfLines = 0
label.font = UIFont(name:"Helvetica Neue", size: 15)
label.text = text
container.addSubview(image)
container.addSubview(label)
self.view.addSubview(container)
}
According to this code the container and Image is coming on right position but if I pass small text so my text is not inline with Image, means my image top position and text top position should be same.
If I will pass a big text so container bottom and Label bottom should be same and all text should be there not truncated and Image and Label should be inline from the top.
You really want to use auto-layout for this, particularly since you're using multi-line label -- meaning, it will vary in height.
Here's an example - read through the comments to understand the auto-layout constraints:
class KingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
bannerNotification(text: "Banner Test")
//bannerNotification(text: "Banner Test with lots of text to wrap onto multiple lines. Of the many advantages with using auto-layout, notice that the banner will stretch when you rotate the device.")
}
func bannerNotification(text: String){
let container = UIView()
let image = UIImageView()
let label = UILabel()
container.addSubview(image)
container.addSubview(label)
self.view.addSubview(container)
[image, label, container].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
container.backgroundColor = UIColor(red: 0.0, green: 0.5, blue: 1.0, alpha: 1.0)
//image.image = UIImage(named: "passport")
image.image = UIImage(named: "swiftRed")
label.backgroundColor = .yellow
label.numberOfLines = 0
label.font = UIFont(name:"Helvetica Neue", size: 15)
label.text = text
// respect the safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain container Top / Leading / Trailing
container.topAnchor.constraint(equalTo: view.topAnchor),
container.leadingAnchor.constraint(equalTo: view.leadingAnchor),
container.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// we'll use the label height to determine the container height
// image view Top = 8 / Leading = 15
image.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
image.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 15.0),
// image view width = 30 / height == width (1:1 ratio)
image.widthAnchor.constraint(equalToConstant: 30.0),
image.heightAnchor.constraint(equalTo: image.widthAnchor),
// label Top aligned to Top of image view
label.topAnchor.constraint(equalTo: image.topAnchor),
// label Leading == image view Trailing + 20
label.leadingAnchor.constraint(equalTo: image.trailingAnchor, constant: 20.0),
// label Trailing = container Trailing - 15
label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -15.0),
// container bottom should be
// At Least 8-pts below the image view bottom
// AND
// At Least 8-pts below the label bottom
container.bottomAnchor.constraint(greaterThanOrEqualTo: image.bottomAnchor, constant: 8.0),
container.bottomAnchor.constraint(greaterThanOrEqualTo: label.bottomAnchor, constant: 8.0),
])
}
}
Result, with short text:
Result with long text:
Note that it auto-adjusts when the size changes - such as with device rotation:

Multiline UILabel on navigation bar title view

I have a custom UIView which has 2 UILabel (same width) and one UIImageView (known width of 44pt). Width sizes are given as an example, they can change but It is exact that UILabels should has same width and UIImage has a 44 point width. I want to add this view to UINavigationBar' titleView BUT ImageView should be in the center of navigation bar.
(60 width) UILabel---UIImageView (44 width) ---UILabel (60 width)
I want is that UILabels to have maximum two line and adJustFontSizeToFitWidth true. I'm giving specific width and height to title view but labels get two line but their font size doesn't change even they don't fit the view.
How I add titleView:
navigationItem.titleView = myTitleView
let widthOfItem: CGFloat = 30.0
let pading: CGFloat = 40
let aWidth: CGFloat = (self.navigationController?.navigationBar.frame.width)! - CGFloat(1) * widthOfItem * 2.0 - pading
myTitleView { (make) in
make.width.equalTo(aWidth)
make.height.equalTo(44)
}
MyCustomView:
override func layoutSubviews() {
super.layoutSubviews()
let preferredWidth = (bounds.width / 2) - 56
firstLabel.preferredMaxLayoutWidth = preferredWidth
secondLabel.preferredMaxLayoutWidth = preferredWidth
}
private func setupViews() {
addSubview(firstLabel)
addSubview(myImageView)
addSubview(secondLabel)
firstLabel.font = .myFont(.bold, size: 36)
firstLabel.adjustsFontSizeToFitWidth = true
firstLabel.minimumScaleFactor = 0.5
firstLabel.textColor = .textPrimary
firstLabel.numberOfLines = 2
firstLabel.lineBreakMode = .byWordWrapping
firstLabel.textAlignment = .right
myImageView.contentMode = .scaleAspectFit
myImageView.clipsToBounds = true
myImageView.layer.minificationFilter = .trilinear
myImageView.layer.cornerRadius = currencyImageSize.height / 2
secondLabel.font = . myFont(.bold, size: 36)
secondLabel.translatesAutoresizingMaskIntoConstraints = false
secondLabel.textColor = .textPrimary
secondLabel.numberOfLines = 2
secondLabel.adjustsFontSizeToFitWidth = true
secondLabel.baselineAdjustment = .none
secondLabel.minimumScaleFactor = 0.5
secondLabel.lineBreakMode = .byWordWrapping
secondLabel.textAlignment = .left
firstLabel.snp.makeConstraints { (make) in
make.leading.equalToSuperview()
make.top.bottom.equalToSuperview()
make.trailing.equalTo(myImageView.snp.leading).offset(-12)
}
myImageView.snp.makeConstraints { (make) in
make.height.equalToSuperview()
make.width.equalTo(myImageView.snp.height)
make.centerX.equalToSuperview()
}
secondLabel.snp.makeConstraints { (make) in
make.top.bottom.equalToSuperview()
make.leading.equalTo(myImageView.snp.trailing).offset(12)
make.trailing.equalToSuperview()
}
}
Maybe this code will work?
class CustomNavClass: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setup_view()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup_view()
}
private func setup_view() {
backgroundColor = .lightGray
let image = UIImageView(image: .init())
addSubview(image)
image.translatesAutoresizingMaskIntoConstraints = false
image.backgroundColor = .green
NSLayoutConstraint.activate([
image.centerXAnchor.constraint(equalTo: centerXAnchor),
image.widthAnchor.constraint(equalToConstant: 44),
image.heightAnchor.constraint(equalToConstant: 44),
image.topAnchor.constraint(equalTo: topAnchor, constant: 3),
image.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -3)
])
let leftLabel = UILabel()
leftLabel.text = "Label label left long label will go here, naturally"
leftLabel.numberOfLines = 2
leftLabel.adjustsFontSizeToFitWidth = true
leftLabel.backgroundColor = .red
addSubview(leftLabel)
leftLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
leftLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
leftLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
leftLabel.trailingAnchor.constraint(equalTo: image.leadingAnchor, constant: -10)
])
let rightLabel = UILabel()
rightLabel.text = "Label label right long label will go here, naturally"
rightLabel.numberOfLines = 2
rightLabel.adjustsFontSizeToFitWidth = true
rightLabel.backgroundColor = .blue
addSubview(rightLabel)
rightLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
rightLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
rightLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
rightLabel.leadingAnchor.constraint(equalTo: image.trailingAnchor, constant: 10)
])
}
}
The code does not use a UIStackView as its limited to its function (specifically, only one UIView can be stretched out on .fill distribution property.
This instead creates layout constraints:
UIImageView that is pegged at the center(with a 3 top and bottom constraint, optional, you can use centerY constraint or the like).
Leading UILabel is pegged to leading edge and trailing edge with UIImageView's leading edge.
Trailing UILabel is pegged to trailing edge and leading edge with UIImageView's trailing edge.

Center UIImageView within a UIScrollView with a larger contentSize

I have a full screen scrollView, to which I add an imageView as subview. I want the imageView to be centered and scaled filling the scrollView's size (that is the screen size) at the beginning, but then to allow the user to scroll the image in both directions (vertical and horizontal) with equal offsets at left, right, top and bottom.
I mean: I've set the scroll view's contentSize to be CGSize(width: screenWidth + 200, height: screenHeight + 200), and if I run the app, I see that I am able to scroll those 200 pts of offset only to the right and to the bottom of the image. I'd like the image to be centered in the content size, and to be able to scroll it horizontally to both to the left and to the right with offset 100 pts each side (similar thing with top and bottom when scrolling vertically).
How could I achieve this?
Note: I'm setting all the UI in code, I'm not using storyboards nor xib files
You may find it easier / more intuitive to use constraints and auto-layout rather than screenWidth and screenHeight:
//
// CenteredScrollViewController.swift
// SW4Temp
//
// Created by Don Mag on 4/18/18.
//
import UIKit
class CenteredScrollViewController: UIViewController {
let theScrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.green
return v
}()
let theImageView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.blue
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add the scrollView to the main view
view.addSubview(theScrollView)
// add the imageView to the scrollView
theScrollView.addSubview(theImageView)
// pin the scrollView to all four sides
theScrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0).isActive = true
theScrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0).isActive = true
theScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0).isActive = true
theScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0).isActive = true
// constrain the imageView's width and height to the scrollView's width and height
theImageView.widthAnchor.constraint(equalTo: theScrollView.widthAnchor, multiplier: 1.0).isActive = true
theImageView.heightAnchor.constraint(equalTo: theScrollView.heightAnchor, multiplier: 1.0).isActive = true
// set the imageView's top / bottom / leading / trailing anchors
// this *also* determines the scrollView's contentSize (scrollable area)
// with 100-pt padding on each side
theImageView.topAnchor.constraint(equalTo: theScrollView.topAnchor, constant: 100.0).isActive = true
theImageView.bottomAnchor.constraint(equalTo: theScrollView.bottomAnchor, constant: -100.0).isActive = true
theImageView.leadingAnchor.constraint(equalTo: theScrollView.leadingAnchor, constant: 100.0).isActive = true
theImageView.trailingAnchor.constraint(equalTo: theScrollView.trailingAnchor, constant: -100.0).isActive = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// set the scrollView's contentOffset (to center the imageView)
theScrollView.contentOffset = CGPoint(x: 100, y: 100)
}
}
You can move only down and right because your current content offset is 0,0 so top left - thus you can move down 200 and right 200.
What you want is to be scrolled 1/2 of vertical padding and 1/2 of horizontal padding, so in your case you would do scrollView.contentOffset = CGPoint(x: 100, y: 100)
Also for everything to work, UIImageView has to be same size as scrollView's contentSize, so bigger than screen size.
Given the comments what I think you want is the image to fill the screen and then user could scroll outside of bounds of the image, then you just need to make UIImageView's size be size of the screen its x and y coordinates to be same as contentOffset of the scrollView so (100, 100).
Here is the video of the sample app doing this:
https://dzwonsemrish7.cloudfront.net/items/2v361r2p0O2j1D3x3W10/Screen%20Recording%202018-04-19%20at%2002.32%20PM.mov
try this in
Swift 4.* or 5.*
let maxScale = self.imageScrollView.maximumZoomScale
let minScale = self.imageScrollView.minimumZoomScale
if let imageSize = imageView.image?.size{
let topOffset: CGFloat = (boundsSize.height - minScale * imageSize.height ) / 2
let leftOffset: CGFloat = (boundsSize.width - minScale * imageSize.width ) / 2
self.imageScrollView.contentInset = UIEdgeInsets(top: topOffset, left: leftOffset, bottom: 0, right: 0)
}

How to size a UIScrollView to fit an unknown amount of text in a UILabel?

I have added a scrollview subview in one of my views, but am having trouble getting it's height to accurately fit the content that the scrollview is showing, which is text in the UILabel. The height needs to be dynamic (i.e. a factor of the text length), because I am instantiating this view for many different text lengths. Whenever I log label.frame.bounds I get (0,0) back. I have also tried sizeToFits() in a few places without much luck.
My goal is to get the scrollview to end when it reaches the last line of text. Also, I am using only programmatic constraints.
A condensed version of my code is the following:
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
let scrollView = UIScrollView()
let containerView = UIView()
let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
// This needs to change
scrollView.contentSize = CGSize(width: 375, height: 1000)
scrollView.addSubview(containerView)
view.addSubview(scrollView)
label.text = unknownAmountOfText()
label.backgroundColor = .gray
containerView.isUserInteractionEnabled = true
containerView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.frame = view.bounds
containerView.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
}
}
Any help is appreciated.
SOLUTION found:
func heightForLabel(text: String, font: UIFont, lineHeight: CGFloat, width: CGFloat) -> CGFloat {
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = text
label.setLineHeight(lineHeight: lineHeight)
label.sizeToFit()
return label.frame.height
}
I found this solution online, that gives me what I need to set the appropriate content size for the scrollView height based on the label's height. Ideally, I'd be able to determine this without this function, but for now I'm satisfied.
The key to UIScrollView and its content size is setting your constraints so that the actual content defines the contentSize.
For a simple example: say you have a UIScrollView with width: 200 and height: 200. Now you put a UIView inside it, that has width: 100 and height: 400. The view should scroll up and down, but not left-right. You can constrain the view to 100x400, and then "pin" the top, bottom, left and right to the sides of the scroll view, and AutoLayout will "auto-magically" set the scrollview's contentSize.
When you add subviews that can change size - either explicitly (code, user interaction) or implicitly - if the constraints are set correctly those changes will also "auto-magically" adjust the scrollview's contentSize.
So... here is an example of what you are trying to do:
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
let scrollView = UIScrollView()
let label = UILabel()
let s1 = "1. This is the first line of text in the label. It has words and punctuation, but no embedded line-breaks, so what you see here is normal UILabel word-wrapping."
var counter = 1
override func viewDidLoad() {
super.viewDidLoad()
// turn off translatesAutoresizingMaskIntoConstraints, because we're going to set them
scrollView.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
// set background colors, just so we can see the bounding boxes
self.view.backgroundColor = UIColor(red: 1.0, green: 0.7, blue: 0.3, alpha: 1.0)
scrollView.backgroundColor = UIColor(red: 0.8, green: 0.8, blue: 1.0, alpha: 1.0)
label.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
// add the label to the scrollView, and the scrollView to the "main" view
scrollView.addSubview(label)
self.view.addSubview(scrollView)
// set top, left, right constraints on scrollView to
// "main" view + 8.0 padding on each side
scrollView.topAnchor.constraint(equalTo: self.topLayoutGuide.bottomAnchor, constant: 8.0).isActive = true
scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 8.0).isActive = true
scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8.0).isActive = true
// set the height constraint on the scrollView to 0.5 * the main view height
scrollView.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.5).isActive = true
// set top, left, right AND bottom constraints on label to
// scrollView + 8.0 padding on each side
label.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 8.0).isActive = true
label.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 8.0).isActive = true
label.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -8.0).isActive = true
label.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -8.0).isActive = true
// set the width of the label to the width of the scrollView (-16 for 8.0 padding on each side)
label.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -16.0).isActive = true
// configure label: Zero lines + Word Wrapping
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = UIFont.systemFont(ofSize: 17.0)
// set the text of the label
label.text = s1
// ok, we're done... but let's add a button to change the label text, so we
// can "see the magic" happening
let b = UIButton(type: UIButtonType.system)
b.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(b)
b.setTitle("Add a Line", for: .normal)
b.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 24.0).isActive = true
b.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
b.addTarget(self, action: #selector(self.btnTap(_:)), for: .touchUpInside)
}
func btnTap(_ sender: Any) {
if let t = label.text {
counter += 1
label.text = t + "\n\n\(counter). Another line"
}
}
}
give top,left,right and bottom constraint to label with containerView.
and
set label.numberOfLines = 0
also ensure that you have given top, left, right and bottom constraint to containerView. this will solve your issue
Set the auto layout constraints from the interface builder as shown in image .
enter image description here
I set the height of UIScrollView as 0.2 of the UIView
Then drag the UIlabel from MainStoryBoard to the view controller.
Add this two lines in viewdidload method.
draggedlabel.numberOfLines = 0
draggedlabel.lineBreakMode = NSLineBreakMode.byWordWrapping

Resources