Layout problems after replacing UILabel with UITextView in a UITableViewCell - ios

I've got basic chat functionality as part of an App I'm building. It is basically a UITable View where the UITableViewCell only contains a UILabel (the chat message text) and a UIView (serving as a speech bubble, surrounding the text. Here's the code:
class ChatMessageViewCellController: UITableViewCell {
var ChatMessageText = UILabel()
var ChatBubble = UIView()
var leadingConstraint: NSLayoutConstraint!
var trailingConstraint: NSLayoutConstraint!
var isIncoming: Bool! {
didSet {
if self.isIncoming {
self.ChatBubble.backgroundColor = UIColor(named: "customGrey")
self.leadingConstraint.isActive = true
self.trailingConstraint.isActive = false
} else {
self.ChatBubble.backgroundColor = UIColor(named: "customGreen")
self.leadingConstraint.isActive = false
self.trailingConstraint.isActive = true
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
addSubview(ChatBubble)
addSubview(ChatMessageText)
self.ChatBubble.translatesAutoresizingMaskIntoConstraints = false
self.ChatMessageText.translatesAutoresizingMaskIntoConstraints = false
self.ChatBubble.backgroundColor = UIColor(named: "customGreen")
self.ChatBubble.layer.cornerRadius = 10
self.ChatMessageText.numberOfLines = 0
self.ChatMessageText.textColor = .white
self.ChatMessageText.font = UIFont.systemFont(ofSize: 15, weight: UIFont.Weight.light)
let constraints = [
self.ChatMessageText.topAnchor.constraint(equalTo: topAnchor, constant: 16),
self.ChatMessageText.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -32),
self.ChatMessageText.widthAnchor.constraint(lessThanOrEqualToConstant: 220),
self.ChatBubble.topAnchor.constraint(equalTo: ChatMessageText.topAnchor, constant: -16),
self.ChatBubble.trailingAnchor.constraint(equalTo: ChatMessageText.trailingAnchor, constant: 16),
self.ChatBubble.bottomAnchor.constraint(equalTo: ChatMessageText.bottomAnchor, constant: 16),
self.ChatBubble.leadingAnchor.constraint(equalTo: ChatMessageText.leadingAnchor, constant: -16),
]
NSLayoutConstraint.activate(constraints)
self.leadingConstraint = self.ChatMessageText.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 32)
self.trailingConstraint = self.ChatMessageText.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32)
}
My problem is this:
I'm not feeding the UILabel with standard strings but with NSAttributedStrings, as I'd like to get some of the links in there clickable and parts of the text selectable by the user.
So I've been told to use a UITextView instead of the UILabel. I've thus made the following 2 changes:
Changed var ChatMessageText = UILabel()to var ChatMessageText = UITextView()
Did remove self.ChatMessageText.numberOfLines = 0 as UITextView doesn't have a numberOfLines member
Xcode doesn't complain and the app compiles and runs but it completely messes with my layout and I just can't figure out why. All the constraints from the UILabel should also work for the UITextView - at least I thought so. But here's how the screen looks like.
What am I doing wrong? Do I need to add / alter constraints?

By default, a UITextView has scrolling enabled.
While this seems obvious, that allows the user to enter more lines of text than will fit in the frame, and the user can scroll the text up and down.
In order for this to happen, UIKit has to know the frame of the text view. If the frame is not set, UIKit has no way to know how many lines to display, or how wide the view should be. So unless we have given the text view a full set of constraints, auto-layout will give it a size of .zero. Even if given a width (or max width) constraint, auto-layout still doesn't know how many scrollable lines of text we want displayed.
Setting .isScrollEnabled = false on the text view changes all of that.
Now, if we only constrained the position and width of the text view, UIKit will calculate the height based on the content size of the .text property.
This can be easily demonstrated. We'll create two text views, give them each top, leading and max-width (lessThanOrEqualTo) constraints, and the same text... but set .isScrollEnabled = false on one of them:
class TextViewTestViewController: UIViewController {
let nonScrollingTextView = UITextView()
let scrollingTextView = UITextView()
override func viewDidLoad() {
super.viewDidLoad()
let s = "This is a test string to demonstrate UITextView size behavior."
[nonScrollingTextView, scrollingTextView].forEach {
tv in
tv.translatesAutoresizingMaskIntoConstraints = false
tv.font = UIFont.systemFont(ofSize: 17.0)
tv.text = s
view.addSubview(tv)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
nonScrollingTextView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
nonScrollingTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
nonScrollingTextView.widthAnchor.constraint(lessThanOrEqualToConstant: 300.0),
scrollingTextView.topAnchor.constraint(equalTo: nonScrollingTextView.bottomAnchor, constant: 40.0),
scrollingTextView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
scrollingTextView.widthAnchor.constraint(lessThanOrEqualToConstant: 300.0),
])
// disable scrolling on the "top" text view
nonScrollingTextView.isScrollEnabled = false
// top text view is cyan
nonScrollingTextView.backgroundColor = .cyan
// bottom text view is green (although we won't see it)
scrollingTextView.backgroundColor = .green
}
}
Result:
We've added two text views, but only disabled scrolling on the "top" one (cyan background). We don't even see the second one (green background), because auto-layout gives it a height of Zero.
Worth noting... if the text view has scrolling disabled and has editing enabled, it will automatically grow / shrink as the user adds / deletes text.

Related

Setting a bottom constraint to a UITextView if needed

I'm trying to fix an issue with a UItextview which I placed at the bottom of a viewcontroller programmatically and sometimes it can clip through the bottom of the view if I don't set a constraint like so.
https://i.stack.imgur.com/YfyPi.png
Whenever I try to constraint the textview to the bottom of the safe area, the text needlessly expands too much if there's less text.
https://i.stack.imgur.com/8w1v2.png
Here's the relevant code snippets from the textview and the constraints respectively:
private let summaryTextView: UITextView = {
let summaryTextView = UITextView()
summaryTextView.textColor = .label
summaryTextView.backgroundColor = .customWhite
summaryTextView.textAlignment = .center
summaryTextView.font = UIFont.systemFont(ofSize: 24)
summaryTextView.clipsToBounds = true
summaryTextView.layer.cornerRadius = 20
summaryTextView.layer.masksToBounds = true
summaryTextView.isSelectable = false
summaryTextView.isEditable = false
summaryTextView.isScrollEnabled = false
summaryTextView.translatesAutoresizingMaskIntoConstraints = false
return summaryTextView
}()
private func setupConstraints() {
NSLayoutConstraint.activate([
imageContainerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 120),
imageContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
imageContainerView.heightAnchor.constraint(equalToConstant: 280),
imageContainerView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.4),
summaryTextView.topAnchor.constraint(equalTo: imageContainerView.bottomAnchor,constant: 15),
summaryTextView.leadingAnchor.constraint(equalTo: view.leadingAnchor,constant: 10),
summaryTextView.trailingAnchor.constraint(equalTo: view.trailingAnchor,constant: -10),
summaryTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
backgroundImage.fillSuperView(to: view)
bookCover.fillSuperView(to: imageContainerView)
}
Any help would be appreciated!
You need to set the height constraint for UITextView(). Because you are giving fixed top anchor and and bottom anchor so it stretches the textview.

Align UIButton and UILabel text with different font sizes

I have a UIButton and a UILabel displayed inline. They have different size fonts, however I would like to align them so they appear on the same line.
At the moment the UILabel is slight above the baseline of the UIButton.
I was hoping to avoid manually setting a content offset as I want this to scale correctly where possible. I worry manual calculations may have unexpected side effects on changing font sizes etc.
I have created a playground that should show the 2 elements:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
lazy var nameButton = configure(UIButton(type: .system), using: {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.titleLabel?.font = .systemFont(ofSize: 18)
$0.setTitleColor(.darkGray, for: .normal)
$0.contentHorizontalAlignment = .leading
$0.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .horizontal)
$0.backgroundColor = .lightGray
$0.setTitle("This is a button", for: .normal)
})
lazy var publishedDateLabel = configure(UILabel(frame: .zero), using: {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.font = .systemFont(ofSize: 14)
$0.textColor = .darkGray
$0.setContentHuggingPriority(UILayoutPriority.defaultLow, for: .horizontal)
$0.backgroundColor = .lightGray
$0.text = "and this is a label"
})
override func loadView() {
let view = UIView()
view.backgroundColor = .white
[nameButton, publishedDateLabel].forEach(view.addSubview(_:))
NSLayoutConstraint.activate([
nameButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
nameButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
publishedDateLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
publishedDateLabel.leadingAnchor.constraint(equalTo: nameButton.trailingAnchor),
publishedDateLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8)
])
self.view = view
}
// setup helper method
func configure<T>(_ value: T, using closure: (inout T) throws -> Void) rethrows -> T {
var value = value
try closure(&value)
return value
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
I have tried making the label and button the same height by adding publishedDateLabel.heightAnchor.constraint(equalTo: nameButton.heightAnchor)
This didn't change the alignment however.
I also tried using publishedDateLabel.lastBaselineAnchor.constraint(equalTo: nameButton.lastBaselineAnchor)
to align the anchors however this aligned the top of the elements
How can align the bottom of the text in the button to the bottom of the text in the label?
Just comment out the heightAnchor use the lastBaselineAnchor:
NSLayoutConstraint.activate([
nameButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
nameButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
publishedDateLabel.lastBaselineAnchor.constraint(equalTo: nameButton.lastBaselineAnchor),
publishedDateLabel.leadingAnchor.constraint(equalTo: nameButton.trailingAnchor),
publishedDateLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8)
])

Is view.leadingAnchor.constraint higher precedence than view.trailingAnchor.constraint?

I have my ViewController code as below
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let titleLabel = UILabel()
titleLabel.text = "Hello World!"
view.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
titleLabel.topAnchor.constraint(equalTo: view.topAnchor),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
titleLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
}
I'm expecting my "Hello World!" to be center aligned. Why wasn't it center aligned? (Vertically it is centered, but horizontally it is aligned to left as shown below.)
p/s: If I remove itleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor), then it is aligned right.
Do you want your label to cover the entire view with the text centered? If so, keep your constraints as they are and set .textAlignment = .center.
I've set the background color of the label to cyan so you can see what's happening:
However, I suspect you just want your label centered in the view. In that case, change your constraints to this:
NSLayoutConstraint.activate([
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
Result:
Until and unless we are not changing priority of constraint the default priority is 1000 for all. So no precedence setting automatically.
For your code, add this line and it will work.
titleLabel.textAlignment = .center
In iOS 9 and later, the default value of textAlignment property of UILabel is NSTextAlignment.natural, prior to iOS 9, the default value was NSTextAlignment.left
If you want your label to cover the entire view with the text-centered then your constraints are perfect, just set
titleLabel.textAlignment = .center

Setting Image and Text to Button with AutoLayout

I'm trying to create a button with a drop down arrow to the right of the text programatically like so:
The solutions I've seen have used title and image insets, but is there a way to set these with autoLayout programatically? Depending on the option selected, the text in the button could change and the text lengths will be different, so I'm not sure if title and edge insets are the way to go.
This is an example of where a UIStackView is placed in the main VC container view (in my case the UIStackView takes up all available space inside the VC). Basic user information is added in this case a telephone number.
I create a telephone number container view (UIView), a UILabel to contain the tel. no. and an UIImageView for the drop down arrow.
let telNoContainerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let telNoLabel: UILabel = {
let view = UILabel()
let font = UIFont.systemFont(ofSize: 15)
view.font = font
view.backgroundColor = UIColor.white
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let telNoImageView: UIImageView = {
let view = UIImageView()
view.backgroundColor = UIColor.white
view.tintColor = ACTION_COLOR
view.image = UIImage(named: "Chevron")?.withRenderingMode(.alwaysTemplate)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
In setBasicInfoViews() simply add the telNoContainerView to he UIStackView. Then the UILabel and the UIImageView are added to the contain view telNoContainerView. Afterward the constraints are added as needed.
You will need to change the constraints to fit your UI design.
fileprivate func setBasicInfoViews(){
infoStackView.addArrangedSubview(telNoContainerView)
telNoContainerView.addSubview(telNoLabel)
telNoContainerView.addSubview(telNoImageView)
NSLayoutConstraint.activate([
telNoLabel.topAnchor.constraint(equalTo: telNoContainerView.topAnchor, constant: 0.0),
telNoLabel.bottomAnchor.constraint(equalTo: telNoContainerView.bottomAnchor, constant: 0.0),
telNoLabel.leadingAnchor.constraint(equalTo: telNoContainerView.leadingAnchor, constant: 0.0),
telNoLabel.trailingAnchor.constraint(equalTo: telNoContainerView.trailingAnchor, constant: 0.0)
])
NSLayoutConstraint.activate([
telNoImageView.centerYAnchor.constraint(equalTo: telNoLabel.centerYAnchor, constant: 0.0),
telNoImageView.heightAnchor.constraint(equalToConstant: 30.0),
telNoImageView.widthAnchor.constraint(equalToConstant: 30.0),
telNoImageView.trailingAnchor.constraint(equalTo: telNoLabel.trailingAnchor, constant: 0.0)
])
}
No, there isn't a way to set the image and title layout properties on a UIButton using AutoLayout.
If you want a fully custom layout for an Image and Title in a UIButton, I would suggest creating a UIView and add a title and an image as subviews using AutoLayout and then add a tap gesture recognizer to the UIView
buttonView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.buttonAction)))

Swift: Programmatically set autolayout constraints for subviews don't resize view

I have the following setup: A UIView adds a bunch of subviews (UILabels) programmatically, and sets also the autolayout constraints, so that the distance between the labels and between the UIViews edges is 10 each. The goal is that the UIView sets its size accordingly to the content of all the subviews (labels with dynamic text) including the spaces.
I use the following code, but it seems not to work. The UIView doesn't resize, it shrinks the labels.
// setup of labelList somewhere else, containing the label data
var lastItemLabel: UILabel? = nil
var i = 1
for item in itemList {
let theLabel = UILabel()
// ... label setup with text, fontsize and color
myView.addSubview(theLabel)
theLabel.translatesAutoresizingMaskIntoConstraints = false
// If it is the second or more
if let lastLabel = lastItemLabel {
theLabel.leadingAnchor.constraint(equalTo: lastLabel.trailingAnchor, constant: 12).isActive = true
theLabel.topAnchor.constraint(equalTo: myView.topAnchor, constant: 10).isActive = true
// if it is the last label
if i == labelList.count {
theLabel.trailingAnchor.constraint(equalTo: myView.trailingAnchor, constant: 12).isActive = true
}
}
// If it is the first label
else {
theLabel.leadingAnchor.constraint(equalTo: myView.leadingAnchor, constant: 12).isActive = true
theLabel.topAnchor.constraint(equalTo: myView.topAnchor, constant: 10).isActive = true
}
lastItemLabel = theLabel
i += 1
}
Since you need your content to be larger than the physical display of the device, you will need to add a UIScrollView to contain your labels.

Resources