Resizeable then scrollable UITextView like Telegram - ios

I want to create UITextView that can resize and scroll at the same time like the ones on Telegram , Instagram or Whats App that allow UITextView to grow up to or 8 lines then you can scroll if you add more text to it I was able to make the UITextView grow to 5 line but if they are more text I can not see since the isScroll property is disabled
my UITextView is inside UIView with two button on the left and right and I would prefer to do it through constrain if that's possible if not through code is fine too

Sagar's answer is great, but I want to enhance it a bit and add some animation to it:
the steps you need
get an outlet to your textView
add a height constraint and get an outlet to it
implement textViewDidChange delegate method of the textView
in textViewDidChange
calculate new height using textView.sizeThatFits(size)
set the height constraint constant to new height
[optional] animate the constraint change to be more user friendly
here is an example
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
#IBOutlet weak var textViewHeightConstraint: NSLayoutConstraint!
let maxTextHeight:CGFloat = 200
let minTextHeight:CGFloat = 50
let animationDuration:Double = 0.3
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
resizeTextViewToFitText()
}
func resizeTextViewToFitText() {
let size = CGSize(width: textView.frame.width, height: .infinity)
let expectedSize = textView.sizeThatFits(size)
self.textViewHeightConstraint.constant = max(min(expectedSize.height, self.maxTextHeight), self.minTextHeight)
self.textView.isScrollEnabled = expectedSize.height > self.maxTextHeight
UIView.animate(withDuration: animationDuration) {
self.view.layoutIfNeeded()
}
}
}
extension ViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
resizeTextViewToFitText()
}
}

You can achieve your expected outcome by following steps:
Assign a textView delegate to your controller
Default disable textView scrolling
On textViewDidChange delegate method measure text height according textView frame
Assign appropriate height to textview & enable scroll if content is exceeded (Up to max height in your case 8 line)
Here below I am attaching code snippet, which may help you:
let commentViewMinHeight: CGFloat = 45.0
let commentViewMaxHeight: CGFloat = 120.0 //In your case it should be 8 lines
func textViewDidChange(_ textView: UITextView) {
//Calculate text height
let size = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
textViewHeightConstraint.constant = size.height.clamped(to: commentViewMinHeight...commentViewMaxHeight)
if textView.contentSize.height < commentViewMaxHeight {
textView.setContentOffset(CGPoint.zero, animated: false)
if textView.isScrollEnabled {
textView.isScrollEnabled = false
}
} else {
if !textView.isScrollEnabled {
textView.isScrollEnabled = true
}
}
}
extension Comparable {
func clamped(to limits: ClosedRange<Self>) -> Self {
return min(max(self, limits.lowerBound), limits.upperBound)
}
}

Related

How do I get UITextView field to expand when adding text and scrolling is disabled?

I am using a UITextView inside a tableView cell to hold varying sized text content with scrolling disabled.
In order to auto-size the UITextView I've used auto-layout to pin it to the layout and also added this method to adjust the height:
override func viewWillAppear(_ animated: Bool) {
tableView.estimatedRowHeight = 50
tableView.rowHeight = UITableViewAutomaticDimension
}
This works correctly on the initial view - when the content first loads. However, I also want the user to be able to edit the text when they tap into the content (similar to the Apple Reminders app). This works correctly with one limitation: UITextView does not expand as the content grows.
How do I enable UITextView to expand during editing without scrolling?
New details:
Here is a screenshot of the current settings.
Per Matt's recommendations below, I created the following subclass.
class MyTextView: UITextView {
#IBOutlet var heightConstraint : NSLayoutConstraint?
override func awakeFromNib() {
super.awakeFromNib()
self.heightConstraint?.isActive = false
}
}
I had to modify the forced unwrapping to avoid a fatal error.
How do I enable UITextView to expand during editing without scrolling
A self-sizing text view is very simple; for a non-scrolling text view with no height constraint, it's the default. In this example, I've added some code to remove the existing height constraint, though you could do that in the storyboard just by indicating that the height constraint is a placeholder:
class MyTextView : UITextView {
#IBOutlet var heightConstraint : NSLayoutConstraint!
override func awakeFromNib() {
super.awakeFromNib()
self.heightConstraint.isActive = false
}
}
Screencast of the result:
If you subsequently do a batch update on the table view, and assuming the cell's other internal constraints are right, the cell will be remeasured as well (but I didn't demonstrate that as part of the example).
Everyone was very diligent about trying to help me resolve this issue. I tried each one and was not able to implement any of them with satisfactory results.
I was directed to this solution by an associate: https://stackoverflow.com/a/36070002/152205 and with the following modifications was able to solve my problem.
// MARK: UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
let startHeight = textView.frame.size.height
let calcHeight = textView.sizeThatFits(textView.frame.size).height
if startHeight != calcHeight {
UIView.setAnimationsEnabled(false)
self.tableView.beginUpdates()
self.tableView.endUpdates()
// let scrollTo = self.tableView.contentSize.height - self.tableView.frame.size.height
// self.tableView.setContentOffset(CGPoint(x: 0, y: scrollTo), animated: false)
UIView.setAnimationsEnabled(true)
}
}
Note: The scrollTo option caused the content to shift up several cell. With that removed everything worked as expected.
you could use var sizeThatFits
override func viewDidLoad() {
super.viewDidLoad()
textView = UITextView()
textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: textView.frame.size.height))
}

Update constraints swift layoutIfNeeded doesn't work

I need to update constraints (height of my CollectionView) when request is done and I have images from server, so height of View also will change.
Result screen
what I need screen
My code:
DispatchQueue.main.async {
self.cvActivity.alpha = 0
self.collectionView.reloadData()
self.collectionView.heightAnchor.constraint(equalToConstant: self.cellWidth * 2).isActive = true
self.collectionView.setNeedsUpdateConstraints()
self.collectionView.setNeedsLayout()
self.view.layoutIfNeeded()
}
Well, basic idea by #J. Doe is correct, here some code explanation (for simplicity i used UIView, not UICollectionView):
import UIKit
class ViewController: UIViewController {
#IBOutlet var heightConstraint: NSLayoutConstraint! // link the height constraint to your collectionView
private var height: CGFloat = 100 // you should keep constraint height for different view states somewhere
override func updateViewConstraints() {
heightConstraint.constant = height // set the correct height
super.updateViewConstraints()
}
// update height by button press with animation
#IBAction func increaseHight(_ sender: UIButton) {
height *= 2
UIView.animate(withDuration: 0.3) {
self.view.setNeedsUpdateConstraints()
self.view.layoutIfNeeded() // if you call `layoutIfNeeded` on your collectionView, animation doesn't work
}
}
}
Result:
Define a height for your collectionView, create an outlet from that constraint and increase the constant of that constraint and call layoutifneeded in an animation block
You need to make object of your Height constraint from storyboard
#IBOutlet weak var YourHeightConstraintName: NSLayoutConstraint!
YourConstraintName.constant = valueYouWantToGive
---------OR--------
collectionViewOutlet.view.frame = CGRect(x:
collectionViewOutlet.frame.origin.x , y:
collectionViewOutlet.frame.origin.y, width:
collectionViewOutlet.frame.width, height:
yourheightValue)

Swift: Scroll a UITextView horizontally

I want to display selected contacts on a label which could be scrolled like in Snapchat. After going through lot of questions on stackoverflow I have used Textview since it is scrollable.
#IBOutlet weak var selectedContactsDisplay: UITextView!
selectedContactsDisplay.delegate = self
selectedContactsDisplay.backgroundColor = UIColor.appColor()
selectedContactsDisplay.textColor = UIColor.white
selectedContactsDisplay.textContainer.maximumNumberOfLines = 1
selectedContactsDisplay.textContainer.lineBreakMode = NSLineBreakMode.byTruncatingHead
let stringOne = selectedContactName.joined(separator: ",")
selectedContactsDisplay.text = stringOne
func textViewDidBeginEditing(_ textView: UITextView) {
textView.resignFirstResponder()
}
But, the horizontal scrolling is still not possible. Can someone help me on how can the enable the scrolling.
You can not scroll in a TextView by yourself, what you can do is to enable autoScroll:
#IBOutlet weak var selectedContactsDisplay: UITextView!
selectedContactsDisplay.delegate = self
selectedContactsDisplay.backgroundColor = UIColor.appColor()
selectedContactsDisplay.textColor = UIColor.white
selectedContactsDisplay.textContainer.maximumNumberOfLines = 1
selectedContactsDisplay.textContainer.lineBreakMode = NSLineBreakMode.byTruncatingHead
let stringOne = selectedContactName.joined(separator: ",")
selectedContactsDisplay.text = stringOne
func textViewDidBeginEditing(_ textView: UITextView) {
textView.resignFirstResponder()
let range = NSMakeRange(selectedContactsDisplay.text.characters.count - 1, 0)
selectedContactsDisplay.scrollRangeToVisible(range)
}
You can not scroll horizontally in UITextView. For solution you can take a UIScrollView which can scroll horizontally and can add label or textfield in to it and increase width of that label according to your content! Proper constraint should be set!

Dynamic UITextView mislocation behavior

I am trying to have a textview similar to iPhone messages, where the textview initially has a constraint (height <= 100) and the scrollEnabled = false
This is a link to the project:
https://github.com/akawther/TextView
The text view increases in height based on the content size as in the image on the left until it reaches the height of 100, then the scrollEnabled is set to true. It works perfectly until I click the "send" button on the lower right where the textView should become empty and go back to the original height and scrollEnabled becomes false. The middle image shows what happens when I click the button. When I start typing the textview moves down as you see in the last image on the right.
I want to be able to click the button and eliminate the behavior shown on the middle image, how can I fix this?
import UIKit
class ViewController: UIViewController, UITextViewDelegate {
#IBOutlet weak var bottomConstraint: NSLayoutConstraint!
#IBOutlet weak var messageTextView: UITextView!
#IBOutlet weak var parent: UIView!
let messageTextViewMaxHeight: CGFloat = 100
override func viewDidLoad() {
super.viewDidLoad()
self.messageTextView.delegate = self
}
#IBAction func Reset(sender: AnyObject) {
messageTextView.text = ""
messageTextView.frame.size.height = messageTextView.contentSize.height
messageTextView.scrollEnabled = false
self.parent.layoutIfNeeded()
}
func textViewDidChange(textView: UITextView) {
if textView.frame.size.height >= self.messageTextViewMaxHeight {
textView.scrollEnabled = true
} else {
textView.scrollEnabled = false
textView.frame.size.height = textView.contentSize.height
}
}
}
You can replicate my issue by following these steps in the github project:
1. keep typing words and pressing enters until you start seeing the scroll
2. Click the button you will see that the textview goes up in the blue
container. This is the issue I want to eliminate!
Try bellow code :-
#IBAction func Reset(sender: AnyObject) {
messageTextView.scrollEnabled = false
messageTextView.text = ""
messageTextView.frame.size.height = messageTextView.contentSize.height
parent.frame.size.height = 20
self.view.layoutIfNeeded()
}
func textViewDidChange(textView: UITextView) {
if textView.contentSize.height >= self.messageTextViewMaxHeight {
textView.scrollEnabled = true
} else {
textView.frame.size.height = textView.contentSize.height
textView.scrollEnabled = false
}
}
Your issue is that the UITextView has conflicting properties:
Place on the screen
Size
The size being constrained will cause an issue when you need a resizable TextView. Also, when the TextView is resized, its location is being changed in this case.
Alternate method to approach the issue:
Try setting constraints to its location in relation to the bottom of the screen. When the Keyboard appears, you should move the TextView up with it. Also setting constraints on the height of a resizable TextView is bad practice unless you are planning on forcing the user to scroll.
Hope this helps.
If you are using auto layout, you should be updating to constraint instead of updating the textView.frame.Try create a IBOutlet for your textView heightConstraint then set the updated height to it.
IBOutlet weak var textViewHeightConstraint: NSLayoutConstraint!
//calculate the height and update the constant
textViewHeightConstraint.constant = textView.contentSize.height

how can i increase the height of an inputAccessoryView

I have spent several days on this with no solution in sight.
I have an inputAccessoryView which consists of a UIView containing a textView and two buttons. The behaviour of the inputAccessoryView is as expected and works fine in all cases except one.
When the height of the textView increases, I am trying to increase the height of the inputAccessoryView by the same amount. When I redefine the height of the inputAccessoryView in textViewDidChange, the inputAccessoryView increases height downwards over the keyboard instead of upwards.
I have tried many different suggestions from SO but nothing has worked. I guess it is the automatically added NSLayoutConstraint of the inputAccessoryView but I have no idea how to change that value in swift and iOS 8.3.
func textViewDidChange(textView: UITextView) {
var contentSize = messageTextView.sizeThatFits(CGSizeMake(messageTextView.frame.size.width, CGFloat.max))
inputAccessoryView.frame.size.height = contentSize.height + 16
}
adding
inputAccessoryView.setTranslatesAutoresizingMaskIntoConstraints(true)
to the above code helps and the inputAccessoryView height increases upwards correctly however I get Unable to simultaneously satisfy constraints for several constraints and it is very difficult to identify the offenders. Also I get an odd effect of the textView creating extra space below on every second instance of a new line.
thanks.
To make input accessory view grow vertically you just set its autoresizingMask = .flexibleHeight, calculate its intrinsicContentSize and let the framework do the rest.
The code:
class InputAccessoryView: UIView, UITextViewDelegate {
let textView = UITextView()
override init(frame: CGRect) {
super.init(frame: frame)
// This is required to make the view grow vertically
self.autoresizingMask = UIView.AutoresizingMask.flexibleHeight
// Setup textView as needed
self.addSubview(self.textView)
self.textView.translatesAutoresizingMaskIntoConstraints = false
self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[textView]|", options: [], metrics: nil, views: ["textView": self.textView]))
self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[textView]|", options: [], metrics: nil, views: ["textView": self.textView]))
self.textView.delegate = self
// Disabling textView scrolling prevents some undesired effects,
// like incorrect contentOffset when adding new line,
// and makes the textView behave similar to Apple's Messages app
self.textView.isScrollEnabled = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
// Calculate intrinsicContentSize that will fit all the text
let textSize = self.textView.sizeThatFits(CGSize(width: self.textView.bounds.width, height: CGFloat.greatestFiniteMagnitude))
return CGSize(width: self.bounds.width, height: textSize.height)
}
// MARK: UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
// Re-calculate intrinsicContentSize when text changes
self.invalidateIntrinsicContentSize()
}
}
Fast forward to 2020, you can just do the following, everything else the same as in maxkonovalov's answer
override var intrinsicContentSize: CGSize {
return .zero
}
// MARK: UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
sizeToFit()
}

Resources