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
Related
I notice that, if I perform add/ expand animation within an UIScrollView, it will cause unwanted scrolling behavior, when the UIScrollView fill with enough content to become scroll-able.
As you can see in the following animation, initially, the add/ expand animation works just fine.
When we have added enough item till the UIScrollView scrollable, whenever a new item is added, and UIScrollView will first perform scroll down, and then scroll up again!
My expectation is that, the UIScrollView should remain static, when add/ expand animation is performed.
Here's the code which performs add/ expand animation.
Add/ expand animation
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
// Clear off horizontal swipe in animation caused by addArrangedSubview
stackView.superview?.layoutIfNeeded()
customView.show()
// Perform expand animation.
UIView.animate(withDuration: 1) {
self.stackView.superview?.layoutIfNeeded()
}
}
Here's the constraint setup of the UIScrollView & added custom view item
Constraint setup
Custom view
class CustomView: UIView {
private var zeroHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.cornerRadius = stackView.frame.height / 2
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
zeroHeightConstraint = self.safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 0)
zeroHeightConstraint.isActive = false
}
func hide() {
zeroHeightConstraint.isActive = true
}
func show() {
zeroHeightConstraint.isActive = false
}
}
Here's the complete source code
https://github.com/yccheok/add-expand-animation-in-scroll-view
Do you have any idea why such problem occur, and we can fix such? Thanks.
Because of the way stack views arrange their subviews, animation can be problematic.
One approach that you may find works better is to embed the stack view in a "container" view.
That way, you can use the .isHidden property when adding an arranged subview, and allow the animation to update the "container" view:
The "add view" function now becomes (I added a Bool so we can skip the animation on the initial add in viewDidLoad()):
func addCustomView(_ animated: Bool) {
let customView = CustomView.instanceFromNib()
stackView.addArrangedSubview(customView)
customView.isHidden = true
if animated {
DispatchQueue.main.async {
UIView.animate(withDuration: 1) {
customView.isHidden = false
}
}
} else {
customView.isHidden = false
}
}
And we can get rid of all of the hide() / show() and zeroHeightConstraint in the custom view class:
class CustomView: UIView {
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
borderView.layer.cornerRadius = borderView.bounds.height * 0.5
}
}
Since it's a bit difficult to clearly show everything here, I forked your project with the changes: https://github.com/DonMag/add-expand-animation-in-scroll-view
Edit
Another "quirk" of animating a stack view shows up when adding the first arranged subview (also, when removing the last one).
One way to get around that is to add an empty view as the first subview.
So, for this example, in viewDidLoad() before adding an instance of CustomView:
let v = UIView()
stackView.addArrangedSubview(v)
This will make the first arranged subview a zero-height view (so it won't be visible).
Then, if you're implementing removing custom views, just make sure you don't remove that first, empty view.
If your stack view has .spacing = 0 noting else is needed.
If your stack view has a non-zero spacing, add another line:
let v = UIView()
stackView.addArrangedSubview(v)
stackView.setCustomSpacing(0, after: v)
I did a little research on this and the consensus was to update the isHidden and alpha properties when inserting a view with animations.
In CustomView:
func hide() {
alpha = 0.0
isHidden = true
zeroHeightConstraint.isActive = true
}
func show() {
alpha = 1.0
isHidden = false
zeroHeightConstraint.isActive = false
}
In your view controller:
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
self.stackView.layoutIfNeeded()
UIView.animate(withDuration: 00.5) {
customView.show()
self.stackView.layoutIfNeeded()
}
}
Also, the constraints in your storyboard aren't totally correct. You are seeing a red constraint error because autolayout doesn't know the height of your stackView. You can give it a fake height and make sure that "Remove at build time" is checked.
Also, get rid of your scrollView contentView height constraint defined as View.height >= Frame Layout Guide.height. Autolayout doesn't need to know the height, it just needs to know how subviews inside of the contentView stack up to define its vertical content size.
Everything else looks pretty good.
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)
}
}
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))
}
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!
Here is my tableview row/cell:
there are constraints set in place - the imageview is below the label and the button is below the imageview.
here is my code:
if(row == 1) {
imageview.hidden = false
} else {
imageview.hidden = true
//how can i change the button constraint from below imageview to below label?
Adding and removing constraints is really bad example for that. I'll make your UI more complex.
Best way of solving these auto-layout problems is adding two constraints. One from imageView to button and second from imageView to label.
Now after setting these constraints, you need to set their priority levels. So, let's say button will be below the imageView first. In this case, you need to set imageView to button constraint's priority to something like 750 or UILayoutPriorityDefaultHigh and label to button constraint's priority to 250 or UILayoutPriorityDefaultLow.
Let's start creating a custom UITableViewCell
class YourTableViewCell: UITableViewCell {
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var label: UILabel!
#IBOutlet weak var button: UIButton!
#IBOutlet weak var buttonToLabelConstraint: NSLayoutConstraint!
#IBOutlet weak var buttonToImageViewConstraint: NSLayoutConstraint!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func shouldHideImageView(hidden: Bool) {
if(hidden == false) {
buttonToLabelConstraint.priority = UILayoutPriorityDefaultLow
buttonToImageViewConstraint.priority = UILayoutPriorityDefaultHigh
imageView.hidden = true
} else {
buttonToLabelConstraint.priority = UILayoutPriorityDefaultHigh
buttonToImageViewConstraint.priority = UILayoutPriorityDefaultLow
imageView.hidden = false
}
self.contentView.layoutIfNeeded()
}
}
After that, in your class where tableView is placed implement a logic like that:
if(row == 1) {
cell.shouldHideImageView(true)
} else {
cell.shouldHideImageView(false)
}
You should be all set.
You can try using a StackView, when you tell something to be hidden, the imageView the stack view will adjust the StackView as if the imageView was never a part of the view and it is an easy work around to not have to worry about constraints.
You can create IBOutlet on constraint and then just simply change the value like this:
buttonConstraint.constant = newValue
But i suggest you create for this a tableView. In this case you code and logic, i think, will be more accurate.
you could to this instead of hiding.
Make an outlet from the heights constraint of the imageview, call it constraint for now.
Set constraint.constant = 0 // effectively same as hiding.
Set constraint.constant = NON_ZERO_VALUE // effectively same as show.
hope it helps!
I see a couple of options. The first is a little easier to implement but a little less flexible if you decide to change your layout later.
Make the button's constraint to be below the label. Keep a reference to this constraint (you can connect it to your code via storyboard just like you do with the button itself, if you're using storyboard). When the imageView is visible, set myConstraint.constant += myImageView.frame.height. When the imageView is hidden, set myConstraint.constant -= myImageView.frame.height. Afterwards, call view.setNeedsLayout to update your constraints.
Make two constraints: one for below the image, and one for below the label ("constraintToImage" and "constraintToLabel"). Hook them both up to your controller like in option 1, and call view.addConstraint(constraintToImage) and view.removeConstraint(constraintToLabel) when the image becomes visible (and the opposite for when it's hidden). Again, call view.setNeedsLayout after.