Prevent UITextView from offsetting its text container - ios

I am tying to modify the height of a UITextView dynamically (up to a max height) while the user enters text. I am experiencing a very strange behavior when there are an even number of lines in the text view.
I am using autolayout and the text view has a height constraint. I respond to calls to the text view's delegate (textViewDidChange(_:)), where I calculate and adjust the height constraint based on the contentSize.
Here is the code:
func textViewDidChange(_ textView: UITextView) {
let newHeight = textView.contentSize.height
let newConstraintConst = max(MinTextViewHeight, min(MaxTextViewHeight, newHeight))
self.textViewHeightConstraint.constant = newConstraintConst
}
This works well, it resizes the frame up to MaxTextViewHeight and then the text view can scroll. However, when there are an even number of lines in the text view, the text view adds a kind of offset to the bottom of its NSTextContainer, causing the top line to be cut off:
However, when there are odd lines the NSTextContainer is no longer offset:
At first I thought it was somehow being controlled by the text view's textContainerInset but that is only used to pad the space inside the NSTextContainer, as setting it to .zero removes the space inside but does not affect the offset (and incidentally makes it even worse, as the top line almost completely disappears):
I have looked through the UITextView class reference and I don't see any property that would let me manipulate or even get the value of this offset.
As a workaround I am increasing the text container's top inset and removing the bottom inset:
textView.textContainerInset = UIEdgeInsetsMake(10, 0, 0, 0)
This works so far, but I arrived at a value of 10 by trial-and-error, and so far I've only tested it on a single device.
I am not interested in more hacky workarounds that require fragile, fixed values; I am trying to understand how this offset is being set and a proper way to fix it. I'm hoping that someone can provide some insight, thanks!

Just a speculation, but I think the problem is that the text view assumes that the height of itself does not change while calling textViewDidChange, so it scrolls when it thinks it has to, regardless of you changing its frame.
Not sure if you think my solution is too hacky, but this will stop it from scrolling when you don't want it. I simply pin the content offset to the top as long as the wanted content size is smaller than your max size.
Just add this:
func scrollViewDidScroll(scrollView: UIScrollView) {
if textView.contentSize.height <= MaxTextViewHeight && textView.contentOffset.y > 0.0 {
textView.contentOffset.y = 0.0;
}
}

Related

How to dynamically size UIScrollView?

I'm adding content to the UIScrollView dynamically. The content size of the scroll view is increasing. How can I change the content size to fit the dynamically added content? See the code I'm using that is not working
extension UIScrollView {
func updateContentView() {
contentSize.height = subviews.sorted(by: { $0.frame.maxY < $1.frame.maxY }).last?.frame.maxY ?? contentSize.height
contentSize.height += 300
}
}
refer this question
I hope the above solves your problem but I'd prefer using a UITableView or a UICollectionView in place of this because everything is handled so smoothly in them. You just have to give the number of rows(return a variable which changes dynamically). That's it, your contentSize is adjusted internally.
But again if you have a different requirement please go with the reference link!

UITextField: adjustsFontSizeToFitWidth behaviour/ issue

I have an issue with the usage of textField’s adjustsFontSizeToFitWidth. I have a textField with font size of 50 and a maximum width of 300 (This is a less than or equal to constraint). TextField is placed centre vertically and horizontally using auto layout. Now when I run the app, the text is being shrinked from the beginning itself. I know this may be because the system thinks the bounds of Texfield is not enough… but I gave the control to auto layout to figure that out and I assume it should work. Below is the snapshot… you can see the big placeholder and then suddenly the text shrinks… Any thoughts? Am I doing something wrong, I was trying to avoid manual calculation of width..
SourceCode Sample
Set leading/trailing space to text field like 15 pt from left safe area and 15 from Button so that it could automatically increase the font size. As now it shrinks the font to minimal 17 (which is set in storyboard) as the width is not enough.
Okay!! Finally I found a way, the idea was to check if the width is greater than the defined width limit... when the width is more I set adjustsFontSizeToFitWidth = true else set to false ... for some reason the solution didnt work when I clear the text... then I tried programatical approach and it started working .... dont know what was the difference... anyways I updated the source..... if there is a better solution let me know.
#IBAction func textChanged(_ txtField: UITextField) {
if txtField.frame.width >= (view.frame.width * widthMultiplier).rounded() {
txtField.adjustsFontSizeToFitWidth = true
} else {
txtField.adjustsFontSizeToFitWidth = false
}
}

How to access scrollbar position in swift

I'm trying to access the position of a scrollbar in scrollview so that I can attach some UI elements to it. The only method I've found to access this is scrollViewDidScroll(_ scrollView:).
This gives me the scrollView.contentOffset.y property, but I can't seem to use it during runtime to attach my UI to it. I think this gives me the entire scrollview length though, and not the position of the scrollbar.
I'm basically trying to do exactly what's circled in red in this screenshot. (KakaoTalk message)
The simplest way I've found to get the relative position of the scrollBar position is to find it's current percentage in the scrollView, and use that value to find the percent the scrollBar is at within the Bounds.
ScrollView position: (scrollView.contentOffset.y / scrollView.contentSize.height)
Which is basically Current Scrolling Position / Total Height of ScrollView
Let's call it scrollPercent.
let scrollPercent = (scrollView.contentOffset.y / scrollView.contentSize.height)
That will give you a % value between 0 and 1 (0% and 100%).
You can take the the scrollPercent and multiply it by the Parent View's max height view.bounds.size.height, to get the approximate Y value of the scrollbar within the view.
let scrollBarPosition = scrollPercent * (view.bounds.size.height)
This can be used as the Y value for your UI element.
When you are doing this, be sure to check that scrollView.contentSize.height is > 0, because it starts as zero, and if you try to divide by that, the number will reach infinite and your app will crash.
The final solution looks like this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollBarPosition: CGFloat = (scrollView.contentOffset.y / scrollView.contentSize.height) * (view.bounds.size.height)
if scrollView.contentSize.height > 0 {
dateScrollbar.snp.remakeConstraints { (make) in
make.centerY.equalTo(scrollBarPosition)
make.trailing.equalToSuperview().inset(110)
}
}
}
Note: I am using an autolayout library called SnapKit for setting autolayout constraints.
EDIT: The above solution works, but it will also extend your new UI off-screen at the top and bottom of your scrollView.
To fix this: Add a subview to the parentView, and make the subView's height the denominator of the scrollBarPosition. This will act like a "track" for any UI like the picture to slide against, and stay within its bounds.
let scrollBarTrack = UIView()
view.addSubView(scrollBarTrack)
//Put it wherever you want, with the height being equal to whatever you want, and the width can be something like 1.
and then update your scrollBarPosition
let scrollBarPosition = scrollPercent * (scrollBarTrack.bounds.size.height)
It should stay within whatever those bounds are :)

Get the content height of a UITextView

I use this open source library called ReadMoreTextView to display a bunch of text. The library enables me to toggle between displaying an excerpt of the text and showing the full length content (the button is a normal UIButton added by me, not the library).
This works as expected.
My problem arises when I have to display a smaller amount of text. If I add some content that doesn't exceed the height of the UITextView, I want to hide the more/less toggle button. So I thought of taking the full content height and if only it's larger than the text view's height, show the toggle button.
In the above example, I aded a long couple of paragraphs that exceeds the text view bounds. The text view's height comes up as 128. But the content height also returns 128. There's a library specific method called boundingRectForCharacterRange which is supposed to return the content height also returns a wrong value (100).
print("TEXTVIEW HEIGHT: \(textView.bounds.height)") // 128
print("CONTENT HEIGHT: \(textView.contentSize.height)") // 128
let rect = textView.layoutManager.boundingRectForCharacterRange(range: NSRange(location: 0, length: textView.text.count), inTextContainer: textView.textContainer)
print("TEXT HEIGHT: \(rect.height)") // 100
I opened an issue at the library's Github page but the owner asked to ask it here.
Why does the content height return a wrong value?
Here is the project I'm using in the above example by the way.
You can simply use following method to get the content size:
let contentSize = self.textView.sizeThatFits(self.textView.bounds.size)
Then update the textview frame accordingly:
self.textView.frame = CGRect(width: contentSize.width, height: contentSize.height)

How to prevent UILabel to fill the entire screen?

I am trying to display a UILabel that may take up multiple lines but I'm having problem with how the height is resized.
Here is what it looks when I have text over a single line, displaying correctly:
When the text spans multiple lines however this happens:
Here's the interface builder settings I'm using:
Ideally I'd like the text view to remain at athe top of the screen and just take up as much space as it needs to diaplay the text but I really can't tell where I am going wrong.
The text view is a bit tricky to handle with automatic layout. If possible use an UILabel. If not then there are several issues with the text view and the most manageable solution is to add the height constraint which is then manipulated in the code.
The height of the text view content can be determined as:
let height = textView.sizeThatFits(textView.frame.size).height
It is also possible to use
let height = textView.contentSize.height
But the results are sometimes incorrect.
You do need to then set the delegate for the text view so that on change you will refresh the size of the text view.
Well you did give it permission to do so based on your constraints. Any height > 0 as long as it's 20 from the top margin. Since you don't have any other views to base your height off of you can hook up an outlet to your label and use this:
#IBOutlet weak var label: UILabel!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
label.sizeToFit()
}
Uncheck the "Preferred Width" explicit checkbox(In Size Inspector)
Remove the height constraint on you UILabel.
It will definitely work.

Resources