Selecting a word in a UITextView

前端 未结 2 2027
再見小時候
再見小時候 2020-12-03 02:30

EDIT: I\'m thinking I should use a UILabel instead of a UITextView as I don\'t want the highlight to use the system wide blue with \'copy/select all/define\' popover.

<
相关标签:
2条回答
  • 2020-12-03 02:35

    UITextView already has a delegate method that is triggered when selection change (note that moving the cursor within the textview is equivalent to changing the selection, the user doesn't actually need to 'select' any text for this to be called):

    - (void)textViewDidChangeSelection:(UITextView *)textView
    

    Whenever this is triggered, get the selectedRange like this:

    NSRange range=textView.selectedRange;
    

    If the user should be able to manually move the cursor or select an entire word, then you're pretty much done, otherwise, just add some processing around the string at selectedRange to figure out what the word around the cursor is, and highlight it with your method of choice.
    You could for example enumerate all words in the textview, and figure out which one contains the current selection (or cursor), and select the entire word (which is a way of highlighting prior 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];
        }
    }];
    }
    
    0 讨论(0)
  • 2020-12-03 03:02

    Update

    This got a lot easier for iOS 7, thanks to the addition of NSLayoutManager in CoreText. If you're dealing with a UITextView you can access the layout manager as a property of the view. In my case I wanted to stick with a UILabel, so you have to create a layout manager with the same size, i.e:

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

    Now you just need to find the index of the character that was clicked, which is simple!

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

    Which makes it trivial to find the word itself:

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

    Original Answer, which works for iOS < 7

    Thanks to @JP Hribovsek for some tips getting this working, I managed to solve this well enough for my purposes. It feels a little hacky, and likely wouldn't work too well for large bodies of text, but for paragraphs at a time (which is what I need) it's fine.

    I created a simple UILabel subclass that allows me to set the inset value:

    #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)];
    }
    

    Then I created a UIView subclass that contained my custom label, and on tap constructed the size of the text for each word in the label, until the size exceeded that of the tap location - this is the word that was tapped. It's not prefect, but works well enough for now.

    I then used a simple NSAttributedString to highlight the text:

    #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;
                }
            }];        
        }
    }
    
    0 讨论(0)
提交回复
热议问题