I have a UITextView
containing an NSAttributedString
. I want to size the text view so that, given a fixed width, it shows the entire string without scrolling.
NSAttributedString
has a method which allows to compute its bounding rect for a given size
let computedSize = attributedString.boundingRect(with: CGSize(width: 200, height: CGFloat.greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
context: nil)
But unfortunately it seems not working, since it always returns the height of a single line.
After several attempts, I figured out that the NSAttributedString
I was setting had byTruncatingTail
as lineBreakMode
value for NSParagraphStyle
(which is the default value we use in our application).
To achieve the desired behaviour I have to change it to byWordWrapping
or byCharWrapping
.
let paragraphStyle = NSMutableParagraphStyle()
// When setting "byTruncatingTail" it returns a single line height
// paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.lineBreakMode = .byWordWrapping
let stringAttributes: [NSAttributedString.Key: Any] = [.font: UIFont(name: "Avenir-Book", size: 16.0)!,
.paragraphStyle: paragraphStyle]
let attributedString = NSAttributedString(string: string,
attributes: stringAttributes)
let computedSize = attributedString.boundingRect(with: CGSize(width: 200, height: CGFloat.greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
context: nil)
computedSize.height
Note that when setting the attributed string with byTruncatingTail
value on a UILabel
(where numberOfLines
value is 0), the string is "automatically" sized to be multiline, which doesn't happen when computing the boundingRect
.
There are other factors to keep in mind when computing NSAttributedString
height for use inside a UITextView
(each one of these can cause the string not to be entirely contained in the text view):
1. Recompute height when bounds change
Since height is based on bounds, it should be recomputed when bounds change. This can be achieved using KVO on bounds
keypath, invalidating the layout when this change.
observe(\.bounds) { (_, _) in
invalidateIntrinsicContentSize()
layoutIfNeeded()
}
In my case I'm invalidating intrinsicContentSize
of my custom UITextView
since is the way I size it based on the computed string height.
2. Use NSTextContainer
width
Use textContainer.width
(instead of bounds.width
) as the fixed width to use for boundingRect
method call, since it keeps any textContainerInset
value into account (although left
and right
default values are 0)
3. Add vertical textContainerInsets
values to string height
After computing NSAttributedString
height we should add textContainerInsets.top
and textContainerInsets.bottom
to compute the correct UITextField
height (their default values is 8.0...)
override var intrinsicContentSize: CGSize {
let computedHeight = attributedText.boundingHeight(forFixedWidth: textContainer.size.width)
return CGSize(width: bounds.width,
height: computedHeight + textContainerInset.top + textContainerInset.bottom)
}
4. Remove lineFragmentPadding
Set 0 as value of lineFragmentPadding
or, if you want to have it, remember to remove its value from the "fixed width" before computing NSAttributedString
height
textView.textContainer.lineFragmentPadding = 0
5. Apply ceil
to computed height
The height value returned by boundingRect
can be fractional, if we use as it is it can potentially cause the last line not to be shown. Pass it to the ceil
function to obtain the upper integer value, to avoid down rounding.
A possible way to do it, is to subclass UITextView
to inform you whenever its contentSize did change (~ the size of the text).
class MyExpandableTextView: UITextView {
var onDidChangeContentSize: ((CGSize) -> Void)?
override var contentSize: CGSize {
didSet {
onDidChangeContentSize?(contentSize)
}
}
}
On the "Parent View":
@IBOulet var expandableTextView: MyExpandableTextView! //Do not forget to set the class in the Xib/Storyboard
// or
var expandableTextView = MyExpandableTextView()
And applying the effect:
expandableTextView. onDidChangeContentSize = { [weak self] newSize in
// if you have a NSLayoutConstraint on the height:
// self?.myExpandableTextViewHeightConstraint.constant = newSize.height
// else if you play with "frames"
// self?.expandableTextView.frame.height = newSize.height
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.