I want size a UITextView dynamically to its contents by restricting both its width and its height to some maximums.
In particular, the textView should have a minimum width and height if there is little data (e.g., "Lorem"). As the data grows (e.g., "Lorem ipsum") it should expand in width as until it hits the maximum width ("Lorem ipsum dolor sit"), and then expand in height until it hits that maximum height. Once it goes beyond the max width and height it should become scrollable.
So far, I am struggling to get just the height working. I have experimented with a number of methods and, while this seemed the simplest and most promising, it just crashes.
func textViewDidChange(_ textView: UITextView ) {
resizeTextView(txtview1)
}
#IBOutlet weak var txtViewHeight: NSLayoutConstraint!
func resizeTextView(_ textView: UITextView ) {
let minHeight: CGFloat = 20.0
let maxHeight: CGFloat = 40.0
txtViewHeight.constant = min(maxHeight, max(minHeight, textView.contentSize.height))
self.view.layoutIfNeeded()
}
So, I changed the resize function to this - which kinda, sorta, sometimes works (sometimes the values show up in the text field, sometimes it doesn't display at all, sometimes the spacing within the text field is different):
func resizeTextView(_ textView: UITextView ) {
let currentHeight = textView.contentSize.height
let minHeight: CGFloat = 20.0
let maxHeight: CGFloat = 40.0
if currentHeight > maxHeight {
textView.frame.size.height = currentHeight
textView.isScrollEnabled = true
}
else if currentHeight < minHeight {
textView.frame.size.height = minHeight
textView.isScrollEnabled = false
}
else {
textView.frame.size.height = currentHeight
textView.isScrollEnabled = false
}
self.view.layoutIfNeeded()
}
I know this question has been asked an answered a dozen or more times, but none of the solutions are working for me (the above being two of the half dozen or more I have tried).
I think it is better to achieve this with Timer.
You solution above is that, UITextView's delegate methods triggers your height changing method.
Your changing method affects the UITextView's delegate method under the hood.
In other words, it is weird that you observe UITextView's property, then do something and affect the UITextView's property,
then the observation triggers again.
to achieve this with Timer, then the height changing events is from your timer source, not from Apple's UIKit
start editing, timer run
end editing, timer gone
Related
I have a tableview cell containing a custom view among other views and autolayout is used.
The purpose of my custom view is to layout its subviews in rows and breaks into a new row if the current subview does not fit in the current line. It kind of works like a multiline label but with views. I achieved this through exact positioning instead of autolayout.
Since I only know the width of my view in layoutSubviews(), I need to calculate the exact positions and number of lines there. This worked out well, but the frame(zero) of my view didn't match because of missing intrinsicContentSize.
So I added a check to the end of my calculation if my height changed since the last layout pass. If it did I update the height property which is used in my intrinsicContentSize property and call invalidateIntrinsicContentSize().
I observed that initially layoutSubviews() is called twice. The first pass works well and the intrinsicContentSize is taken into account even though the width of the cell is smaller than it should be. The second pass uses the actual width and also updates the intrinsicContentSize. However the parent(contentView in tableview cell) ignores this new intrinsicContentSize.
So basically the result is that the subviews are layout and drawn correctly but the frame of the custom view is not updated/used in parent.
The question:
Is there a way to notify the parent about the change of the intrinsic size or a designated place to update the size calculated in layoutSubviews() so the new size is used in the parent?
Edit:
Here is the code in my custom view.
FYI: 8 is just the vertical and horizontal space between two subviews
class WrapView : UIView {
var height = CGFloat.zero
override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: height)
}
override func layoutSubviews() {
super.layoutSubviews()
guard frame.size.width != .zero else { return }
// Make subviews calc their size
subviews.forEach { $0.sizeToFit() }
// Check if there is enough space in a row to fit at least one view
guard subviews.map({ $0.frame.size.width }).max() ?? .zero <= frame.size.width else { return }
let width = frame.size.width
var row = [UIView]()
// rem is the remaining space in the current row
var rem = width
var y: CGFloat = .zero
var i = 0
while i < subviews.count {
let view = subviews[i]
let sizeNeeded = view.frame.size.width + (row.isEmpty ? 0 : 8)
let last = i == subviews.count - 1
let fit = rem >= sizeNeeded
if fit {
row.append(view)
rem -= sizeNeeded
i += 1
guard last else { continue }
}
let rowWidth = row.map { $0.frame.size.width + 8 }.reduce(-8, +)
var x = (width - rowWidth) * 0.5
for vw in row {
vw.frame.origin = CGPoint(x: x, y: y)
x += vw.frame.width + 8
}
y += row.map { $0.frame.size.height }.max()! + 8
rem = width
row = []
}
if height != y - 8 {
height = y - 8
invalidateIntrinsicContentSize()
}
}
}
After a lot of trying and research I finally solved the bug.
As #DonMag mentioned in the comments the new size of the cell wasn't recognized until a new layout pass. This could be verified by scrolling the cell off-screen and back in which showed the correct layout. Unfortunately it is harder than expected to trigger new pass as .beginUpdates() + .endUpdates()didn't
do the job.
Anyway I didn't find a way to trigger it but I followed the instructions described in this answer. Especially the part with the prototype cell for the height calculation provided a value which can be returned in tableview(heightForRowAt:).
Swift 5:
This is the code used for calculation:
let fitSize = CGSize(width: view.frame.size.width, height: .zero)
/* At this point populate the cell with the exact same data as the actual cell in the tableview */
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
cell.bounds = CGRect(x: .zero, y: .zero, width: view.frame.size.width, height: cell.bounds.height)
cell.setNeedsLayout()
cell.layoutIfNeeded()
height = headerCell.contentView.systemLayoutSizeFitting(fitSize).height + 1
The value is only calculated once and the cached as the size doesn't change anymore in my case.
Then the value can be returned in the delegate:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
indexPath.row == 0 ? height : UITableView.automaticDimension
}
I only used for the first cell as it is my header cell and there is only one section.
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 have tried and failed to resize a UITextField that resides inside an UIView, that is set as the inputAccessoryView of that text field.
Now I have been trying to get the resizing to work. When the user enters text of 2 lines or more, the textfield should resize, and so should the surrounding view (inputAccessoryView).
I have the following code in my UITextFieldDelegate:
func textViewDidChange(textView: UITextView) {
println("textViewDidChange called")
sendButton.enabled = textView.hasText()
var oldHeight = textView.frame.height
var textWidth = textView.frame.width
textView.sizeToFit()
var textFrame = textView.frame
textFrame.size.height = textView.contentSize.height
textFrame.size.width = textWidth
textView.frame = textFrame
var newHeight = textView.frame.height
moveToolbarUp(newHeight - oldHeight)
self.frame.size.height += newHeight - oldHeight
self.superview?.updateConstraints()
}
Now when I type too much in the textfield, this happens. How can I make sure this does not happen? Also, the complete inputAccessoryView resets when I press the return key. Should I be using another method in the delegate? Should I not be calculating these heights myself? Why doesn't my inputAccessoryView resize properly, but only my textView?
As I was using Autolayout, I should have used that... It works by setting a height constraint on the UITextField, which I then change with these calculations...
How can a textField be resized based on content while using auto-layout in an iOS application written in Swift?
The text field will resize as necessary to fit its content when the view loads as well as while the user is typing.
Ideally, the text field would stop resizing at a certain point, say, 6 lines, and become scrollable.
You have to use an UITextView instead of an UITextField.
Then, you can use the sizeThatFits method.
But first you have to know how high one line will be. You can get that information by using lineHeight:
var amountOfLinesToBeShown: CGFloat = 6
var maxHeight: CGFloat = yourTextview.font.lineHeight * amountOfLinesToBeShown
After that, just call the sizeThatFits method inside your viewDidLoad method and set the maxHeight (line * 6) as your textview height:
yourTextview.sizeThatFits(CGSizeMake(yourTextview.frame.size.width, maxHeight))
Swift 3
var textView : UITextView!
override func viewDidLoad() {
super.viewDidLoad()
textView = UITextView()
textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: textView.frame.size.height))
}
I'm trying to have a UIViewController to display some text centered vertically in a UITextView. When app is launched, the text is aligned to the top. But if I try to select some text, the text would then move to the center vertically. It seems like setting contentOffset doesn't do anything at the first time, unless there's some action to trigger it to readjust the layout (like selecting some text in my scenario).
Also when I log the values of height, size, contentHeight and topCorrect, the values from the first call is the same as the second call. So I'm getting the same value but UITextView somehow is not responding to the contentOffset the very first time.
I even tried moving the logic to viewDidLayoutSubviews() and viewDidAppear(). I had the same result. So I don't think it's a problem of having the logic in the wrong place.
I think some people have success with it on iOS 7 and Obj-C. I'm trying get it running on iOS 8 and Swift.
Any suggestion or help would be very much appreciated.
class PageContentViewController : UIViewController {
#IBOutlet weak var textView: UITextView!
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.textView.textContainerInset = UIEdgeInsetsMake(0, 16.0, 0, 16.0)
let height = self.textView.bounds.size.height
let size = CGSizeMake(self.textView.frame.width, CGFloat.max)
var contentHeight = self.textView.sizeThatFits(size).height
var topCorrect = (height - contentHeight * self.textView.zoomScale) / 2.0
topCorrect = topCorrect < 0.0 ? 0.0 : topCorrect;
self.textView.setContentOffset(CGPointMake(0, -topCorrect), animated: false)
}
}