UITextView的文本超出了边界

19

我有一个不可滚动的UITextView,它的layoutManager maximumNumberOfLines设置为9,这很好用,但是我无法在NSLayoutManager中找到一种方法来限制文本不超出UITextView的框架。

例如,在此屏幕截图中,光标位于第9行(第1行被剪切在屏幕截图的顶部,所以请忽略它)。如果用户继续键入新字符、空格或按下回车键,光标会继续超出屏幕,UITextView的字符串也会变得越来越长。

enter image description here

我不想限制UITextView的字符数量,因为外语字符的大小不同。

我已经试图解决这个问题几周了,非常感谢任何帮助。

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

更新:问题仍未解决


1
我会做的就是加载一个UIWebView,从Xcode文件中提取一个包含<textfield>且具有限制功能的JavaScript函数的.html文件。然后使用JavaScript注入来查询输入。同时通过JavaScript和Objective-C将UIWebView的背景颜色设置为透明,这样它看起来不像是加载了网站,而是一个UITextView(尽管它并不是一个UITextView)...当然这不是一个“答案”,所以我把它放在了这个问题的评论中。 - Albert Renshaw
UITextView,特别是在iOS 7中有许多已知的错误。您应该考虑使用PSPDFTextView,看看是否可以解决问题。 - Holly
我不认为我的问题与错误有关,因为我遇到的相同问题可以追溯到iOS 5和6。 - klcjr89
只是想说这个问题仍未解决。 - klcjr89
如果您不想限制字符数量,那么这将非常困难。有没有特定的原因使其无法滚动?问题在于不能滚动且字符无限制的组合。您必须在其他地方设置限制,否则它永远不会起作用。您不能在预定义的空间中放置无限项。 - ophychius
可能是Text scrolls outside of the UITextView box boundary的重复问题。 - Najam
7个回答

8

我认为这里有一个更好的答案。每次调用shouldChangeTextInRange代理方法时,我们调用doesFit:string:range函数来检查结果文本高度是否超过视图高度。如果是,则返回NO以防止更改发生。

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

编辑 好的,如果更改文本格式,则还需要添加一些检查。在我的情况下,用户可以更改字体或使其加粗,更改段落样式等。因此,现在任何这些更改也可能导致文本超出textView边界。

首先,您需要确保使用textView的undoManager注册这些更改。以下是一个示例(我只是复制整个attributedString,以便在调用撤消时可以将其放回)。

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

现在在委托中检查大小,如果大小不合适,则撤销。
-(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);
        }
    }
}

Duncan,我无法使用你的代码解决我的问题;而且,我不允许用户更改文本的格式。有没有办法向您展示我想要实现的内容?非常感谢您的帮助。 - klcjr89
请将其压缩并通过电子邮件发送给我,我的邮箱是duncan.groenewald@ossh.com.au - Duncan Groenewald
我投票支持你的答案,因为它非常好!还要考虑使用inset更好!(textHeight < viewHeight-1-textView.textContainerInset.top-textView.textContainerInset.bottom) - Codus

3

boundingRectWithSize:options:attributes:context: 不建议用于文本视图,因为它不考虑文本视图的各种属性(例如填充),从而返回不正确或不精确的值。

要确定文本视图的文本大小,请使用布局管理器的 usedRectForTextContainer: 与文本视图的文本容器一起使用,以获取所需的精确矩形,考虑到所有必需的布局约束和文本视图的特殊性。

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

我建议在调用super实现之后,在processEditingForTextStorage:edited:range:changeInLength:invalidatedRange:中执行此操作。这意味着通过提供自己的文本容器并将其布局管理器设置为子类实例来替换文本视图的布局管理器。这样,您可以提交用户对文本视图所做的更改,检查矩形是否仍然可接受,如果不行,则可以撤消。


我不确定如何实现这个。 - klcjr89
1
当提交更改到文本存储时,将调用@troop231 processEditingForTextStorage:edited:range:changeInLength:invalidatedRange:。您要实现的是检查这些更改是否可接受,如果不可接受,则撤消它们。要捕获processEditingForTextStorage:edited:range:changeInLength:invalidatedRange:,您需要子类化NSLayoutManagerUITextView有一个新的初始化方法:initWithFrame:textContainer:,您需要使用它。 - Léo Natan
我创建了一个NSLayoutManager的子类,但不知道如何设置我的CustomTextView类来使用它。 - klcjr89
1
@troop231 请看这里如何使用自定义布局管理器:https://dev59.com/C3rZa4cB1Zd3GeqP8OSU#20916091 - Léo Natan
1
@troop231 不需要,只需使用您自己的实例即可将其布局管理器设置为您的自定义管理器。 - Léo Natan
显示剩余4条评论

2
你需要自己完成这个任务。基本上它的工作方式如下:
1. 在你的UITextViewDelegate的textView:shouldChangeTextInRange:replacementText:方法中,查找当前文本的大小(例如使用NSString sizeWithFont:constrainedToSize:)。
2. 如果大小超过你允许的范围,则返回FALSE;否则返回TRUE。
3. 如果用户输入的内容超过了你允许的范围,请提供自己的反馈。
编辑:由于sizeWithFont:已经被弃用,请使用boundingRectWithSize:options:attributes:context:
示例:
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];

你能提供一个例子吗?我相信 sizeWithFont 方法已经被弃用了。这还要考虑到新的换行符和文本视图的内置文本换行。 - klcjr89
请查看我在问题上面的更新,看看我是否走在正确的轨道上?谢谢。 - klcjr89
2
在文本视图中使用boundingRectWithSize:...是一个不好的主意。 - Léo Natan

1
你可以检查边界矩形的大小,如果太大,则调用撤销管理器撤销上一次操作。这可能是粘贴操作或输入文本或换行符。
这里有一个快速技巧,检查文本的高度是否太接近textView的高度。还检查textView矩形是否包含文本矩形。您可能需要进一步调整以适应您的需求。
-(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;
}

另一种选择-在这里我们检查大小,如果太大,则防止输入任何新字符,除非是删除。 这并不美观,因为它还防止了在顶部填充行(如果超过高度)。

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

1
是的,@LeoNatan 是正确的,只需使用 textView.superView 或 self.view。我使用可滚动表单,因此 contentView 是放置 UI 控件的视图,它本身位于 scrollView 中。 - Duncan Groenewald
我不确定我能否让这个工作起来。16的值会有很大的影响吗?因为当我到达第8行并且文本即将换行到第9行时,该方法被调用了,但它应该在第9行的末尾被调用(我指的是1-9行,而不是0-9行)。 - klcjr89
1
你需要稍微调整一下,找出哪些是有效的。将其设置为0并查看。我认为你需要考虑字母之间的差异,例如a和p,其中p有一个下降 - 因此,如果用户在最后一行键入a,则可能适合,但如果他们接着键入p,则不会适合。因此,最好获取您正在使用的特定字体和字体大小的最大高度,而不是硬编码值。 - Duncan Groenewald
嘿,邓肯,检查文本视图中的文本是否适合基于宽度而不是高度会更好,这样你提到的a和p字母大小就不会有影响了。 - klcjr89
1
问题在于你只能在编辑完成后确定大小,然后你必须撤消。不幸的是,似乎撤消管理器不能一次撤消一个字符。另一个选项是拥有一个隐藏的TextView,将编辑从shouldChangeTextInRange传递给它,然后使用它来计算大小。如果太大,就返回NO。 - Duncan Groenewald
显示剩余5条评论

1

我创建了一个测试VC。每当UITextView达到新的一行时,它就会增加一条线路计数器。据我所知,您希望将文本输入限制在不超过9行。希望这回答了您的问题。

#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

1
还没有能够让这个工作起来。您能确认一下,如果用户输入2行文本,然后将光标移动到第一行的开头,然后按回车键,那么前面的2行文本会全部移动到文本视图底部,并在第2行到达文本视图底部后停止吗? - klcjr89
我并没有运行所有可能的情况。我的理解是,当达到一定数量的行时,您需要某种提示。 - sangony
我有几个场景需要解决,这是一个很头疼的问题。禁止用户使用回车键可以很容易地解决这个问题,但这不是一个良好的体验。 - klcjr89
请确保在您的.h文件中设置了<UITextViewDelegate>。 - sangony
好吧,我不知道该说什么...这段代码准确地计算每次使用新行。我用换行和回车键进行了测试。你也可以试一下。 - sangony
今天一整天都很忙,还没来得及尝试。 - klcjr89

1
在IOS 7中有一个新的类与UITextviews紧密配合使用,这个类叫做NSTextContainer Class。 它通过Textviews的text container属性与UITextview配合使用。它拥有一个被称为size的属性... size:控制接收器的边界矩形的大小。默认值:CGSizeZero。 @property(nonatomic) CGSize size 讨论:此属性定义了从lineFragmentRectForProposedRect:atIndex:writingDirection:remainingRect:返回的布局区域的最大尺寸。0.0或更小的值表示没有限制。 我仍在尝试理解和尝试它,但我相信它应该可以解决您的问题。

1
不需要查找行数。 我们可以通过从textview计算光标位置来获取所有这些内容,并根据此最小化UITextView的UIFont,以适应UITextView的高度。
以下是链接,请参考。 https://github.com/jayaprada-behera/CustomTextView

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接