Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITextView's text going beyond bounds

I have a non-scrollable UITextView with it's layoutManager maximumNumberOfLines set to 9, which works fine, but, I cannot seem to find a method in NSLayoutManager that restricts the text to not go beyond the frame of the UITextView.

Take for example in this screenshot, the cursor is on the 9th line (the 1st line is clipped at top of screenshot, so disregard that). If the user continues to type new characters, spaces, or hit the return key, the cursor continues off screen and the UITextView's string continues to get longer.

enter image description here

I don't want to limit the amount of characters of the UITextView, due to foreign characters being different sizes.

I've been trying to fix this for several weeks; I'd greatly appreciate any help.

CustomTextView.h

#import <UIKit/UIKit.h>

@interface CustomTextView : UITextView <NSLayoutManagerDelegate>

@end

CustomTextView.m

#import "CustomTextView.h"

@implementation CustomTextView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        self.backgroundColor = [UIColor clearColor];
        self.font = [UIFont systemFontOfSize:21.0];
        self.dataDetectorTypes = UIDataDetectorTypeAll;
        self.layoutManager.delegate = self;
        self.tintColor = [UIColor companyBlue];
        [self setLinkTextAttributes:@{NSForegroundColorAttributeName:[UIColor companyBlue]}];
        self.scrollEnabled = NO;
        self.textContainerInset = UIEdgeInsetsMake(8.5, 0, 0, 0);
        self.textContainer.maximumNumberOfLines = 9;
    }
    return self;
}

- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect
{
    return 4.9;
}

@end

Update, still not resolved

like image 893
klcjr89 Avatar asked Feb 19 '14 18:02

klcjr89


6 Answers

You can check the size of the bounding rectangle and if it is too big call the undo manager to undo the last action. Could be a paste operation or enter in text or new line character.

Here is a quick hack that checks if the height of the text is too close to the height of the textView. Also checks that the textView rect contains the text rect. You might need to fiddle with this some more to suit your needs.

-(void)textViewDidChange:(UITextView *)textView {
    if ([self isTooBig:textView]) {
        FLOG(@" too big so undo");
        [[textView undoManager] undo];
    }
}
/** Checks if the frame of the selection is bigger than the frame of the textView
 */
- (bool)isTooBig:(UITextView *)textView {
    FLOG(@" called");

    // Get the rect for the full range
    CGRect rect = [textView.layoutManager usedRectForTextContainer:textView.textContainer];

    // Now convert to textView coordinates
    CGRect rectRange = [textView convertRect:rect fromView:textView.textInputView];
    // Now convert to contentView coordinates
    CGRect rectText = [self.contentView convertRect:rectRange fromView:textView];

    // Get the textView frame
    CGRect rectTextView = textView.frame;

    // Check the height
    if (rectText.size.height > rectTextView.size.height - 16) {
        FLOG(@" rectText height too close to rectTextView");
        return YES;
    }

    // Find the intersection of the two (in the same coordinate space)
    if (CGRectContainsRect(rectTextView, rectText)) {
        FLOG(@" rectTextView contains rectText");
        return NO;
    } else
        return YES;
}

ANOTHER OPTION - here we check the size and if its too big prevent any new characters being typed in except if its a deletion. Not pretty as this also prevents filling a line at the top if the height is exceeded.

bool _isFull;

-(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    FLOG(@" called");

    // allow deletes
    if (text.length == 0)
        return YES;

    // Check if the text exceeds the size of the UITextView
    if (_isFull) {
        return NO;
    }

    return YES;
}
-(void)textViewDidChange:(UITextView *)textView {
    FLOG(@" called");
    if ([self isTooBig:textView]) {
        FLOG(@" text is too big!");
        _isFull = YES;
    } else {
        FLOG(@" text is not too big!");
        _isFull = NO;
    }
}

/** Checks if the frame of the selection is bigger than the frame of the textView
 */
- (bool)isTooBig:(UITextView *)textView {
    FLOG(@" called");

    // Get the rect for the full range
    CGRect rect = [textView.layoutManager usedRectForTextContainer:textView.textContainer];

    // Now convert to textView coordinates
    CGRect rectRange = [textView convertRect:rect fromView:textView.textInputView];
    // Now convert to contentView coordinates
    CGRect rectText = [self.contentView convertRect:rectRange fromView:textView];

    // Get the textView frame
    CGRect rectTextView = textView.frame;

    // Check the height
    if (rectText.size.height >= rectTextView.size.height - 10) {
        return YES;
    }

    // Find the intersection of the two (in the same coordinate space)
    if (CGRectContainsRect(rectTextView, rectText)) {
        return NO;
    } else
        return YES;
}
like image 90
Duncan Groenewald Avatar answered Nov 18 '22 18:11

Duncan Groenewald


boundingRectWithSize:options:attributes:context: is not recommended for textviews, because it does not take various attributes of the textview (such as padding), and thus return an incorrect or imprecise value.

To determine the textview's text size, use the layout manager's usedRectForTextContainer: with the textview's text container to get a precise rectangle required for the text, taking into account all required layout constraints and textview quirks.

CGRect rect = [self.textView.layoutManager usedRectForTextContainer:self.textView.textContainer];

I would recommend doing this in processEditingForTextStorage:edited:range:changeInLength:invalidatedRange:, after calling the super implementation. This would mean replacing the textview's layout manager by providing your own text container and setting its layout manager to your subclass' instance. This way you can commit the changes from the textview made by the user, check if the rect is still acceptable and undo if not.

like image 3
Léo Natan Avatar answered Nov 18 '22 17:11

Léo Natan


You will need to do this yourself. Basically it would work like this:

  1. In your UITextViewDelegate's textView:shouldChangeTextInRange:replacementText: method find the size of your current text (NSString sizeWithFont:constrainedToSize: for example).
  2. If the size is larger than you allow return FALSE, otherwise return TRUE.
  3. Provide your own feedback to the user if they type something larger than you allow.

EDIT: Since sizeWithFont: is deprecated use boundingRectWithSize:options:attributes:context:

Example:

NSString *string = @"Hello World"; 

UIFont *font = [UIFont fontWithName:@"Helvetica-BoldOblique" size:21];

CGSize constraint = CGSizeMake(300,NSUIntegerMax);

NSDictionary *attributes = @{NSFontAttributeName: font};

CGRect rect = [string boundingRectWithSize:constraint 
                                   options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)  
                                attributes:attributes 
                                   context:nil];
like image 2
Patrick Tescher Avatar answered Nov 18 '22 16:11

Patrick Tescher


I created a test VC. It increases a line counter every time a new line is reached in the UITextView. As I understand you want to limit your text input to no more than 9 lines. I hope this answers your question.

#import "ViewController.h"

@interface ViewController ()

@property IBOutlet UITextView *myTextView;

@property CGRect previousRect;
@property int lineCounter;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

[self.myTextView setDelegate:self];

self.previousRect = CGRectZero;
self.lineCounter = 0;
}

- (void)textViewDidChange:(UITextView *)textView {
UITextPosition* position = textView.endOfDocument;

CGRect currentRect = [textView caretRectForPosition:position];

if (currentRect.origin.y > self.previousRect.origin.y){
    self.lineCounter++;
    if(self.lineCounter > 9) {
        NSLog(@"Reached line 10");
        // do whatever you need to here...
    }
}
self.previousRect = currentRect;

}

@end
like image 1
sangony Avatar answered Nov 18 '22 17:11

sangony


There is a new Class in IOS 7 that works hand in hand with UITextviews which is the NSTextContainer Class

It works with UITextview through the Textviews text container property

it has this property called size ...

size Controls the size of the receiver’s bounding rectangle. Default value: CGSizeZero.

@property(nonatomic) CGSize size Discussion This property defines the maximum size for the layout area returned from lineFragmentRectForProposedRect:atIndex:writingDirection:remainingRect:. A value of 0.0 or less means no limitation.

I am still in the process of understanding it and trying it out but I believe it should resolve your issue.

like image 1
Paulo Avatar answered Nov 18 '22 17:11

Paulo


No need to find number of lines. We can get all these things by calculating the cursor position from the textview and according to that we can minimize the UIFont of UITextView according to the height of UITextView.

Here is below link.Please refer this. https://github.com/jayaprada-behera/CustomTextView

like image 1
Jayaprada Avatar answered Nov 18 '22 17:11

Jayaprada