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

前端 未结 30 2478
半阙折子戏
半阙折子戏 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:43

    I follow this version,

    Swift 4:

    import Foundation
    
    class AELinkedClickableUILabel: UILabel {
    
        typealias YourCompletion = () -> Void
    
        var linkedRange: NSRange!
        var completion: YourCompletion?
    
        @objc func linkClicked(sender: UITapGestureRecognizer){
    
            if let completionBlock = completion {
    
                let textView = UITextView(frame: self.frame)
                textView.text = self.text
                textView.attributedText = self.attributedText
                let index = textView.layoutManager.characterIndex(for: sender.location(in: self),
                                                                  in: textView.textContainer,
                                                                  fractionOfDistanceBetweenInsertionPoints: nil)
    
                if linkedRange.lowerBound <= index && linkedRange.upperBound >= index {
    
                    completionBlock()
                }
            }
        }
    
    /**
     *  This method will be used to set an attributed text specifying the linked text with a
     *  handler when the link is clicked
     */
        public func setLinkedTextWithHandler(text:String, link: String, handler: @escaping ()->()) -> Bool {
    
            let attributextText = NSMutableAttributedString(string: text)
            let foundRange = attributextText.mutableString.range(of: link)
    
            if foundRange.location != NSNotFound {
                self.linkedRange = foundRange
                self.completion = handler
                attributextText.addAttribute(NSAttributedStringKey.link, value: text, range: foundRange)
                self.isUserInteractionEnabled = true
                self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(linkClicked(sender:))))
                return true
            }
            return false
        }
    }
    

    Call Example:

    button.setLinkedTextWithHandler(text: "This website (stackoverflow.com) is awesome", link: "stackoverflow.com") 
    {
        // show popup or open to link
    }
    
    0 讨论(0)
  • 2020-11-22 02:44

    Drop-in solution as a category on UILabel (this assumes your UILabel uses an attributed string with some NSLinkAttributeName attributes in it):

    @implementation UILabel (Support)
    
    - (BOOL)openTappedLinkAtLocation:(CGPoint)location {
      CGSize labelSize = self.bounds.size;
    
      NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
      textContainer.lineFragmentPadding = 0.0;
      textContainer.lineBreakMode = self.lineBreakMode;
      textContainer.maximumNumberOfLines = self.numberOfLines;
      textContainer.size = labelSize;
    
      NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];
      [layoutManager addTextContainer:textContainer];
    
      NSTextStorage* textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
      [textStorage addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, textStorage.length)];
      [textStorage addLayoutManager:layoutManager];
    
      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(location.x - textContainerOffset.x, location.y - textContainerOffset.y);
      NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInTextContainer inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:nullptr];
      if (indexOfCharacter >= 0) {
        NSURL* url = [textStorage attribute:NSLinkAttributeName atIndex:indexOfCharacter effectiveRange:nullptr];
        if (url) {
          [[UIApplication sharedApplication] openURL:url];
          return YES;
        }
      }
      return NO;
    }
    
    @end
    
    0 讨论(0)
  • 2020-11-22 02:46

    I had a hard time dealing with this... UILabel with links on it on attributed text... it is just a headache so I ended up using ZSWTappableLabel.

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

    Here is my answer based on @Luca Davanzo's answer, override the touchesBegan event instead of a tap gesture:

    import UIKit
    
    public protocol TapableLabelDelegate: NSObjectProtocol {
       func tapableLabel(_ label: TapableLabel, didTapUrl url: String, atRange range: NSRange)
    }
    
    public class TapableLabel: UILabel {
    
    private var links: [String: NSRange] = [:]
    private(set) var layoutManager = NSLayoutManager()
    private(set) var textContainer = NSTextContainer(size: CGSize.zero)
    private(set) var textStorage = NSTextStorage() {
        didSet {
            textStorage.addLayoutManager(layoutManager)
        }
    }
    
    public weak var delegate: TapableLabelDelegate?
    
    public override var attributedText: NSAttributedString? {
        didSet {
            if let attributedText = attributedText {
                textStorage = NSTextStorage(attributedString: attributedText)
            } else {
                textStorage = NSTextStorage()
                links = [:]
            }
        }
    }
    
    public override var lineBreakMode: NSLineBreakMode {
        didSet {
            textContainer.lineBreakMode = lineBreakMode
        }
    }
    
    public override var numberOfLines: Int {
        didSet {
            textContainer.maximumNumberOfLines = numberOfLines
        }
    }
    
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }
    
    public override func layoutSubviews() {
        super.layoutSubviews()
        textContainer.size = bounds.size
    }
    
    
    /// addLinks
    ///
    /// - Parameters:
    ///   - text: text of link
    ///   - url: link url string
    public func addLink(_ text: String, withURL url: String) {
        guard let theText = attributedText?.string as? NSString else {
            return
        }
    
        let range = theText.range(of: text)
    
        guard range.location !=  NSNotFound else {
            return
        }
    
        links[url] = range
    }
    
    private func setup() {
        isUserInteractionEnabled = true
        layoutManager.addTextContainer(textContainer)
        textContainer.lineFragmentPadding = 0
        textContainer.lineBreakMode = lineBreakMode
        textContainer.maximumNumberOfLines  = numberOfLines
    }
    
    public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let locationOfTouch = touches.first?.location(in: self) else {
            return
        }
    
        textContainer.size = bounds.size
        let indexOfCharacter = layoutManager.glyphIndex(for: locationOfTouch, in: textContainer)
    
        for (urlString, range) in links {
            if NSLocationInRange(indexOfCharacter, range), let url = URL(string: urlString) {
                delegate?.tapableLabel(self, didTapUrl: urlString, atRange: range)
            }
        }
    }}
    
    0 讨论(0)
  • 2020-11-22 02:48

    Translating @samwize's Extension to Swift 4:

    extension UITapGestureRecognizer {
        func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
            guard let attrString = label.attributedText else {
                return false
            }
    
            let layoutManager = NSLayoutManager()
            let textContainer = NSTextContainer(size: .zero)
            let textStorage = NSTextStorage(attributedString: attrString)
    
            layoutManager.addTextContainer(textContainer)
            textStorage.addLayoutManager(layoutManager)
    
            textContainer.lineFragmentPadding = 0
            textContainer.lineBreakMode = label.lineBreakMode
            textContainer.maximumNumberOfLines = label.numberOfLines
            let labelSize = label.bounds.size
            textContainer.size = labelSize
    
            let locationOfTouchInLabel = self.location(in: label)
            let textBoundingBox = layoutManager.usedRect(for: textContainer)
            let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
            let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
            let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
            return NSLocationInRange(indexOfCharacter, targetRange)
        }
    }
    

    To set up the recognizer (once you colored the text and stuff):

    lblTermsOfUse.isUserInteractionEnabled = true
    lblTermsOfUse.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTapOnLabel(_:))))
    

    ...then the gesture recognizer:

    @objc func handleTapOnLabel(_ recognizer: UITapGestureRecognizer) {
        guard let text = lblAgreeToTerms.attributedText?.string else {
            return
        }
    
        if let range = text.range(of: NSLocalizedString("_onboarding_terms", comment: "terms")),
            recognizer.didTapAttributedTextInLabel(label: lblAgreeToTerms, inRange: NSRange(range, in: text)) {
            goToTermsAndConditions()
        } else if let range = text.range(of: NSLocalizedString("_onboarding_privacy", comment: "privacy")),
            recognizer.didTapAttributedTextInLabel(label: lblAgreeToTerms, inRange: NSRange(range, in: text)) {
            goToPrivacyPolicy()
        }
    }
    
    0 讨论(0)
  • 2020-11-22 02:49

    TAGS #Swift2.0

    I take inspiration on - excellent - @NAlexN's answer and I decide to write by myself a wrapper of UILabel.
    I also tried TTTAttributedLabel but I can't make it works.

    Hope you can appreciate this code, any suggestions are welcome!

    import Foundation
    
    @objc protocol TappableLabelDelegate {
        optional func tappableLabel(tabbableLabel: TappableLabel, didTapUrl: NSURL, atRange: NSRange)
    }
    
    /// Represent a label with attributed text inside.
    /// We can add a correspondence between a range of the attributed string an a link (URL)
    /// By default, link will be open on the external browser @see 'openLinkOnExternalBrowser'
    
    class TappableLabel: UILabel {
    
        // MARK: - Public properties -
    
        var links: NSMutableDictionary = [:]
        var openLinkOnExternalBrowser = true
        var delegate: TappableLabelDelegate?
    
        // MARK: - Constructors -
    
        override func awakeFromNib() {
            super.awakeFromNib()
            self.enableInteraction()
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            self.enableInteraction()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    
        private func enableInteraction() {
            self.userInteractionEnabled = true
            self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: Selector("didTapOnLabel:")))
        }
    
        // MARK: - Public methods -
    
        /**
        Add correspondence between a range and a link.
    
        - parameter url:   url.
        - parameter range: range on which couple url.
        */
        func addLink(url url: String, atRange range: NSRange) {
            self.links[url] = range
        }
    
        // MARK: - Public properties -
    
        /**
        Action rised on user interaction on label.
    
        - parameter tapGesture: gesture.
        */
        func didTapOnLabel(tapGesture: UITapGestureRecognizer) {
            let labelSize = self.bounds.size;
    
            let layoutManager = NSLayoutManager()
            let textContainer = NSTextContainer(size: CGSizeZero)
            let textStorage = NSTextStorage(attributedString: self.attributedText!)
    
            // configure textContainer for the label
            textContainer.lineFragmentPadding = 0
            textContainer.lineBreakMode = self.lineBreakMode
            textContainer.maximumNumberOfLines = self.numberOfLines
            textContainer.size = labelSize;
    
            // configure layoutManager and textStorage
            layoutManager.addTextContainer(textContainer)
            textStorage.addLayoutManager(layoutManager)
    
            // find the tapped character location and compare it to the specified range
            let locationOfTouchInLabel = tapGesture.locationInView(self)
    
            let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer)
            let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
            let locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                locationOfTouchInLabel.y - textContainerOffset.y)
            let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer,
                inTextContainer:textContainer,
                fractionOfDistanceBetweenInsertionPoints: nil)
    
            for (url, value) in self.links {
                if let range = value as? NSRange {
                    if NSLocationInRange(indexOfCharacter, range) {
                        let url = NSURL(string: url as! String)!
                        if self.openLinkOnExternalBrowser {
                            UIApplication.sharedApplication().openURL(url)
                        }
                        self.delegate?.tappableLabel?(self, didTapUrl: url, atRange: range)
                    }
                }
            }
        }
    
    }
    
    0 讨论(0)
提交回复
热议问题