Search code examples
ioshyperlinkuitextviewuigesturerecognizernsmutableattributedstring

NSMutableAttributedString on UITextView not clickable


I have a UITextView and String is set with an attributed text that says "See More". I have also added the gesture for the UITextView. Gesture works when the click is performed on any other place but not when tapped on "See More".

I need the click to respond on "See More" click.

Below is how I have implemented:

//link text
 NSDictionary *linkAttributes = @{ NSForegroundColorAttributeName : [UIColor colorWithRed:0.05 green:0.4 blue:0.65 alpha:1.0],
                                                  NSFontAttributeName : [UIFont fontWithName:@"Roboto-Regular" size:10.0],
                                                  NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle) };

[attributedString setAttributes:linkAttributes range:linkRange];

self.classDesc.userInteractionEnabled = YES;
[self.classDesc addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleExpand:)]];
self.classDesc.selectable = YES;
 // Assign attributedText to UILabel
 self.classDesc.attributedText = attributedString;

Below is the gesture listener:

- (void)handleExpand:(UITapGestureRecognizer *)tapGesture
{
    self.classDesc.text = selectedClasses.classDescription;
    [self textViewDidChange:self.classDesc];

       CGSize sizeThatFitsTextView = [self.classDesc sizeThatFits:CGSizeMake(self.classDesc.frame.size.width, MAXFLOAT)];
    self.descHeight.constant = sizeThatFitsTextView.height ;


}

Any help is greatly appreciated.


Solution

  • You can use UILabel for this, Here's how to do,

    First create a category class for UITapGestureRecognizer and add below method,

    /**
     Returns YES if the tap gesture was within the specified range of the attributed text of the label.
     */
    - (BOOL)didTapAttributedTextInLabel:(UILabel *)label inRange:(NSRange)targetRange {
        NSParameterAssert(label != nil);
    
        CGSize labelSize = label.bounds.size;
        // create instances of NSLayoutManager, NSTextContainer and NSTextStorage
        NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
        NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
        NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];
    
        // configure layoutManager and textStorage
        [layoutManager addTextContainer:textContainer];
        [textStorage addLayoutManager:layoutManager];
    
        // configure textContainer for the label
        textContainer.lineFragmentPadding = 0.0;
        textContainer.lineBreakMode = label.lineBreakMode;
        textContainer.maximumNumberOfLines = label.numberOfLines;
        textContainer.size = labelSize;
    
        // find the tapped character location and compare it to the specified range
        CGPoint locationOfTouchInLabel = [self locationInView:label];
        CGRect textBoundingBox = [layoutManager usedRectForTextContainer:textContainer];
        CGPoint textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                                  (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
        CGPoint locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                                                             locationOfTouchInLabel.y - textContainerOffset.y);
        NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInTextContainer
                                                           inTextContainer:textContainer
                                  fractionOfDistanceBetweenInsertionPoints:nil];
        if (NSLocationInRange(indexOfCharacter, targetRange)) {
            return YES;
        } else {
            return NO;
        }
    }
    

    Implementation

    //Global variables
    NSString *longString = @"very very long string";
    NSRange moreRange;
    NSRange lessRange;
    
    -(void)setReadLessDescriptionText{
        self.lblDescription.userInteractionEnabled = YES;
        [self.lblDescription addGestureRecognizer:
         [[UITapGestureRecognizer alloc] initWithTarget:self
                                                 action:@selector(handleTapMore:)]];
    
        // create your attributed text and keep an variable of your "link" text range
        NSAttributedString *plainText;
        NSAttributedString *linkText;
        plainText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ ...", [longString substringWithRange:NSMakeRange(0, 200)]] attributes:nil];//here 200 is  your character limit, it can be any value depending your requirement
        linkText = [[NSMutableAttributedString alloc] initWithString:@"more"
                                                          attributes:@{
                                                                       NSForegroundColorAttributeName:[UIColor blueColor]
                                                                       }];
        NSMutableAttributedString *attrText = [[NSMutableAttributedString alloc] init];
        [attrText appendAttributedString:plainText];
        [attrText appendAttributedString:linkText];
    
        // Variable -- keep track of the target range so you can compare in the callback
            moreRange = NSMakeRange(plainText.length, linkText.length);
    
        self.lblDescription.attributedText = attrText;
    
    }
    
    -(void)setReadMoreDescriptionText{
        self.lblDescription.userInteractionEnabled = YES;
        [self.lblDescription addGestureRecognizer:
         [[UITapGestureRecognizer alloc] initWithTarget:self
                                                 action:@selector(handleTapLess:)]];
    
        // create your attributed text and keep an variable of your "link" text range
        NSAttributedString *plainText;
        NSAttributedString *linkText;
        plainText = [[NSMutableAttributedString alloc] initWithString:longString attributes:nil];
        linkText = [[NSMutableAttributedString alloc] initWithString:@" less"
                                                          attributes:@{
                                                                       NSForegroundColorAttributeName:[UIColor blueColor]
                                                                       }];
        NSMutableAttributedString *attrText = [[NSMutableAttributedString alloc] init];
        [attrText appendAttributedString:plainText];
        [attrText appendAttributedString:linkText];
    
        // Variable -- keep track of the target range so you can compare in the callback
        lessRange = NSMakeRange(plainText.length, linkText.length);
    
        self.lblDescription.attributedText = attrText;
    }
    

    Gesture Methods

    - (void)handleTapMore:(UITapGestureRecognizer *)tapGesture {
        BOOL didTapLink = [tapGesture didTapAttributedTextInLabel:self.lblDescription
                                                          inRange:moreRange];
    
        if (didTapLink) {
            [self setReadMoreDescriptionText];
        }
    }
    
    - (void)handleTapLess:(UITapGestureRecognizer *)tapGesture {
        BOOL didTapLink = [tapGesture didTapAttributedTextInLabel:self.lblDescription
                                                          inRange:lessRange];
    
        if (didTapLink) {
            [self setReadLessDescriptionText];
        }
    }