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

前端 未结 30 2607
半阙折子戏
半阙折子戏 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 03:10

    Here is a swift version of NAlexN's answer.

    class TapabbleLabel: UILabel {
    
    let layoutManager = NSLayoutManager()
    let textContainer = NSTextContainer(size: CGSize.zero)
    var textStorage = NSTextStorage() {
        didSet {
            textStorage.addLayoutManager(layoutManager)
        }
    }
    
    var onCharacterTapped: ((label: UILabel, characterIndex: Int) -> Void)?
    
    let tapGesture = UITapGestureRecognizer()
    
    override var attributedText: NSAttributedString? {
        didSet {
            if let attributedText = attributedText {
                textStorage = NSTextStorage(attributedString: attributedText)
            } else {
                textStorage = NSTextStorage()
            }
        }
    }
    
    override var lineBreakMode: NSLineBreakMode {
        didSet {
            textContainer.lineBreakMode = lineBreakMode
        }
    }
    
    override var numberOfLines: Int {
        didSet {
            textContainer.maximumNumberOfLines = numberOfLines
        }
    }
    
    /**
     Creates a new view with the passed coder.
    
     :param: aDecoder The a decoder
    
     :returns: the created new view.
     */
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setUp()
    }
    
    /**
     Creates a new view with the passed frame.
    
     :param: frame The frame
    
     :returns: the created new view.
     */
    override init(frame: CGRect) {
        super.init(frame: frame)
        setUp()
    }
    
    /**
     Sets up the view.
     */
    func setUp() {
        userInteractionEnabled = true
        layoutManager.addTextContainer(textContainer)
        textContainer.lineFragmentPadding = 0
        textContainer.lineBreakMode = lineBreakMode
        textContainer.maximumNumberOfLines = numberOfLines
        tapGesture.addTarget(self, action: #selector(TapabbleLabel.labelTapped(_:)))
        addGestureRecognizer(tapGesture)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        textContainer.size = bounds.size
    }
    
    func labelTapped(gesture: UITapGestureRecognizer) {
        guard gesture.state == .Ended else {
            return
        }
    
        let locationOfTouch = gesture.locationInView(gesture.view)
        let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer)
        let textContainerOffset = CGPoint(x: (bounds.width - textBoundingBox.width) / 2 - textBoundingBox.minX,
                                          y: (bounds.height - textBoundingBox.height) / 2 - textBoundingBox.minY)        
        let locationOfTouchInTextContainer = CGPoint(x: locationOfTouch.x - textContainerOffset.x,
                                                     y: locationOfTouch.y - textContainerOffset.y)
        let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer,
                                                                    inTextContainer: textContainer,
                                                                    fractionOfDistanceBetweenInsertionPoints: nil)
    
        onCharacterTapped?(label: self, characterIndex: indexOfCharacter)
    }
    }
    

    You can then create an instance of that class inside your viewDidLoad method like this:

    let label = TapabbleLabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(label)
    view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[view]-|",
                                                   options: [], metrics: nil, views: ["view" : label]))
    view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[view]-|",
                                                   options: [], metrics: nil, views: ["view" : label]))
    
    let attributedString = NSMutableAttributedString(string: "String with a link", attributes: nil)
    let linkRange = NSMakeRange(14, 4); // for the word "link" in the string above
    
    let linkAttributes: [String : AnyObject] = [
        NSForegroundColorAttributeName : UIColor.blueColor(), NSUnderlineStyleAttributeName : NSUnderlineStyle.StyleSingle.rawValue,
        NSLinkAttributeName: "http://www.apple.com"]
    attributedString.setAttributes(linkAttributes, range:linkRange)
    
    label.attributedText = attributedString
    
    label.onCharacterTapped = { label, characterIndex in
        if let attribute = label.attributedText?.attribute(NSLinkAttributeName, atIndex: characterIndex, effectiveRange: nil) as? String,
            let url = NSURL(string: attribute) {
            UIApplication.sharedApplication().openURL(url)
        }
    }
    

    It's better to have a custom attribute to use when a character is tapped. Now, it's the NSLinkAttributeName, but could be anything and you can use that value to do other things other than opening a url, you can do any custom action.

提交回复
热议问题