UITextView's text going beyond bounds

陌路散爱 提交于 2019-12-18 12:14:13

问题


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.

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


回答1:


Here is a better answer I think. Whenever the shouldChangeTextInRange delegate method is called we call our doesFit:string:range function to see whether the resulting text height exceeds the view height. If it does we return NO to prevent the change from taking place.

-(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
    return [self doesFit:textView string:text range:range];

}
- (float)doesFit:(UITextView*)textView string:(NSString *)myString range:(NSRange) range;
{
    // Get the textView frame
    float viewHeight = textView.frame.size.height;
    float width = textView.textContainer.size.width;

    NSMutableAttributedString *atrs = [[NSMutableAttributedString alloc] initWithAttributedString: textView.textStorage];
    [atrs replaceCharactersInRange:range withString:myString];

    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:atrs];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize: CGSizeMake(width, FLT_MAX)];
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];

    [layoutManager addTextContainer:textContainer];
    [textStorage addLayoutManager:layoutManager];
    float textHeight = [layoutManager
            usedRectForTextContainer:textContainer].size.height;
    FLOG(@" viewHeight = %f", viewHeight);
    FLOG(@" textHeight = %f", textHeight);

    if (textHeight >= viewHeight - 1) {
        FLOG(@" textHeight >= viewHeight - 1");
        return NO;
    } else
        return YES;
}

EDIT OK you will also need to add some checks if you change the format of the text. In my case the user can change the font or make it bold, change paragraph style, etc.. So now any of these changes could also cause the text to exceed the textView borders.

So first you need to make sure you are registering these changes with the textViews undoManager. See below for an example (I just copy the whole attributedString so I can put it back if undo is called).

// This is in my UITextView subclass but could be anywhere

// This gets called to undo any formatting changes 
- (void)setMyAttributedString:(NSAttributedString*) atstr {
    self.attributedText = atstr;
    self.selectedRange = _undoSelection;
}
// Before we make any format changes save the attributed string with undoManager
// Also save the current selection (maybe should save this with undoManager as well using a custom object containing selection and attributedString)
- (void)formatText:(id)sender {
    //LOG(@"formatText: called");
    NSAttributedString *atstr = [[NSAttributedString alloc] initWithAttributedString:self.textStorage];
    [[self undoManager] registerUndoWithTarget:self
                               selector:@selector(setMyAttributedString:)
                                 object:atstr];
    // Remember selection
    _undoSelection = self.selectedRange;

   // Add text formatting attributes
   ...
   // Now tell the delegate that something changed
   [self.delegate textViewDidChange:self];
}

Now check the size in the delegate and undo if it does not fit.

-(void)textViewDidChange:(UITextView *)textView {
    FLOG(@" called");
    if ([self isTooBig:textView]) {
        FLOG(@" text is too big so undo it!");
        @try {
            [[textView undoManager] undo];
        }
        @catch (NSException *exception) {
            FLOG(@" exception undoing things %@", exception);
        }
    }
}



回答2:


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.




回答3:


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];



回答4:


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



回答5:


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;
}



回答6:


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.




回答7:


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



来源:https://stackoverflow.com/questions/21889657/uitextviews-text-going-beyond-bounds

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!