Adjusting height of UITextView to its text does not work properly - ios

I wrote following code to fit UITextView's height to its text.
The size changes but top margin relative to first line of text differ every other time when I tap enter key on keyboard to add new line.
Setting
xCode 7.3
Deployment target: iOS 9
import UIKit
class ViewController: UIViewController, UITextViewDelegate {
lazy var textView: UITextView = {
let tv = UITextView(frame: CGRectMake(20, 200, (self.view.frame.width - 40), 0) )
tv.backgroundColor = UIColor.lightGrayColor()
return tv
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview( textView )
textView.delegate = self
let height = self.height(textView)
let frame = CGRectMake(textView.frame.origin.x, textView.frame.origin.y, textView.frame.width, height)
textView.frame = frame
}
func textViewDidChange(textView: UITextView) {
let frame = CGRect(x: textView.frame.origin.x, y: textView.frame.origin.y, width: textView.frame.width, height: height(textView) )
textView.frame = frame
}
func height(textView: UITextView) -> CGFloat {
let size = CGSizeMake(textView.frame.size.width, CGFloat.max)
let height = textView.sizeThatFits(size).height
return height
}
}
I tried few other ways to fit UITextView height but they just acted the same say.

To fix this, subclass UITextView and override setContentOffset to allow scrolling only if the content height is larger than the intrinsic content height. Something like:
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
let allowScrolling = (contentSize.height > intrinsicContentSize.height)
if allowScrolling {
super.setContentOffset(contentOffset, animated: animated)
}
}
For auto-growing dynamic height text view, the caret moves on starting a new line. At this moment, the size of the text view hasn't grown to the new size yet. The text view tries to make the caret visible by scrolling the text content which is unnecessary.
You may also need to override intrinsicContentSize too.

Related

UITextField shrinks text before necessary

I have a UITextField for which I've set autoAdjustFontSizeToFitWidth to true and minimumFontSize to 0. The problem is the setting shrinks the text noticeably sooner than it really should. For example, here is an image of a UITextField with the above settings:
The green is the background color of the UITextField. In this example, the text has not shrunk yet, but no matter what I type as the next character the text field always begins shrinking; despite clearly being enough room on the left side for a few more characters. Here is another image with additional characters entered:
As you can see, there is a relatively large area on the left side that the text field won't place text in when auto adjusting. This is for a right aligned text field. The same can be said of center aligned text fields as well, where there is space on the left and right that seems as if an auto adjusting text field won't place text inside.
How do I get it so that auto adjusting text fields use the entire available space?
Update:
You can do the text calculations and font resizing manually. By doing so you will avoid hacks and future compatibility issues.
A simple implementation looks like this:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
var originalFont: UIFont!
override func viewDidLoad() {
super.viewDidLoad()
self.textField.font = self.textField.font?.withSize(44)
self.textField.adjustsFontSizeToFitWidth = false
self.originalFont = textField.font
self.textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
#objc
func textFieldDidChange(sender: UITextField) {
let textRect = sender.textRect(forBounds: sender.bounds)
let textWidth = textRect.width
var currentFont = self.originalFont!
var i = 0
while (i < 10) {
let unrestrictedTextWidth = sender.text!.boundingRect(with: CGSize(width: .greatestFiniteMagnitude,
height: textRect.height),
attributes: [.font : currentFont],
context: nil).width
if unrestrictedTextWidth <= textWidth {
break
}
let factor = textWidth / max(textWidth, unrestrictedTextWidth)
let originalSize = currentFont.pointSize
currentFont = self.originalFont!.withSize(originalSize * factor)
i += 1
}
sender.font = currentFont
}
}
Interestingly the actual relationship between text rect and font size is non-linear and non-trivial. So I added multiple iteration steps to approximate the correct size. I chose a maximum of 10 iterations to avoid infinite loops on very small sizes and rounding errors.
Original Answer:
There has always been some magic around UITextField and adjustsFontSizeToFitWidth. See for example this post from 2015 about how the initial font size affects the minimum font size:
https://stackoverflow.com/a/30881385/921573
A UITextField with:
Font size 17, minimum size 15 will go down to 15 if need be
Font size 17, minimum size 10 will only go down to 14
Font size 13, minimum size 4 will stay at 13
In my tests, setting the minimum font size in IB to 0 just gets ignored – in order so see the shrinking effect it has to be a small value like 1.
Setting it in code to 0 works fine.
So I think it is safe to say that UITextField might be considered historically buggy when it comes to adjustsFontSizeToFitWidth.
That being said, I found a workaround for you:
class FixedTextField: UITextField {
override func textRect(forBounds bounds: CGRect) -> CGRect {
let magicNumber = -15.0
if self.adjustsFontSizeToFitWidth {
return CGRect(
x: bounds.origin.x + magicNumber,
y: bounds.origin.y,
width: bounds.size.width - magicNumber,
height: bounds.size.height
)
} else {
return super.textRect(forBounds: bounds)
}
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return self.textRect(forBounds: bounds)
}
}
This custom text field uses countermagic to mitigate the issue.
You may have to play with the magicNumber according to your font or dimensions or device. For me 15 works ok:
This works for me and the textField.textAlignment is set to .right (it will depend on how many characters you put in the textField though) :
class TextFieldOne: UITextField {
override func alignmentRect(forFrame frame: CGRect) -> CGRect {
// let newWidth = frame.width + 10 // if you want to reduce the right side too.
let x = frame.origin.x - 15 // suit yourself here
let newFrame = CGRect(x: x, y: frame.origin.y, width: frame.width, height: frame.height)
return newFrame
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return self.alignmentRect(forFrame: self.bounds)
}
override func textRect(forBounds bounds: CGRect) -> CGRect {
return self.alignmentRect(forFrame: self.bounds)
}
}

UITextView doesn't print text on the nextLine?

I put the UITextView inside a UIView. The UIView expands as the user types in the UITextView but the problem is that if the user types on the next line, it doesn't show the text being typed until the user types on the third line, then it shows the text printed on the second line. Same goes with the 3rd line and 4th line, etc.
How can I fix this?
func textViewDidChange(_ textView: UITextView) {
print(textView.text)
let size = CGSize(width: prayerRequest.frame.width, height: .infinity)
let estimatedSize = textView.sizeThatFits(size)
textView.constraints.forEach { (constraints) in
if constraints.firstAttribute == .height {
constraints.constant = estimatedSize.height
}
viewContainer.constraints.forEach({ (constraints) in
if constraints.firstAttribute == .height {
constraints.constant = estimatedSize.height
viewContainer.layoutIfNeeded()
}
})
}
}
If you're using interface builder try setting the number of lines to 0.
Or from code textView.textContainer.maximumNumberOfLines = 10
First of all, you don't need to set height for textView's Parent. Even if you do set the low priority for textView's parent.
See the image, Purple being parent and Yellow is textView.
In viewDidLoad, add the following code:
textView.textContainerInset = UIEdgeInsets.zero
textView.textContainer.lineFragmentPadding = 0
Then, implement textView Delegate method:
extension ViewController : UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
let textHeight = textView.attributedText.boundingRect(with:
CGSize.init(width: textView.frame.width, height: .infinity),
options:[.usesLineFragmentOrigin, .usesFontLeading],
context: nil).height
if previousHeight != textHeight {
previousHeight = textHeight
print("Height text: \(textHeight)")
textViewHeight.constant = textHeight
self.view.layoutIfNeeded()
textView.setContentOffset(.zero, animated: false)
}
}
}
textViewHeight is height constraint of textView.
Initialize var previousHeight = CGFloat(0) as instance variable. It will help limit calls to layoutIfNeeded to only when the height is changed.
Bottom constraint of the textView will expand parent view. So you do not need to set Parent's height as well.

Auto-size view with dynamic font in enclosed textview

So here's one I just can't seem to find a matching case for in searching on here.
I have a small UIView that contains a UITextView, and the UIView needs to auto-size around the TextView for presentation over another view. Basically the TextView needs to fully fill the UIView, and the UIView should only be big enough to contain the TextView.
The TextView just contains a couple sentences that are meant to stay on the screen until an external thing happens, and certain values change.
Everything is great when I used a fixed-size font.
But hey... I'm an old guy, and I have the text size jacked up a bit on my phone. Testing it on my device shows where I must be missing something.
When using the dynamic font style "Title 2" in the textview properties, and turning on "Automatically adjust font" in the TextView properties, and having the text larger than the default, it seems as if I'm not properly capturing the size of the TextView's growth (with the bigger text) when creating the new bounding rect to toss at the frame. It's returning values that look a lot like the smaller, default-size text values rather than the increased text size.
Code is below, the view's class code as well as the calling code (made super explicit for posting here). I figure I'm either missing something silly like capturing the size after something happens to the fonts, but even moving this code to a new function and explicitly calling it after the controls fully draw doesn't seem to do it.
I hope this make sense.
Thanks, all.
Calling code:
let noWView:NoWitnessesYetView = (Bundle.main.loadNibNamed("NoWitnessesYetView", owner: nil, options: nil)!.first as! NoWitnessesYetView)
//if nil != noWView {
let leftGutter:CGFloat = 20.0
let bottomGutter:CGFloat = 24.0
let newWidth = self.view.frame.width - ( leftGutter + leftGutter )
let newTop = (eventMap.frame.minY + eventMap.frame.height) - ( noWView.frame.height + bottomGutter ) // I suspect here is the issue
// I suspect that loading without drawing is maybe not allowing
// the fonts to properly draw and the
// TextView to figure out the size...?
noWView.frame = CGRect(x: 20, y: newTop, width: newWidth, height: noWView.frame.height)
self.view.addSubview(noWView)
//}
Class code:
import UIKit
class NoWitnessesYetView: UIView {
#IBOutlet weak var textView: EyeneedRoundedTextView!
override func draw(_ rect: CGRect) {
let newWidth = self.frame.width
// form up a dummy size just to get the proper height for the popup
let workingSize:CGSize = self.textView.sizeThatFits(CGSize(width: newWidth, height: CGFloat(MAXFLOAT)))
// then build the real newSize value
let newSize = CGSize(width: newWidth, height: workingSize.height)
textView.frame.size = newSize
self.textView.isHidden = false
}
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = UIColor.clear // .blue
self.layer.cornerRadius = 10
}
}
This perfect way to do it the content comes from : https://www.youtube.com/watch?v=0Jb29c22xu8 .
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// let's create our text view
let textView = UITextView()
textView.frame = CGRect(x: 0, y: 0, width: 200, height: 100)
textView.backgroundColor = .lightGray
textView.text = "Here is some default text that we want to show and it might be a couple of lines that are word wrapped"
view.addSubview(textView)
// use auto layout to set my textview frame...kinda
textView.translatesAutoresizingMaskIntoConstraints = false
[
textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
textView.heightAnchor.constraint(equalToConstant: 50)
].forEach{ $0.isActive = true }
textView.font = UIFont.preferredFont(forTextStyle: .headline)
textView.delegate = self
textView.isScrollEnabled = false
textViewDidChange(textView)
}
}
extension ViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
print(textView.text)
let size = CGSize(width: view.frame.width, height: .infinity)
let estimatedSize = textView.sizeThatFits(size)
textView.constraints.forEach { (constraint) in
if constraint.firstAttribute == .height {
constraint.constant = estimatedSize.height
}
}
}
}

Self sizing uitextview till specific height

I have an app which has chatting functionality where UITextview is used for entering the message. UITextview height has to be dynamic (if user enters the message, the height has to be changed according to the text length till a specific Height).
How can I achieve this?
Disable Scrolling of textView.
TO Increase Height to a specific value and then enable scrolling.
Provide a maximum height constraint then add this code to your viewController
class YourViewController: UIViewController, UITextViewDelegate
{
#IBOutlet weak var yourTextView: UITextView!
let textViewMaxHeight: CGFloat = 100
override func viewDidLoad()
{
super.viewDidLoad()
yourTextView.delegate = self
}
func textViewDidChange(textView: UITextView)
{
if textView.contentSize.height >= self.textViewMaxHeight
{
textView.scrollEnabled = true
}
else
{
textView.frame.size.height = textView.contentSize.height
textView.scrollEnabled = false
}
}
}
Add a height constraint to your textView and create an outlet so you can adjust it. Then you can use the UITexfield delegate method textViewDidChange(_ textView: UITextView) to adjust the height.
func textViewDidChange(_ textView: UITextView) {
// get the current height of your text from the content size
var height = textView.contentSize.height
// clamp your height to desired values
if height > 90 {
height = 90
} else if height < 50 {
height = 50
}
// update the constraint
textViewHeightConstraint.constant = height
self.view.layoutIfNeeded()
}
shorter version...
func textViewDidChange(_ textView: UITextView) {
let maxHeight: CGFloat = 90.0
let minHeight: CGFloat = 50.0
textViewHeightConstraint.constant = min(maxHeight, max(minHeight, textView.contentSize.height))
self.view.layoutIfNeeded()
}
In case you are subclassing UITextView this could be a solution as well:
class Message: UITextView {
var maximalSizeNotScrollable: CGFloat = 200
var minimumHeightTextView: CGFloat = 35
var textViewHeightAnchor: NSLayoutConstraint!
override var contentSize: CGSize {
didSet {
if textViewHeightAnchor != nil {
textViewHeightAnchor.isActive = false
}
textViewHeightAnchor = heightAnchor.constraint(equalToConstant: min(maximalSizeNotScrollable, max(minimumHeightTextView, contentSize.height)))
textViewHeightAnchor.priority = UILayoutPriority(rawValue: 999)
textViewHeightAnchor.isActive = true
self.layoutIfNeeded()
}
}
}
The priority needs to be smaller than 1000 otherwise you will encounter problems with _UITemporaryLayoutHeight.
I was struggling with the same problem. After reading #Anuraj's response, I dragged and dropped the height constraint of the UITextView to view controller because when textview reaches to the max height and when the scroll is enabled textview was getting smaller just to display one line, so I found the solution by giving the max height to the height constraint of textview. Textview will expand until it reaches max height, after that it will be scrollable. When you delete text, textview height will get smaller automatically.
#IBOutlet weak var txMessageHeight: NSLayoutConstraint!
let textViewMaxHeight: CGFloat = 120
func textViewDidChange(_ textView: UITextView) {
if textView.contentSize.height >= self.textViewMaxHeight{
txMessageHeight.constant = self.textViewMaxHeight
textView.isScrollEnabled = true
}else{
textView.frame.size.height = textView.contentSize.height
textView.isScrollEnabled = false
}
}
Set the Height Constraint of the TextView to <= 120(maxHeight) in StoryBoard
func textViewDidChange(_ textView: UITextView) {
if textView.contentSize.height >= maxHeight {
textView.isScrollEnabled = true
} else {
textView.isScrollEnabled = false
}
}
And if you want to reset the height of TextView when its empty, you can just write
UIView.animate(withDuration: 0.15) {
self.view.layoutIfNeeded()
}

UITextView that starts scrolling when text reached N lines

I'm start implementing text input to a chat app and wondering that is standard behavior of a UITextView with scroll enabled absolutely does not meet expectations.
I want just it is done in chats like WhatsApp. When text reached N, 5 for example lines, scroll bar appear and text container starts scrolling. I wrote code like this, but it doesn't work.
As i think needs to count rows in text container and make content insets, or something like this.
func textViewDidChange(_ textView: UITextView) {
let fixedWidth = myTextView.frame.size.width
myTextView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
let newSize = myTextView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
var newFrame = myTextView.frame
let oldFrame = myTextView.frame
newFrame.size = CGSize(width: max(newSize.width, fixedWidth), height: newSize.height)
myTextView.frame = newFrame
let shift = oldFrame.height - newFrame.height
textView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: shift, right: 0)
textView.scrollIndicatorInsets = textView.contentInset
textView.scrollRangeToVisible(textView.selectedRange)
}
And myTextView is specified as:
let myTextView : UITextView = {
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.isScrollEnabled = false
textView.textContainer.maximumNumberOfLines = 5
textView.textContainer.lineBreakMode = .byWordWrapping
textView.inputAccessoryView = UIView()
return textView
}()
Not based on number of lines, but on a user defined height. You'll find your answer here:
https://stackoverflow.com/a/51235517/10115072
If you want this behaviour to happen is simple:
Create a UIView having UITextView inside
Create a height constraint in UIView priority 1000 of less than or equal your MAX_HEIGHT and also greater than or equal you MIN_HEIGHT
Create a height constraint in you TextView priority 999 equal to your MIN_HEIGHT
Then add this code to your controller
Code:
class YourViewController: KUIViewController {
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
textView.isScrollEnabled = true
}
}
extension YourViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
let size = CGSize(width: view.frame.width, height: .infinity)
let estimatedSize = textView.sizeThatFits(size)
textView.constraints.forEach { (constraint) in
if constraint.firstAttribute == .height {
constraint.constant = estimatedSize.height
}
}
}
}
This has the same behaviour as WhatsApp textView

Resources