Changing/Animating maximumNumberOfLines in UITextView? - ios

I have a UITextView and initially I want to set it to have 4 lines, however when the user clicks a read more button, I want to expand it to full length, I assume this is either through getting maximumNumberOfLines to 0 or to a high number, say 30
Issue is, after changing lines from 4 to 0 (or 30) it doesn't relayout the uitextview to be its full height, it seems capped at 4 lines.
I call self.setNeedsLayout() and self.layoutIfNeeded() to trigger layout, but it won't revert to its full height
I have also tried calling descriptionTextView.invalidateIntrinsicContentSize() after changing the line count with no luck
What am I doing wrong here?
Thanks

You may be doing a couple things wrong...
First, in order to "auto-size" the height of the textView, it must have scrolling disabled.
Second, it cannot have a fixed height (neither a height constraint not top & bottom constraints).
Edit: For clarification... When I say "no bottom constraint" that doesn't mean it cannot have a bottom constraint. Rather, the bottom constraint cannot be set in a way that would prevent the textView's height from changing. So, for example, if the textView is in a table view cell, a bottom constraint is fine, as long as the cell is designed and used in a way that the height of the textView controls (or contributes to) the height of the cell.
This is a simple example that will toggle the textView between 4-lines and Zero-lines (showing all the text content):
class ExpandingTextViewViewController: UIViewController {
let descriptionTextView: UITextView = {
let v = UITextView()
v.translatesAutoresizingMaskIntoConstraints = false
// disable scrolling
v.isScrollEnabled = false
// give it a background color to make it easy to see the frame
v.backgroundColor = .yellow
return v
}()
let theButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
v.setTitle("Toggle TextView", for: .normal)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(theButton)
view.addSubview(descriptionTextView)
NSLayoutConstraint.activate([
// button 40-pts from the top, centered horizontally
theButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
// textView 40-pts from bottom of button, 20-pts padding left and right
// NO height or bottom constraint
descriptionTextView.topAnchor.constraint(equalTo: theButton.bottomAnchor, constant: 40.0),
descriptionTextView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20.0),
descriptionTextView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20.0),
])
// give the textView some sample text
descriptionTextView.text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
// start with max number of lines set to 4
descriptionTextView.textContainer.maximumNumberOfLines = 4
theButton.addTarget(self, action: #selector(toggleTextView), for: .touchUpInside)
}
#objc func toggleTextView() -> Void {
// toggle max number of lines between 4 and Zero
descriptionTextView.textContainer.maximumNumberOfLines =
(descriptionTextView.textContainer.maximumNumberOfLines == 4) ? 0 : 4
// tell auto-layout abour the change
descriptionTextView.invalidateIntrinsicContentSize()
}
}
Results:
Of course, you'll need to add some code to handle a case where your textView has so much text it will extend beyond the bottom of the screen (or outside the bounds of its superview) -- either by checking the resulting height, adjusting it and toggling scrolling, or embedding the textView in a UIScrollView (for example).

Changing the number of lines on your textField just affects the number of lines your textField is allowed to use to display its text. Setting maximumNumberOfLines to 0 crams the text all on one "line" and truncates it at the end of its width - so it doesn't really hide the remaining text in that sense.
Instead of changing maximumNumberOfLines, you'd be best off letting the text fill the number of lines that is natural for it, and animating the UITextView's heightAnchor instead.

Related

How can I tell if an iPhone's home indicator is present or not in xcode? [duplicate]

This question already has answers here:
How to detect if the device (iphone) has physical home button?
(2 answers)
Closed 9 months ago.
I have placed a button at the bottom of the safe area.
On iPhone X and later devices, it is placed well above a certain margin, but in the case of an iPhone with a home button, it is attached to the bottom.
So, I would like to set a separate constraint only on iPhones with a home button to leave a certain margin.
iPhoneSE
iPhone 13 Pro
You don't need to detect the home button... in fact, that's probably not what you want to do anyway.
Another approach is to use two bottom constraints, with different priorities.
You can constrain the bottom of the button to the bottom of the safe area, and give that constraint a less-than-required priority.
You can then apply another constraint, this time at a minimum distance from the bottom of the view.
Here's an example:
class BottomPaddingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let safeAreaView = UIView()
safeAreaView.backgroundColor = .red
safeAreaView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(safeAreaView)
let btn = UIButton()
btn.backgroundColor = .systemBlue
btn.setTitle("Button", for: [])
btn.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn)
let g = view.safeAreaLayoutGuide
// bottom constraint for the button
// to the safe area bottom
// with less-than-required priority
let bConstraint = btn.bottomAnchor.constraint(equalTo: g.bottomAnchor)
bConstraint.priority = .required - 1
NSLayoutConstraint.activate([
// constrain the red view to the safe area bottom
safeAreaView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
safeAreaView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
safeAreaView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
safeAreaView.heightAnchor.constraint(equalToConstant: 40.0),
// constrain button 200-pts wide, centered horizontally
btn.widthAnchor.constraint(equalToConstant: 200.0),
btn.heightAnchor.constraint(equalToConstant: 40.0),
btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// and bottom AT LEAST 20-points from the bottom of the VIEW
btn.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -20.0),
// activate the button's safe area bottom constraint
bConstraint,
])
}
}
When running on a phone without a physical button (iPhone 12, for example) it looks like this:
we see that both the blue button and the red view are bottom-constrained to the safe area.
When running on a phone with a physical button (iPhone 8, for example) it looks like this:
the red view is still constrained to the safe area, but the blue button keeps its minimum distance from the view bottom.

How to wrap text in Swift 5

Brand new to Xcode and Swift, but I'm trying to have a headline and subheading for a news type app I'm prototyping out. I can't get the text to wrap, leaving out most of the headline and brief summary. I need it to say
Unity with purpose. Amanda Gorman and Michelle Obama Discuss Art Identity and Optimism
Amanda Gorman captivated the world when she read her poem “The Hill We
Climb” at President Joe Biden and Vice President Kamala Harris’ Jan.
20 Inauguration ceremony.
All I get is:
Here is the code generating the labels, this is the headline but they are basically the same:
private let headlineLabel: UILabel = {
let label = UILabel()
label.textAlignment = .left
label.textColor = .white
label.font = UIFont.preferredFont(forTextStyle: .largeTitle) //Scales font automatically
label.adjustsFontForContentSizeCategory = true //Adjusts font sized based on settings user has
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
return label
}()
I've tried setting numberOfLines = 0 as well as label.lineBreakMode = .byWordWrapping and neither seem to work.
How can I get the entire string for both the headline and subheading to be displayed?
Thanks!
The properties that you applied to your UILabel to allow wrapping looks correct, but
without seeing more of the code it's hard to tell. So I'm betting it's your horizontal layout constraints (if any). If you did not set an explicit frame to your view. Then ensure you're setting+activating vertical and horizontal constraints for your view.
// Important step to enable contraints
label.translatesAutoresizingMaskIntoConstraints = false
....
// Important that you add your view to the view hierarchy first before activating
// constraints. If not this will produce a runtime error.
self.view.addSubview(label)
....
NSLayoutConstraint.activate([
label.leadingAnchor.contraint(equalTo: self.view.leadingAnchor)
label.trailingAnchor.contraint(equalTo: self.view.trailingAnchor)
label.topAnchor.contraint(equalTo: self.view.topAnchor)
])
The .activate(:) is another important step and common pitfall. Ensure you're activating your contraints!
leadingAnchor is attaching the left side of your label's frame to the left side of your parent view.
trailingAnchor is doing the same thing, but for the right side.
Then finally we pin the label to the top of the parent view. You may want use .contraint(equalTo: .., constant: ..) to push your label down a little bit as well.
So, in the next update loop and when your parent view lays out the subviews. Your label's frame will be calculated based on your activated constraints. So in this case your label's frame will look something like this (x:0, y:0, width: 300, height: *height is intrinsic here) This is assuming a parent frame of (x: 0, y: 0, width: 300, height: 600)
You mentioned you're new to iOS, so I hope this info helps you in some way.

How do I offset UILabel text alignment?

I am trying to offset the center starting point for a UILabel. The problem that I am facing is that I can't make the label text to grow from a point that is offset from center until it reaches one end of the label. Then, I want the text to shift one character to the left with each additional added character until it reaches the the other end. Then it would be acceptable for the text to truncate with an ellipses.
So far, my code looks like this but I don't know where to go from here.
private let amountLabel: UILabel = {
let label = UILabel()
label.textColor = .blue
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
I suggest you put the UILabel into a container view, center horizontally with the offset you want, but set its priority lower than the compression resistance., e.g. 750, compression resistance 999. Then create a trailing constraint >= with priority 1000, and leading constraint >= priority 1000. In that way centering will be the weakest constraint and as the text grows it will shift to the left until it reaches the leading.

Stack view - but with "proportional" gaps

Imagine a stack view with four items, filling something. (Say, filling the screen).
Notice there are three gaps, ABC.
(Note - the yellow blocks are always some fixed height each.)
(Only the gaps change, depending on the overall height available to the stack view.)
Say UISV is able to draw everything, with say 300 left over. The three gaps will be 100 each.
In the example, 9 is left over, so A B and C are 3 each.
However.
Very often, you want the gaps themselves to enjoy a proportional relationship.
Thus - your designer may say something like
If the screen is too tall, expand the spaces at A, B and C. However. Always expand B let's say 4x as fast as the gaps at A and B."
So, if "12" is left over, that would be 2,8,2. Whereas when 18 is left over, that would be 3,12,3.
Is this concept available in stack view? Else, how would you do it?
(Note that recently added to stack view, you can indeed specify the gaps individually. So, it would be possible to do it "manually", but it would be a real mess, you'd be working against the solver a lot.)
You can achieve that by following workaround. Instead of spacing, for each space add a new UIView() that would be a stretchable space. And then just add constraints between heights of these "spaces" that would constrain their heights together based on the multipliers you want, so e.g.:
space1.heightAnchor.constraint(equalTo: space2.heightAnchor, multiplier: 2).isActive = true
And to make it work I think you'd have to add one constraint that would try to stretch those spaces in case there is free space:
let stretchingConstraint = space1.heightAnchor.constraint(equalToConstant: 1000)
// lowest priority to make sure it wont override any of the rest of constraints and compression resistances
stretchingConstraint.priority = UILayoutPriority(rawValue: 1)
stretchingConstraint.isActive = true
The "normal" content views would have to have intrinsic size or explicit constraints setting their heights to work properly.
Here is an example:
class MyViewController: UIViewController {
fileprivate let stack = UIStackView()
fileprivate let views = [UIView(), UIView(), UIView(), UIView()]
fileprivate let spaces = [UIView(), UIView(), UIView()]
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(stack)
// let stack fill the whole view
stack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: self.view.topAnchor),
stack.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
stack.leftAnchor.constraint(equalTo: self.view.leftAnchor),
stack.rightAnchor.constraint(equalTo: self.view.rightAnchor),
])
stack.alignment = .fill
// distribution must be .fill
stack.distribution = .fill
stack.spacing = 0
stack.axis = .vertical
for (index, view) in views.enumerated() {
stack.addArrangedSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
// give it explicit height (or use intrinsic height)
view.heightAnchor.constraint(equalToConstant: 50).isActive = true
view.backgroundColor = .orange
// intertwin it with spaces
if index < spaces.count {
stack.addArrangedSubview(spaces[index])
spaces[index].translatesAutoresizingMaskIntoConstraints = false
}
}
// constraints for 1 4 1 proportions
NSLayoutConstraint.activate([
spaces[1].heightAnchor.constraint(equalTo: spaces[0].heightAnchor, multiplier: 4),
spaces[2].heightAnchor.constraint(equalTo: spaces[0].heightAnchor, multiplier: 1),
])
let stretchConstraint = spaces[0].heightAnchor.constraint(equalToConstant: 1000)
stretchConstraint.priority = UILayoutPriority(rawValue: 1)
stretchConstraint.isActive = true
}
}
Remarkably, #MilanNosáľ 's solution works perfectly.
You do not need to set any priorities/etc - it works perfectly "naturally" in the iOS solver!
Set the four content areas simply to 50 fixed height. (Use any intrinsic content items.)
Simply don't set the height at all of "gap1".
Set gap2 and gap3 to be equal height of gap1.
Simply - set the ratios you want for gap2 and gap3 !
Versus gap1.
So, gap2 is 0.512 the height of gap1, gap3 is 0.398 the height of gap1, etc.
It does solve it in all cases.
Fantastic!!!!!!!!!!
So: in the three examples (being phones with three different screen heights). In fact the relative heights of the gaps, is always the same. Your design department will rejoice! :)
Created: a gist with a storyboard example
The key here is Equal Heights between your arranged views and your reference view:
And then change the 'Multiplier` to your desired sizes:
In this example I have 0.2 for the main view sizes (dark grey), 0.05 within the pairs (black), and 0.1 between the pairs (light grey)
Then simply changing the size of the containing view will cause the views to re-size proportionally:
This is entirely within the storyboard, but you could do the same thing in code.
Note that I'm using only proportions within the StackView to avoid having an incorrect total size, (and making sure they add up to 1.0), but it should be possible to also have some set heights within the StackView if done correctly.

Swift 3 UILabel get actual height (intrinsicContentSize)

I'm trying to simulate a kind of "Please wait..." UILabel. The label's text must be regularly updated. So far everything works as expected. However, I need to get the intrinsic content height of the label to be able to position its container view (UIView).
The label is the one with the red background, whereas the one with the white background is its container.
I've tried a few different approaches, unfortunately, all in vain. Any help would be greatly appreciated.
private func createBusyLabel(labelText: String) -> CGFloat {
self.busyViewContainer.addSubview(self.busyLabel)
self.busyLabel.backgroundColor = UIColor.red
self.busyLabel.numberOfLines = 0
self.busyLabel.lineBreakMode = NSLineBreakMode.byWordWrapping
self.busyLabel.sizeToFit()
//set the constraints, but skip height constraints
self.busyLabel.translatesAutoresizingMaskIntoConstraints = false
self.busyLabel.horizontalLeft(toItem: self.busyViewContainer, constant: 60)
self.busyLabel.horizontalRight(toItem: self.busyViewContainer, constant: -10)
self.busyLabel.topConstraints(toItem: self.busyViewContainer, constant: 10)
self.busyLabel.text = labelText
//calculate height with margin
let height: CGFloat = self.busyLabel.intrinsicContentSize.height + 20
return height
}
Also, the line counting function, from a previously asked and already answered question, delivers only 1
Here is what it look like after I set the bottom constraint:
A million Thanks to ozgur, who changed my approach. Ozgur, your code works perfect, but unfortunately not for me, as I faced problems with bottomLayoutGuide part. The reason for this is that the label and its container are created in an external class.
Previously I tried to set bottom constraint to the label, which did not return the expected result. However, inspired by ozgur's answer, this time I simply set the bottom constraint to its container and not the label, giving in expected result, like following:
self.busyViewContainer.bottomConstraints(toItem: self.busyLabel, constant: 10)
Thanks to all who put in their precious efforts.
private func createBusyLabel(labelText: String) -> Void {
self.busyLabel.text = labelText
self.busyLabel.font = UIFont.getGlobalFont(size: _textSizeSmall, type: "bold")
self.busyLabel.backgroundColor = UIColor.red
// handle multiline problem
self.busyLabel.numberOfLines = 0
self.busyLabel.lineBreakMode = NSLineBreakMode.byWordWrapping
self.busyLabel.sizeToFit()
self.busyViewContainer.addSubview(self.busyLabel)
self.busyLabel.translatesAutoresizingMaskIntoConstraints = false
self.busyLabel.horizontalLeft(toItem: self.busyViewContainer, constant: 60)
self.busyLabel.horizontalRight(toItem: self.busyViewContainer, constant: -10)
self.busyLabel.topConstraints(toItem: self.busyViewContainer, constant: 10)
// the following line made the difference
self.busyViewContainer.bottomConstraints(toItem: self.busyLabel, constant: 10)
}
The simple fix is adding the bottom constraint between 'self.busyViewContainer' and its superview.
Following your code and syntax it can be something like this:
self.busyLabel.bottomConstraints(toItem: self.busyViewContainer, constant: 10)
It is a common problem with 'Unsatisfiable Constraints'. The autolayout should ensure it satisfies horizontal axis layout so it needs to have both top and bottom constraints in this case.
Apple doc - Unsatisfiable Constraints
Apple doc - Logical Errors
UPD: In this case, the layout of the superview can define a height with intrinsicContentSize:
var intrinsicContentSize: CGSize { return ... }
Where the height of the parent view will be calculated based on the label one.

Resources