Create tap-able “links” in the NSAttributedString of a UILabel?

前端 未结 30 2480
半阙折子戏
半阙折子戏 2020-11-22 02:07

I have been searching this for hours but I\'ve failed. I probably don\'t even know what I should be looking for.

Many applications have text and in this text are web

相关标签:
30条回答
  • 2020-11-22 02:52

    Create the class with the following .h and .m files. In the .m file there is the following function

     - (void)linkAtPoint:(CGPoint)location
    

    Inside this function we will check the ranges of substrings for which we need to give actions. Use your own logic to put your ranges.

    And following is the usage of the subclass

    TaggedLabel *label = [[TaggedLabel alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    [self.view addSubview:label];
    label.numberOfLines = 0;
    NSMutableAttributedString *attributtedString = [[NSMutableAttributedString alloc] initWithString : @"My name is @jjpp" attributes : @{ NSFontAttributeName : [UIFont systemFontOfSize:10],}];                                                                                                                                                                              
    //Do not forget to add the font attribute.. else it wont work.. it is very important
    [attributtedString addAttribute:NSForegroundColorAttributeName
                            value:[UIColor redColor]
                            range:NSMakeRange(11, 5)];//you can give this range inside the .m function mentioned above
    

    following is the .h file

    #import <UIKit/UIKit.h>
    
    @interface TaggedLabel : UILabel<NSLayoutManagerDelegate>
    
    @property(nonatomic, strong)NSLayoutManager *layoutManager;
    @property(nonatomic, strong)NSTextContainer *textContainer;
    @property(nonatomic, strong)NSTextStorage *textStorage;
    @property(nonatomic, strong)NSArray *tagsArray;
    @property(readwrite, copy) tagTapped nameTagTapped;
    
    @end   
    

    following is the .m file

    #import "TaggedLabel.h"
    @implementation TaggedLabel
    
    - (id)initWithFrame:(CGRect)frame
    {
     self = [super initWithFrame:frame];
     if (self)
     {
      self.userInteractionEnabled = YES;
     }
    return self;
    }
    
    - (id)initWithCoder:(NSCoder *)aDecoder
    {
     self = [super initWithCoder:aDecoder];
    if (self)
    {
     self.userInteractionEnabled = YES;
    }
    return self;
    }
    
    - (void)setupTextSystem
    {
     _layoutManager = [[NSLayoutManager alloc] init];
     _textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
     _textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
     // Configure layoutManager and textStorage
     [_layoutManager addTextContainer:_textContainer];
     [_textStorage addLayoutManager:_layoutManager];
     // Configure textContainer
     _textContainer.lineFragmentPadding = 0.0;
     _textContainer.lineBreakMode = NSLineBreakByWordWrapping;
     _textContainer.maximumNumberOfLines = 0;
     self.userInteractionEnabled = YES;
     self.textContainer.size = self.bounds.size;
    }
    
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
     if (!_layoutManager)
     {
      [self setupTextSystem];
     }
     // Get the info for the touched link if there is one
     CGPoint touchLocation = [[touches anyObject] locationInView:self];
     [self linkAtPoint:touchLocation];
    }
    
    - (void)linkAtPoint:(CGPoint)location
    {
     // Do nothing if we have no text
     if (_textStorage.string.length == 0)
     {
      return;
     }
     // Work out the offset of the text in the view
     CGPoint textOffset = [self calcGlyphsPositionInView];
     // Get the touch location and use text offset to convert to text cotainer coords
     location.x -= textOffset.x;
     location.y -= textOffset.y;
     NSUInteger touchedChar = [_layoutManager glyphIndexForPoint:location inTextContainer:_textContainer];
     // If the touch is in white space after the last glyph on the line we don't
     // count it as a hit on the text
     NSRange lineRange;
     CGRect lineRect = [_layoutManager lineFragmentUsedRectForGlyphAtIndex:touchedChar effectiveRange:&lineRange];
     if (CGRectContainsPoint(lineRect, location) == NO)
     {
      return;
     }
     // Find the word that was touched and call the detection block
        NSRange range = NSMakeRange(11, 5);//for this example i'm hardcoding the range here. In a real scenario it should be iterated through an array for checking all the ranges
        if ((touchedChar >= range.location) && touchedChar < (range.location + range.length))
        {
         NSLog(@"range-->>%@",self.tagsArray[i][@"range"]);
        }
    }
    
    - (CGPoint)calcGlyphsPositionInView
    {
     CGPoint textOffset = CGPointZero;
     CGRect textBounds = [_layoutManager usedRectForTextContainer:_textContainer];
     textBounds.size.width = ceil(textBounds.size.width);
     textBounds.size.height = ceil(textBounds.size.height);
    
     if (textBounds.size.height < self.bounds.size.height)
     {
      CGFloat paddingHeight = (self.bounds.size.height - textBounds.size.height) / 2.0;
      textOffset.y = paddingHeight;
     }
    
     if (textBounds.size.width < self.bounds.size.width)
     {
      CGFloat paddingHeight = (self.bounds.size.width - textBounds.size.width) / 2.0;
      textOffset.x = paddingHeight;
     }
     return textOffset;
     }
    
    @end
    
    0 讨论(0)
  • 2020-11-22 02:54

    based on Charles Gamble answer, this what I used (I removed some lines that confused me and gave me wrong indexed) :

    - (BOOL)didTapAttributedTextInLabel:(UILabel *)label inRange:(NSRange)targetRange TapGesture:(UIGestureRecognizer*) gesture{
        NSParameterAssert(label != nil);
    
        // create instances of NSLayoutManager, NSTextContainer and NSTextStorage
        NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
        NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];
    
        // configure layoutManager and textStorage
        [textStorage addLayoutManager:layoutManager];
    
        // configure textContainer for the label
        NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(label.frame.size.width, label.frame.size.height)];
    
        textContainer.lineFragmentPadding = 0.0;
        textContainer.lineBreakMode = label.lineBreakMode;
        textContainer.maximumNumberOfLines = label.numberOfLines;
    
        // find the tapped character location and compare it to the specified range
        CGPoint locationOfTouchInLabel = [gesture locationInView:label];
        [layoutManager addTextContainer:textContainer]; //(move here, not sure it that matter that calling this line after textContainer is set
    
        NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInLabel
                                                               inTextContainer:textContainer
                                      fractionOfDistanceBetweenInsertionPoints:nil];
        if (NSLocationInRange(indexOfCharacter, targetRange)) {
            return YES;
        } else {
            return NO;
        }
    }
    
    0 讨论(0)
  • 2020-11-22 02:55

    Here’s a Swift implementation that is about as minimal as possible that also includes touch feedback. Caveats:

    1. You must set fonts in your NSAttributedStrings
    2. You can only use NSAttributedStrings!
    3. You must ensure your links cannot wrap (use non breaking spaces: "\u{a0}")
    4. You cannot change the lineBreakMode or numberOfLines after setting the text
    5. You create links by adding attributes with .link keys

    .

    public class LinkLabel: UILabel {
        private var storage: NSTextStorage?
        private let textContainer = NSTextContainer()
        private let layoutManager = NSLayoutManager()
        private var selectedBackgroundView = UIView()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            textContainer.lineFragmentPadding = 0
            layoutManager.addTextContainer(textContainer)
            textContainer.layoutManager = layoutManager
            isUserInteractionEnabled = true
            selectedBackgroundView.isHidden = true
            selectedBackgroundView.backgroundColor = UIColor(white: 0, alpha: 0.3333)
            selectedBackgroundView.layer.cornerRadius = 4
            addSubview(selectedBackgroundView)
        }
    
        public required convenience init(coder: NSCoder) {
            self.init(frame: .zero)
        }
    
        public override func layoutSubviews() {
            super.layoutSubviews()
            textContainer.size = frame.size
        }
    
        public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            super.touchesBegan(touches, with: event)
            setLink(for: touches)
        }
    
        public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            super.touchesMoved(touches, with: event)
            setLink(for: touches)
        }
    
        private func setLink(for touches: Set<UITouch>) {
            if let pt = touches.first?.location(in: self), let (characterRange, _) = link(at: pt) {
                let glyphRange = layoutManager.glyphRange(forCharacterRange: characterRange, actualCharacterRange: nil)
                selectedBackgroundView.frame = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer).insetBy(dx: -3, dy: -3)
                selectedBackgroundView.isHidden = false
            } else {
                selectedBackgroundView.isHidden = true
            }
        }
    
        public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
            super.touchesCancelled(touches, with: event)
            selectedBackgroundView.isHidden = true
        }
    
        public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            super.touchesEnded(touches, with: event)
            selectedBackgroundView.isHidden = true
    
            if let pt = touches.first?.location(in: self), let (_, url) = link(at: pt) {
                UIApplication.shared.open(url)
            }
        }
    
        private func link(at point: CGPoint) -> (NSRange, URL)? {
            let touchedGlyph = layoutManager.glyphIndex(for: point, in: textContainer)
            let touchedChar = layoutManager.characterIndexForGlyph(at: touchedGlyph)
            var range = NSRange()
            let attrs = attributedText!.attributes(at: touchedChar, effectiveRange: &range)
            if let urlstr = attrs[.link] as? String {
                return (range, URL(string: urlstr)!)
            } else {
                return nil
            }
        }
    
        public override var attributedText: NSAttributedString? {
            didSet {
                textContainer.maximumNumberOfLines = numberOfLines
                textContainer.lineBreakMode = lineBreakMode
                if let txt = attributedText {
                    storage = NSTextStorage(attributedString: txt)
                    storage!.addLayoutManager(layoutManager)
                    layoutManager.textStorage = storage
                    textContainer.size = frame.size
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-22 02:55
    - (BOOL)didTapAttributedTextInLabel:(UILabel *)label inRange:(NSRange)targetRange{
        NSLayoutManager *layoutManager = [NSLayoutManager new];
        NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
        NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];
    
        [layoutManager addTextContainer:textContainer];
        [textStorage addLayoutManager:layoutManager];
    
        textContainer.lineFragmentPadding = 0.0;
        textContainer.lineBreakMode = label.lineBreakMode;
        textContainer.maximumNumberOfLines = label.numberOfLines;
        CGSize labelSize = label.bounds.size;
        textContainer.size = labelSize;
    
        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);
        NSUInteger indexOfCharacter =[layoutManager characterIndexForPoint:locationOfTouchInTextContainer inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:nil];
    
        return NSLocationInRange(indexOfCharacter, targetRange);
    }
    
    0 讨论(0)
  • 2020-11-22 02:56

    I'd strongly recommend using a library that automatically detects URLs in text and converts them to links. Try:

    • TTTAttributedLabel (pod)
    • ZSWTappableLabel (pod).

    Both are under MIT license.

    0 讨论(0)
  • 2020-11-22 02:57

    This generic method works too !

    func didTapAttributedTextInLabel(gesture: UITapGestureRecognizer, inRange targetRange: NSRange) -> Bool {
    
            let layoutManager = NSLayoutManager()
            let textContainer = NSTextContainer(size: CGSize.zero)
            guard let strAttributedText = self.attributedText else {
                return false
            }
    
            let textStorage = NSTextStorage(attributedString: strAttributedText)
    
            // Configure layoutManager and textStorage
            layoutManager.addTextContainer(textContainer)
            textStorage.addLayoutManager(layoutManager)
    
            // Configure textContainer
            textContainer.lineFragmentPadding = Constants.lineFragmentPadding
            textContainer.lineBreakMode = self.lineBreakMode
            textContainer.maximumNumberOfLines = self.numberOfLines
            let labelSize = self.bounds.size
            textContainer.size = CGSize(width: labelSize.width, height: CGFloat.greatestFiniteMagnitude)
    
            // Find the tapped character location and compare it to the specified range
            let locationOfTouchInLabel = gesture.location(in: self)
    
            let xCordLocationOfTouchInTextContainer = locationOfTouchInLabel.x
            let yCordLocationOfTouchInTextContainer = locationOfTouchInLabel.y
            let locOfTouch = CGPoint(x: xCordLocationOfTouchInTextContainer ,
                                     y: yCordLocationOfTouchInTextContainer)
    
            let indexOfCharacter = layoutManager.characterIndex(for: locOfTouch, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
    
            guard let strLabel = text else {
                return false
            }
    
            let charCountOfLabel = strLabel.count
    
            if indexOfCharacter < (charCountOfLabel - 1) {
                return NSLocationInRange(indexOfCharacter, targetRange)
            } else {
                return false
            }
        }
    

    And you can call the method with

    let text = yourLabel.text
    let termsRange = (text as NSString).range(of: fullString)
    if yourLabel.didTapAttributedTextInLabel(gesture: UITapGestureRecognizer, inRange: termsRange) {
                showCorrespondingViewController()
            }
    
    0 讨论(0)
提交回复
热议问题