How to change WIDTH of UITableView with animation? - ios

I have UITableView on the left size and plain UIView on the right side in my UIViewController
UITableView connects to .top, .leading and .bottom of
superview
UIView connects to .top, .trailing and .bottom of
superview, also it has .width
And UITableView .leading ==
.trailing of UIView
All these constraints you can see on the screenshot:
Here is my animation code in the controller, as you can see I update .width of UIView and also I cycle the animation.
class ViewController: UIViewController {
#IBOutlet var widthConstaint: NSLayoutConstraint!
#IBOutlet var tableView: UITableView!
#IBOutlet var watchContainerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
start()
}
func start() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if self.widthConstaint.constant == 50 {
self.change(width: 100)
} else {
self.change(width: 50)
}
self.start()
}
}
func change(width: CGFloat) {
widthConstaint.constant = width
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 300
}
}
As a result you can see this on .gif, what I should for smooth animation, without this glitch?

UPDATE
I tried to reproduce the UILabel wrong behavior with my own UIView subclass:
class MyView: UIView {
var currentSize: CGSize = .zero
override func layoutSubviews() {
super.layoutSubviews()
guard currentSize != bounds.size else { return }
currentSize = bounds.size
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
UIColor.purple.setFill()
UIBezierPath(rect: rect).fill()
let square = CGRect(x: rect.width - 50, y: rect.origin.y, width: 50, height: rect.height)
UIColor.blue.setFill()
UIBezierPath(rect: square).fill()
}
}
The view draws a little blue rectangle at its right, as a text. And… well… the animation is also good!
But if I change the contentMode to left (the default value is scaleToFill):
The blue square jumps from one place to the other.
So, I found out this is not only related to the UILabel's textAlignment. It's its combinaison with the default left content mode of the UILabel.
contentMode defines how the content should be adjusted if the content's size and the bounds size are different - this is case during the animation, the view redraws itself with its new size right before being resized.
So
label.contentMode = .left // default
label.textAlignment = .left
will behave as:
label.contentMode = .right
label.textAlignment = .right
PREVIOUS ANSWER
I spent some time on your problem. Guess what?
It's not related to the table view at all. It's because of the .right text alignment of the label.
You can reproduce the glitch with a simple UILabel:
class ViewController: UIViewController {
let label = UILabel()
var rightConstraint: NSLayoutConstraint!
override func loadView() {
view = UIView()
view.backgroundColor = .white
}
override func viewDidLoad() {
super.viewDidLoad()
setUpView()
start()
}
func start() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if self.rightConstraint.constant == 50 {
self.change(width: 100)
} else {
self.change(width: 50)
}
self.start()
}
}
func change(width: CGFloat) {
rightConstraint.constant = width
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
func setUpView() {
label.text = "Label"
label.textAlignment = .right
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
rightConstraint = view.trailingAnchor.constraint(equalTo: label.trailingAnchor)
rightConstraint.isActive = true
label.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
}
}
Why?
To find out the error, I created a UILabel subclass and overrode action(for: CALayer, forKey: String). It's where each UIView decides which animation to apply in response to a property change (ref). I was looking for some weird animation behaviors when a label is inside a table view.
If you print key, you'd see that actions are requested for the position, the bounds and the contents of the label when UIKit is about to commit the animations related to your UIView.animate(withDuration:) call.
In fact, setNeedsDisplay is called each time the size of an UILabel is changed (it makes sens). So I guess there is a conflict between the content redrawing of the label and its frame change.
I think you should recreate the right text alignment of the label without using textAlignment = .right.
Horizontally, pin the label to the right only. When layoutIfNeeded will be called, only the label's position will change, not its content.

As mentioned in the #Gaétanz answer. The abnormal behaviour is due to the Lable inside the cell.
You can remove the glitches by changing the Labels leading constraint relation to >= and change the priority to 999 from 1000.
Please refer the screenshot attached for your reference.
When you add constraint with a priority less than 1000 it will be an optional constraint. Priority 1000 is the required priority for a constraint. Here if you added a priority of 999, all the other constraints with priority 1000 will layout first and the less priority constraint will layout last.

You are changing the width of your side view outside of the animation block. put you widthConstaint.constant = width inside your animation block.
func change(width: CGFloat) {
UIView.animate(withDuration: 1.0) {
self.widthConstaint.constant = width
self.view.layoutIfNeeded()
}
}

According to your code the View is animated but the constraint is not put in the UIView animation block. So it looks like a glitch cause it changes the frame directly. Please try to change the code as below and try.
func change(width: CGFloat) {
UIView.animate(withDuration: 1.0) {
widthConstaint.constant = width
self.tableView.layoutIfNeeded()
self.view.layoutIfNeeded()
}
}

Related

How to Apply Animation while changing UIView size? + Swift 5

Im new to IOS development and I have two buttons in the UIView and when user select the option portrait or landscape, change the UIView re-size and change the background color as well and i need to add animation for that process.
As as ex:
User select portrait and then user can see red color UIVIew. after click the landscape option, animation should be started and it looks like, red color image come front and change the size (changing height and width) for landscape mode and go to previous position and change color to green. i have added small UIView animation on code and it is helpful to you identify the where should we start the animation and finish it. if someone know how to it properly, please, let me know and appreciate your help. please, refer below code
import UIKit
class ViewController: UIViewController {
let portraitWidth : CGFloat = 400
let portraitHeight : CGFloat = 500
let landscapeWidth : CGFloat = 700
let landscapeHeight : CGFloat = 400
var mainView: UIView!
var mainStackView: UIStackView!
let segment: UISegmentedControl = {
let segementControl = UISegmentedControl()
return segementControl
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.translatesAutoresizingMaskIntoConstraints = false
mainStackView = UIStackView()
mainStackView.axis = .vertical
mainStackView.translatesAutoresizingMaskIntoConstraints = false
mainStackView.alignment = .center
mainStackView.distribution = .equalCentering
self.view.addSubview(mainStackView)
self.segment.insertSegment(withTitle: "Portrait", at: 0, animated: false)
self.segment.insertSegment(withTitle: "Landscape", at: 1, animated: false)
self.segment.selectedSegmentIndex = 0
self.segment.addTarget(self, action: #selector(changeOrientation(_:)), for: .valueChanged)
self.segment.translatesAutoresizingMaskIntoConstraints = false
self.segment.selectedSegmentIndex = 0
mainStackView.addArrangedSubview(self.segment)
let safeAreaLayoutGuide = self.view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
self.mainStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20),
self.mainStackView.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor, constant: 0),
])
NSLayoutConstraint.activate([
self.segment.heightAnchor.constraint(equalToConstant: 35),
self.segment.widthAnchor.constraint(equalToConstant: 300)
])
mainView = UIView(frame: .zero)
mainView.translatesAutoresizingMaskIntoConstraints = false
mainStackView.addArrangedSubview(mainView)
mainView.backgroundColor = UIColor.red
NSLayoutConstraint.activate([
mainView.heightAnchor.constraint(equalToConstant: portraitHeight),
mainView.widthAnchor.constraint(equalToConstant: portraitWidth),
mainView.topAnchor.constraint(equalTo: segment.bottomAnchor, constant: 30)
])
}
#IBAction func changeOrientation(_ sender: UISegmentedControl) {
self.mainView.constraints.forEach{ (constraint) in
self.mainView.removeConstraint(constraint)
}
UIView.animate(withDuration: 1.0) {
if (sender.selectedSegmentIndex == 0) {
self.mainView.backgroundColor = UIColor.red
NSLayoutConstraint.activate([
self.mainView.heightAnchor.constraint(equalToConstant: self.portraitHeight),
self.mainView.widthAnchor.constraint(equalToConstant: self.portraitWidth),
self.mainView.topAnchor.constraint(equalTo: self.segment.bottomAnchor, constant: 30)
])
} else {
self.mainView.backgroundColor = UIColor.green
NSLayoutConstraint.activate([
self.mainView.heightAnchor.constraint(equalToConstant: self.landscapeHeight),
self.mainView.widthAnchor.constraint(equalToConstant: self.landscapeWidth),
self.mainView.topAnchor.constraint(equalTo: self.segment.bottomAnchor, constant: 30)
])
}
}
}
}
updated logic
#IBAction func changeOrientation(_ sender: UISegmentedControl) {
UIView.animate(withDuration: 4.0) {
if (sender.selectedSegmentIndex == 0) {
self.mainView.backgroundColor = UIColor.red
self.widthConstraint.constant = self.portraitWidth
self.heightConstraint.constant = self.portraitWidth
} else {
self.mainView.backgroundColor = UIColor.green
self.widthConstraint.constant = self.landscapeWidth
self.heightConstraint.constant = self.landscapeHeight
}
self.mainView.layoutIfNeeded()
} }
It's possible, the basic problem is:
• It's not possible to animate an actual change between one constraint and another.
To change size or shape you simply animate the length of a constraint.
While you are learning I would truly urge you to simply animate the constraints.
So, don't try to "change" constraints, simply animate the length of one or the other.
yourConstraint.constant = 99 // it's that easy
I also truly urge you to just lay it out on storyboard.
You must master that first!
Also there is no reason at all for the stack view, get rid of it for now.
Just have an outlet
#IBOutlet var yourConstraint: NSLayoutConstraint!
and animate
UIView.animateWithDuration(4.0) {
self.yourConstraint.constant = 666
self.view.layoutIfNeeded()
}
Be sure to search in Stack Overflow for "iOS animate constraints" as there is a great deal to learn.
Do this on storyboard, it will take 20 seconds tops for such a simple thing. Once you have mastered storyboard, only then move on to code if there is some reason to do so
Forget the stack view
Simply animate the constant of the constraint(s) to change the size, shape or anything you like.
Be sure to google for the many great articles "animating constraints in iOS" both here on SO and other sites!

Toggle Enabling UITextView Scroll After Max Number of Lines

I have a uitextview subview in an inputContainerView as it may appear in a messaging app.
Programmatically, on load, the height anchor of the uitextview is set when the inputContainerView is initialized. This constraint works great in increasing the container height as more lines of text are typed.
My goal is to cap the height of the uitextview after reaching X number of lines where any lines typed thereafter are scrollable. And likewise, when the number of lines falls below the max, it returns to its original form - not scrollable and auto-sizing height based on content.
After multiple trials and research, I've managed to get the height to fixate once hitting the max number of lines, but I cannot seem to figure out how to return it to its original form once the number of lines have fallen below the max. It appears the uitextview height stays stuck at the height the number of lines have maxed on.
Below is relevant code:
//Container View Initialization, onLoad Frame Height is set to 60
override init(frame: CGRect) {
super.init(frame: frame)
autoresizingMask = .flexibleHeight
addSubview(chatTextView)
textView.heightAnchor.constraint(greaterThanOrEqualToConstant: frame.height - 20).isActive = true
}
//TextView Delegate
var isTextViewOverMaxHeight = false
var textViewMaxHeight: CGFloat = 0.0
func textViewDidChange(_ textView: UITextView) {
let numberOfLines = textView.contentSize.height/(textView.font?.lineHeight)!
if Int(numberOfLines) > 5 {
if !isTextViewOverMaxHeight {
containerView.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 40).isActive = false
containerView.textView.heightAnchor.constraint(equalToConstant:
containerView.textView.frame.height).isActive = true
containerView.textView.isScrollEnabled = true
textViewMaxHeight = containerView.textView.frame.height
isTextViewOverMaxHeight = true
}
} else {
if isTextViewOverMaxHeight {
containerView.textView.heightAnchor.constraint(equalToConstant: textViewMaxHeight).isActive = false
containerView.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 40).isActive = true
containerView.textView.isScrollEnabled = false
isTextViewOverMaxHeight = false
}
}
}
You have overcomplicated a simple problem at hand, here is how can you achieve what you want
class ViewController: UIViewController {
var heightConstraint: NSLayoutConstraint!
var heightFor5Lines: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let textView = UITextView(frame: CGRect.zero)
textView.delegate = self
textView.backgroundColor = UIColor.red
textView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(textView)
textView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
textView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
textView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
heightConstraint = textView.heightAnchor.constraint(equalToConstant: 30.0)
heightConstraint.isActive = true
}
}
extension ViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
let numberOfLines = textView.contentSize.height/(textView.font?.lineHeight)!
if Int(numberOfLines) > 5 {
self.heightConstraint.constant = heightFor5Lines
} else {
if Int(numberOfLines) == 5 {
self.heightFor5Lines = textView.contentSize.height
}
self.heightConstraint.constant = textView.contentSize.height
}
textView.layoutIfNeeded()
}
}
How this works?:
Its simple, your textView starts off with some height (height constraint is necessary here because I haven't neither disabled its scroll nor have I provided bottom constraint, so it does not have enough data to evaluate its intrinsic height) and every time text changes you check for number of lines, as long as number of lines is less than threshold number of lines you keep increasing the height constraint of your textView so that its frame matches the contentSize (hence no scrolling) and once it hits the expected number of lines, you restrict height, so that textView frame is less than the actual content size, hence scrolls automatically
Hope this helps
It's also possible to disable initial textView's isScrollEnabled and then implement text view resizing like this:
private var heightConstraint: NSLayoutConstraint?
private let inputLinesScrollThreshold = 5
func textViewDidChange(_ textView: UITextView) {
let isConstraintActive = heightConstraint.flatMap { $0.isActive } ?? false
let lineHeight = textView.font?.lineHeight ?? 1
let linesCount = Int(textView.contentSize.height / lineHeight)
if isConstraintActive == false {
heightConstraint = textView.heightAnchor.constraint(equalToConstant: textView.frame.height)
heightConstraint?.isActive = true
textView.isScrollEnabled = true
} else {
heightConstraint?.constant = linesCount > inputLinesScrollThreshold ?
lineHeight * CGFloat(inputLinesScrollThreshold) : textView.contentSize.height
}
textView.layoutIfNeeded()
}

UIImageView in UIScrollView does not zoom correctly

I want to allow the user to side pan an image. The image should be scaled to the height of the device and the user is supposed to only be able to scroll left and right. The users is not supposed to be able to zoom.
I have a UIViewController, to which I add a custom subclass ImageScrollView.
This is supposed to display an image in full height, but instead the image is basically displayed un-zoomed. Even though the zoomScale gets calculated correctly, but does not have an effect.
What am I doing wrong?
Thanks
override func viewDidLoad() {
super.viewDidLoad()
let i = UIImage(named: "test.jpg")
let iSV = ImageScrollView(image: i)
self.view.addSubview(iSV)
iSV.fillSuperview()
}
class ImageScrollView: UIScrollView {
let image: UIImage
let imageView = UIImageView()
init(image img: UIImage) {
image = img
imageView.image = image
super.init(frame: .zero)
self.addSubview(imageView)
imageView.fillSuperview()
self.contentSize = image.size
self.showsHorizontalScrollIndicator = false
self.showsVerticalScrollIndicator = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.zoomScale = getZoomScale()
}
func getZoomScale() -> CGFloat{
let boundSize = self.frame.size
let yScale = boundSize.height / image.size.height
return yScale
}
}
And just for the case it has to do with auto-layout I included the fillSuperview extension.
extension UIView {
public func fillSuperview() {
translatesAutoresizingMaskIntoConstraints = false
if let superview = superview {
topAnchor.constraint(equalTo: superview.topAnchor, constant: 0).isActive = true
leftAnchor.constraint(equalTo: superview.leftAnchor, constant: 0).isActive = true
bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: 0).isActive = true
rightAnchor.constraint(equalTo: superview.rightAnchor, constant: 0).isActive = true
}
}
}
Since you don't want the image to zoom, I recommend you don't even bother with the zoom controls. A UIImageView knows how to scale its content.
I recommend you do it like this:
Add a constraint that sets the imageView height equal to the scrollView height. This will prevent vertical scrolling.
Add a constraint that sets the imageView width equal to the imageView height with multiplier image.size.width / image.size.height.
Set imageView content mode to .scaleToFill.
To allow you to change the image, keep an aspectRatio property that retains the aspect ratio constraint set in step 2. Set aspectRatio.isActive = false, and then create and activate a new constraint for the new image.
Also, if you might ever have images that aren't wide enough to fill the scrollView horizontally when scaled to fit vertically, consider these changes:
Replace the constraint that sets the imageView width with one that sets the width equal to the imageView height with multiplier max(image.size.width / image.size.height, scrollView.bounds.width / scrollView.bounds.height).
Set imageView content mode to .scaleAspectFit.
Then, when you have a narrow image, the imageView will fill the scrollView, but the .scaleAspectFit will show the entire image centered in the scrollView. This will still work correctly for wide images because the multiplier will match the image aspect ratio and .scaleAspectFit will fill the entire imageView.
You forgot to implement scrollview delegate. And set min & max zoom level for scrollview.
var iSV: ImageScrollView?
let i = UIImage(named: "noWiFi")!
iSV = ImageScrollView(image: i)
if let iSV = iSV {
self.view.addSubview(iSV)
iSV.fillSuperview()
iSV.delegate = self
}
override func viewDidLayoutSubviews() {
if let iSV = iSV {
let scale = iSV.getZoomScale()
iSV.minimumZoomScale = scale
iSV.maximumZoomScale = scale
}
}
extension ViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return iSV?.imageView
}
}
Note: I just did rough. It may not fulfil your complete requirement

UITextView height to change dynamically when typing / without using storyboards

I have this UITextView & I want it's height to change dynamically when the user is typing on it. I want to do it programmatically. I have the UITextView above another UIView. The constraints are set as below:
addtextview.leadingAnchor.constraint(equalTo: addtasktextview.leadingAnchor, constant: 8).isActive = true
addtextview.trailingAnchor.constraint(equalTo: addtasktextview.trailingAnchor, constant: -8).isActive = true
addtextview.topAnchor.constraint(equalTo: addtasktextview.topAnchor, constant: 8).isActive = true
addtextview.bottomAnchor.constraint(equalTo: addtasktextview.bottomAnchor, constant: -40).isActive = true
Interestingly enough, I am using textview in tableViewCells as well and dynamically changing the height by using just this constraint method, but over here it is not working.
I want the textview's height to increase in such a way that it moves upward. So when a new line starts, the top part should move maintaining the spacing below.
How can I do it? Help will be appreciated it.
UPDATE: I was able to get it working with #upholder-of-truth 's answer below. I was also able to dynamically change the parent UIView container height by finding the difference between the textview normal height and the newSize.height and then adding that difference to the container's height.
First make sure your class adopts the UITextViewDelegate protocol so you can be informed when the text changes like this:
class MyClass: UIViewContoller, UITextViewDelegate
Next define this variable somewhere in your class so that you can keep track of the height in a constraint:
var textHeightConstraint: NSLayoutConstraint!
Next add the following constraint and activate it:
self.textHeightConstraint = addtextview.heightAnchor.constraint(equalToConstant: 40)
self.textHeightConstraint.isActive = true
(if you don't do this in viewDidLoad you need to make textHeightConstraint an optional)
Next subscribe to the delegate (if not already done):
addTextView.delegate = self
Add this function which recalculates the height constraint:
func adjustTextViewHeight() {
let fixedWidth = addtextview.frame.size.width
let newSize = addtextview.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
self.textHeightConstraint.constant = newSize.height
self.view.layoutIfNeeded()
}
Next add a call to that function after the constraints are created to set the initial size:
self.adjustTextViewHeight()
Finally add this method to adjust the height whenever the text changes:
func textViewDidChange(_ textView: UITextView) {
self.adjustTextViewHeight()
}
Just in case that is all confusing here is a minimal example in a sub class of a UIViewController:
class ViewController: UIViewController, UITextViewDelegate {
#IBOutlet var textView: UITextView!
#IBOutlet var textHolder: UIView!
var textHeightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
textView.leadingAnchor.constraint(equalTo: textHolder.leadingAnchor, constant: 8).isActive = true
textView.trailingAnchor.constraint(equalTo: textHolder.trailingAnchor, constant: -8).isActive = true
textView.topAnchor.constraint(equalTo: textHolder.topAnchor, constant: 8).isActive = true
textView.bottomAnchor.constraint(equalTo: textHolder.bottomAnchor, constant: -40).isActive = true
self.textHeightConstraint = textView.heightAnchor.constraint(equalToConstant: 40)
self.textHeightConstraint.isActive = true
self.adjustTextViewHeight()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func textViewDidChange(_ textView: UITextView) {
self.adjustTextViewHeight()
}
func adjustTextViewHeight() {
let fixedWidth = textView.frame.size.width
let newSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
self.textHeightConstraint.constant = newSize.height
self.view.layoutIfNeeded()
}
}
After implementing the selected answer's solution from "Upholder Of Truth", I was wondering how to get rid of the weird text bottom offset that is brought by the changing of the height constraint.
A simple solution is setting this at the start of your initialisations:
textView.isScrollEnabled = false
Finally, if you need your textview to stop growing after a specific height, you can change the "adjustTextViewHeight" function to this:
func adjustTextViewHeight() {
let fixedWidth = growingTextView.frame.size.width
let newSize = growingTextView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
if newSize.height > 100 {
growingTextView.isScrollEnabled = true
}
else {
growingTextView.isScrollEnabled = false
textViewHeightConstraint.constant = newSize.height
}
view.layoutSubviews()
}
Hope this helps :)
There's a clever answer here in objective-c where you can detect a newline character in your textView, and adjust your textView's frame appropriately. Here is the swift version:
var previousPosition:CGRect = CGRect.zero
func textViewDidChange(_ textView: UITextView) {
let position:UITextPosition = textView.endOfDocument
let currentPosition:CGRect = textView.caretRect(for: position)
if(currentPosition.origin.y > previousPosition.origin.y){
/*Update your textView's height here*/
previousPosition = currentPosition
}
}
Make sure that the textView's view controller adopts the UITextViewDelegate and sets self.textView.delegate = self
Demo Here the textView grows down, but having it grow up is a matter of your constraints.

Adjusting height of UITextView to its text does not work properly

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.

Resources