Get the content height of a UITextView - ios

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)

Related

Prevent UITextView from offsetting its text container

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;
}
}

How to subclass init#coder for a UITextView? [duplicate]

Background
I am making a vertical label to use with traditional Mongolian script. Before I was just rotating a UILabel but there were some performance issues and other complications with this. Now I am working on making a label from scratch. However, I need the vertical label to tell auto layout when its height adjusts (based on string length).
What I have read
I read the Intrinsic Content Size and Views with Intrinsic Content Size documentation. These were more about how to use it, though, and not how to define it in a custom view.
Searching for "ios intrinsic content size for a custom view" only gives me
Proper usage of intrinsicContentSize and sizeThatFits: on UIView Subclass with autolayout
in Stack Overflow. This particular question didn't even need intrinsic content size because their view was just an assembly of standard views.
What I am trying
What I am trying is my answer below. I am adding this Q&A pair so that it won't take other people as long to find the answer as it took me with the search keywords that I used.
Setting the intrinsic content size of a custom view lets auto layout know how big that view would like to be. In order to set it, you need to override intrinsicContentSize.
override var intrinsicContentSize: CGSize {
return CGSize(width: x, height: y)
}
Then call
invalidateIntrinsicContentSize()
Whenever your custom view's intrinsic content size changes and the frame should be updated.
Notes
Swift 3 update: Easier Auto Layout: Coding Constraints in iOS 9
Just because you have the intrinsic content size set up in your custom view doesn't mean it will work as you expect. Read the documentation for how to use it, paying special attention to Content-Hugging and Compression-Resistance.
Thanks also to this Q&A for putting me on the right track: How can I add padding to the intrinsic content size of UILabel?
Thanks also to this article and the documentation for help with invalidateIntrinsicContentSize().
Example of a "view with intrinsic height" ...
#IBDesignable class HView: UIView {
#IBInspectable var height: CGFloat = 100.0
override var intrinsicContentSize: CGSize {
return CGSize(width: 99, height: height)
// if using in, say, a vertical stack view, the width is ignored
}
override func prepareForInterfaceBuilder() {
invalidateIntrinsicContentSize()
}
}
which you can set as an inspectable
Since it has an intrinsic height, it can (for example) be immediately inserted in a stack view in code:
stack?.insertArrangedSubview(HView(), at: 3)
In contrast, if it was a normal view with no intrinsic height, you'd have to add a height anchor or it would crash:
let v:UIView = HView()
v.heightAnchor.constraint(equalToConstant: 100).isActive = true
stack?.insertArrangedSubview(v, at: 3)
Note that in ...
the important special case of a stack view:
you set only ONE anchor (for vertical stack view, the height; for horizontal the width)
so, setting the intrinsic height works perfectly, since:
the intrinsic height indeed means that the height anchor specifically will be set automatically if needed.
Remembering that in all normal cases of a subview, many other anchors are needed.

How to set a custom view's intrinsic content size in Swift?

Background
I am making a vertical label to use with traditional Mongolian script. Before I was just rotating a UILabel but there were some performance issues and other complications with this. Now I am working on making a label from scratch. However, I need the vertical label to tell auto layout when its height adjusts (based on string length).
What I have read
I read the Intrinsic Content Size and Views with Intrinsic Content Size documentation. These were more about how to use it, though, and not how to define it in a custom view.
Searching for "ios intrinsic content size for a custom view" only gives me
Proper usage of intrinsicContentSize and sizeThatFits: on UIView Subclass with autolayout
in Stack Overflow. This particular question didn't even need intrinsic content size because their view was just an assembly of standard views.
What I am trying
What I am trying is my answer below. I am adding this Q&A pair so that it won't take other people as long to find the answer as it took me with the search keywords that I used.
Setting the intrinsic content size of a custom view lets auto layout know how big that view would like to be. In order to set it, you need to override intrinsicContentSize.
override var intrinsicContentSize: CGSize {
return CGSize(width: x, height: y)
}
Then call
invalidateIntrinsicContentSize()
Whenever your custom view's intrinsic content size changes and the frame should be updated.
Notes
Swift 3 update: Easier Auto Layout: Coding Constraints in iOS 9
Just because you have the intrinsic content size set up in your custom view doesn't mean it will work as you expect. Read the documentation for how to use it, paying special attention to Content-Hugging and Compression-Resistance.
Thanks also to this Q&A for putting me on the right track: How can I add padding to the intrinsic content size of UILabel?
Thanks also to this article and the documentation for help with invalidateIntrinsicContentSize().
Example of a "view with intrinsic height" ...
#IBDesignable class HView: UIView {
#IBInspectable var height: CGFloat = 100.0
override var intrinsicContentSize: CGSize {
return CGSize(width: 99, height: height)
// if using in, say, a vertical stack view, the width is ignored
}
override func prepareForInterfaceBuilder() {
invalidateIntrinsicContentSize()
}
}
which you can set as an inspectable
Since it has an intrinsic height, it can (for example) be immediately inserted in a stack view in code:
stack?.insertArrangedSubview(HView(), at: 3)
In contrast, if it was a normal view with no intrinsic height, you'd have to add a height anchor or it would crash:
let v:UIView = HView()
v.heightAnchor.constraint(equalToConstant: 100).isActive = true
stack?.insertArrangedSubview(v, at: 3)
Note that in ...
the important special case of a stack view:
you set only ONE anchor (for vertical stack view, the height; for horizontal the width)
so, setting the intrinsic height works perfectly, since:
the intrinsic height indeed means that the height anchor specifically will be set automatically if needed.
Remembering that in all normal cases of a subview, many other anchors are needed.

UITextView content size not updating with font size change

I have this function here, which is called after every time the text in my quoteTextView changes:
func sizeFontToFit(){
quoteTextView.font = UIFont(name: quoteTextView.font.fontName, size: 20.0)
self.view.setNeedsLayout()
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
println("Attempting to fit \(quoteTextView.text)")
println()
println("textView frame is w: \(quoteTextView.frame.size.width) h: \(quoteTextView.frame.size.height)")
println("contentSize is w: \(quoteTextView.contentSize.width) h: \(quoteTextView.contentSize.height)")
println()
while ((quoteTextView.contentSize.height < quoteTextView.frame.size.height) && (quoteTextView.font.pointSize < 100)){
quoteTextView.font = UIFont(name: quoteTextView.font.fontName, size: quoteTextView.font.pointSize + 1)
self.view.setNeedsLayout()
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
println("font size is \(quoteTextView.font.pointSize)")
println("contentSize is w: \(quoteTextView.contentSize.width) h: \(quoteTextView.contentSize.height)")
}
println("Done resizing")
}
What it's supposed to do:
I want the text in quoteTextView to always cover the entire screen, by changing its font size to fit. The function starts at the minimum size of 20, and if that doesn't cover it, then it should gradually increase the font size until it does (or hits the maximum size of 100).
My problem is:
The program recognises if the initial text is indeed too small or not. If there is enough text to cover the screen, it leaves it alone, as it should do; but if not then it starts doing the gradual changes to font size. The issue is that quoteTextView.contentSize doesn't update after each of those changes (the println's keep showing the initial value all theway through) and if there wasn't a limit of 100 font size, it would just go on an infinite loop.
So in the end I am left with all or nothing. The text size is always either at the minimum 20 or all the way at 100.
Is there a way to force contentSize to update? And if not, is there another way of doing this?
EDIT:
replaced
quoteTextView.layoutSubviews()
with
self.view.setNeedsLayout()
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
where self is the viewController holding the textView
(no difference)
FIXED:
The problem was, as Rory said in the comments, that the texView had editable property turned OFF. Turning it on fixed it for me.

Is it possible to add an offset of text to the UITextView?

I have a book app. User choose a book and UITextView show it. Every book has a caption. I need to add 55px offset to the top of UITextView. I want that every caption has 55px offset. But I don't know how to do this?
1)My text height is 22px. I can add 2 or 3 empty line of text, but it doesn't suit me because I need 55px.
2)I can add contentOffset.y = 55. But I have a problem. contentOffset will be zero if an user scroll to top.
Have you tried using contentInset in UIScrollView?
textView.contentInset = UIEdgeInsetsMake(55.0, 0, 0, 0);

Resources