Reputation: 12004
I was wondering how to limit the amount of LINES (not characters as asked in other questions) a user can enter when editing a UITextField.
Ideally, I would like to limit the input to max. 10 lines.
Where would I need to start? Do I do this with a method? In
- (BOOL)textViewShouldBeginEditing:(UITextView *)aTextView
Upvotes: 84
Views: 101111
Reputation: 885
Swift 5.7, iOS 16
Inside your UIViewRepresentable
wrapper for UITextView
(or UITextField
)
func makeUIView(context: Context) -> UITextView {
let textView = UITextView() // ..or MyCustomUITextView()
// ..your existing makeUIView body here..
// Limit to N = 3 visible (displayed) lines.
textView.textContainer.maximumNumberOfLines = 3
// When visible line count is exceeded typing can continue,
// but what's entered will be truncated so hidden from view.
textView.textContainer.lineBreakMode = .byTruncatingTail
// ..or prevent further text entry when limit is hit.
// See https://developer.apple.com/documentation/uikit/nslinebreakmode
//textView.textContainer.lineBreakMode = .byClipping
// Set delegate now that other changes are made.
textView.delegate = self
return textView
}
Upvotes: 3
Reputation: 155
Refer to APPLE documentation: Counting Lines of Text https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextLayout/Tasks/CountLines.html
- (void)textViewDidChangeSelection:(UITextView *)textView{
NSLayoutManager *layoutManager = [textView layoutManager];
unsigned numberOfLines, index, numberOfGlyphs =
(unsigned)[layoutManager numberOfGlyphs];
NSRange lineRange;
for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++){
(void) [layoutManager lineFragmentRectForGlyphAtIndex:index
effectiveRange:&lineRange];
index = (unsigned)NSMaxRange(lineRange);
}
if(numberOfLines > 10){
self.textField.text = self.lastString;
}
}
handle "\n"
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{
if([text containsString:@"\n"]){
NSLayoutManager *layoutManager = [textView layoutManager];
unsigned numberOfLines, index, numberOfGlyphs =
(unsigned)[layoutManager numberOfGlyphs];
NSRange lineRange;
for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++){
(void) [layoutManager lineFragmentRectForGlyphAtIndex:index
effectiveRange:&lineRange];
index = (unsigned)NSMaxRange(lineRange);
}
NSLog(@"lines = %d",numberOfLines);
if(numberOfLines >= 10){
return NO;
}
}
self.lastString = textView.text;
return YES;
}
Upvotes: 1
Reputation: 387
Swift 4
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let existingLines = textView.text.components(separatedBy: CharacterSet.newlines)
let newLines = text.components(separatedBy: CharacterSet.newlines)
let linesAfterChange = existingLines.count + newLines.count - 1
return linesAfterChange <= textView.textContainer.maximumNumberOfLines
}
And if you want to limit characters also:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let existingLines = textView.text.components(separatedBy: CharacterSet.newlines)
let newLines = text.components(separatedBy: CharacterSet.newlines)
let linesAfterChange = existingLines.count + newLines.count - 1
if(text == "\n") {
return linesAfterChange <= textView.textContainer.maximumNumberOfLines
}
let newText = (textView.text as NSString).replacingCharacters(in: range, with: text)
let numberOfChars = newText.count
return numberOfChars <= 30 // 30 characters limit
}
}
don't forget to add how many lines you want the limit to be in viewDidLoad
:
txtView.textContainer.maximumNumberOfLines = 2
Upvotes: 5
Reputation: 5647
Here's a improved Version of Numereyes answer in Swift 4.2 / Swift 5
I made a little extension so I can reuse the code. I'm using a While-Loop to check if the size fits. This also works when the user pastes a lot of text at once.
extension UITextView {
var numberOfCurrentlyDisplayedLines: Int {
let size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
//for Swift <=4.0, replace with next line:
//let size = systemLayoutSizeFitting(UILayoutFittingCompressedSize)
return Int(((size.height - layoutMargins.top - layoutMargins.bottom) / font!.lineHeight))
}
/// Removes last characters until the given max. number of lines is reached
func removeTextUntilSatisfying(maxNumberOfLines: Int) {
while numberOfCurrentlyDisplayedLines > (maxNumberOfLines) {
text = String(text.dropLast())
layoutIfNeeded()
}
}
}
// Use it in UITextView's delegate method:
func textViewDidChange(_ textView: UITextView) {
textView.removeTextUntilSatisfying(maxNumberOfLines: 10)
}
Upvotes: 7
Reputation: 2677
Maciek Czarnik answer does not worked for me, but it got me insights what to do.
iOS 7+
Swift
textView.textContainer.maximumNumberOfLines = 10
textView.textContainer.lineBreakMode = .byTruncatingTail
ObjC
textView.textContainer.maximumNumberOfLines = 10;
textView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
Upvotes: 266
Reputation: 13414
Similar to other answers, but usable directly from Storyboard and without subclassing:
extension UITextView {
@IBInspectable var maxNumberOfLines: NSInteger {
set {
textContainer.maximumNumberOfLines = maxNumberOfLines
}
get {
return textContainer.maximumNumberOfLines
}
}
@IBInspectable var lineBreakByTruncatingTail: Bool {
set {
if lineBreakByTruncatingTail {
textContainer.lineBreakMode = .byTruncatingTail
}
}
get {
return textContainer.lineBreakMode == .byTruncatingTail
}
}
}
Upvotes: 1
Reputation: 1151
The other solutions given do not solve an issue related to a last line being created at the end (an 11th line in the question's case).
Here is a working solution with Swift 4.0
& Xcode 9.0 beta (found on this blog post)
class ViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
textView.textContainer.maximumNumberOfLines = 10
textView.textContainer.lineBreakMode = .byWordWrapping
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let existingLines = textView.text.components(separatedBy: CharacterSet.newlines)
let newLines = text.components(separatedBy: CharacterSet.newlines)
let linesAfterChange = existingLines.count + newLines.count - 1
return linesAfterChange <= textView.textContainer.maximumNumberOfLines
}
Nota bene: This solution does not handle the scenario where the last line is too long to display (text will be hidden on the far right side of the UITextView).
Upvotes: 3
Reputation: 810
in Swift 3.0
version:
self.textView.textContainer.maximumNumberOfLines = self.textViewNumberOflines
self.textView.textContainer.lineBreakMode = .byTruncatingTail
Upvotes: 14
Reputation: 6181
Maybe this can help (iOS 7+):
textView.textContainer.maximumNumberOfLines = 10;
[textView.layoutManager textContainerChangedGeometry:textView.textContainer];
Even first line should do the trick I guess, but doesn't... Maybe its a bug in SDK
Upvotes: 59
Reputation: 938
Maciek Czarnik's answer does not seem to work for me, even in iOS7. It gives me strange behavior, I don't know why.
What I do to limit the number of lines in the UITextView is simply :
(tested only in iOS7) In the following UITextViewDelegate method :
- (void)textViewDidChange:(UITextView *)textView
{
NSUInteger maxNumberOfLines = 5;
NSUInteger numLines = textView.contentSize.height/textView.font.lineHeight;
if (numLines > maxNumberOfLines)
{
textView.text = [textView.text substringToIndex:textView.text.length - 1];
}
}
Upvotes: 12
Reputation: 94794
You have the right idea, but the wrong method. textView:shouldChangeTextInRange:replacementText:
is called whenever the text is going to change; you can access the current content of the text view using its text
property, and you can construct the new content from the passed range and replacement text with [textView.text stringByReplacingCharactersInRange:range withString:replacementText]
. You can then count the number of lines and return YES to allow the change or NO to reject it.
Upvotes: 15