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

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

    I'm extending @samwize's answer to handle multi-line UILabel and give an example on using for a UIButton

    extension UITapGestureRecognizer {
    
        func didTapAttributedTextInButton(button: UIButton, inRange targetRange: NSRange) -> Bool {
            guard let label = button.titleLabel else { return false }
            return didTapAttributedTextInLabel(label, inRange: targetRange)
        }
    
        func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
            // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
            let layoutManager = NSLayoutManager()
            let textContainer = NSTextContainer(size: CGSize.zero)
            let textStorage = NSTextStorage(attributedString: label.attributedText!)
    
            // Configure layoutManager and textStorage
            layoutManager.addTextContainer(textContainer)
            textStorage.addLayoutManager(layoutManager)
    
            // Configure textContainer
            textContainer.lineFragmentPadding = 0.0
            textContainer.lineBreakMode = label.lineBreakMode
            textContainer.maximumNumberOfLines = label.numberOfLines
            let labelSize = label.bounds.size
            textContainer.size = labelSize
    
            // Find the tapped character location and compare it to the specified range
            let locationOfTouchInLabel = self.locationInView(label)
            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),
                                                             0 );
            // Adjust for multiple lines of text
            let lineModifier = Int(ceil(locationOfTouchInLabel.y / label.font.lineHeight)) - 1
            let rightMostFirstLinePoint = CGPointMake(labelSize.width, 0)
            let charsPerLine = layoutManager.characterIndexForPoint(rightMostFirstLinePoint, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
    
            let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
            let adjustedRange = indexOfCharacter + (lineModifier * charsPerLine)
    
            return NSLocationInRange(adjustedRange, targetRange)
        }
    
    }
    
    0 讨论(0)
  • 2020-11-22 03:08

    Like there is reported in earlier answer the UITextView is able to handle touches on links. This can easily be extended by making other parts of the text work as links. The AttributedTextView library is a UITextView subclass that makes it very easy to handle these. For more info see: https://github.com/evermeer/AttributedTextView

    You can make any part of the text interact like this (where textView1 is a UITextView IBOutlet):

    textView1.attributer =
        "1. ".red
        .append("This is the first test. ").green
        .append("Click on ").black
        .append("evict.nl").makeInteract { _ in
            UIApplication.shared.open(URL(string: "http://evict.nl")!, options: [:], completionHandler: { completed in })
        }.underline
        .append(" for testing links. ").black
        .append("Next test").underline.makeInteract { _ in
            print("NEXT")
        }
        .all.font(UIFont(name: "SourceSansPro-Regular", size: 16))
        .setLinkColor(UIColor.purple) 
    

    And for handling hashtags and mentions you can use code like this:

    textView1.attributer = "@test: What #hashtags do we have in @evermeer #AtributedTextView library"
        .matchHashtags.underline
        .matchMentions
        .makeInteract { link in
            UIApplication.shared.open(URL(string: "https://twitter.com\(link.replacingOccurrences(of: "@", with: ""))")!, options: [:], completionHandler: { completed in })
        }
    
    0 讨论(0)
  • 2020-11-22 03:09

    Worked in Swift 3, pasting the entire code here

        //****Make sure the textview 'Selectable' = checked, and 'Editable = Unchecked'
    
    import UIKit
    
    class ViewController: UIViewController, UITextViewDelegate {
    
        @IBOutlet var theNewTextView: UITextView!
        override func viewDidLoad() {
            super.viewDidLoad()
    
            //****textview = Selectable = checked, and Editable = Unchecked
    
            theNewTextView.delegate = self
    
            let theString = NSMutableAttributedString(string: "Agree to Terms")
            let theRange = theString.mutableString.range(of: "Terms")
    
            theString.addAttribute(NSLinkAttributeName, value: "ContactUs://", range: theRange)
    
            let theAttribute = [NSForegroundColorAttributeName: UIColor.blue, NSUnderlineStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue] as [String : Any]
    
            theNewTextView.linkTextAttributes = theAttribute
    
         theNewTextView.attributedText = theString             
    
    theString.setAttributes(theAttribute, range: theRange)
    
        }
    
        func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
    
            if (URL.scheme?.hasPrefix("ContactUs://"))! {
    
                return false //interaction not allowed
            }
    
            //*** Set storyboard id same as VC name
            self.navigationController!.pushViewController((self.storyboard?.instantiateViewController(withIdentifier: "TheLastViewController"))! as UIViewController, animated: true)
    
            return true
        }
    
    }
    
    0 讨论(0)
  • 2020-11-22 03:09

    For fully custom links, you'll need to use a UIWebView - you can intercept the calls out, so that you can go to some other part of your app instead when a link is pressed.

    0 讨论(0)
  • 2020-11-22 03:09

    Modified @timbroder code to handle multiple line correctly for swift4.2

    extension UITapGestureRecognizer {
    
        func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
            // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
            let layoutManager = NSLayoutManager()
            let textContainer = NSTextContainer(size: CGSize.zero)
            let textStorage = NSTextStorage(attributedString: label.attributedText!)
    
            // Configure layoutManager and textStorage
            layoutManager.addTextContainer(textContainer)
            textStorage.addLayoutManager(layoutManager)
    
            // Configure textContainer
            textContainer.lineFragmentPadding = 0.0
            textContainer.lineBreakMode = label.lineBreakMode
            textContainer.maximumNumberOfLines = label.numberOfLines
            let labelSize = label.bounds.size
            textContainer.size = labelSize
    
            // Find the tapped character location and compare it to the specified range
            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: 0 );
            // Adjust for multiple lines of text
            let lineModifier = Int(ceil(locationOfTouchInLabel.y / label.font.lineHeight)) - 1
            let rightMostFirstLinePoint = CGPoint(x: labelSize.width, y: 0)
            let charsPerLine = layoutManager.characterIndex(for: rightMostFirstLinePoint, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
    
            let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
            let adjustedRange = indexOfCharacter + (lineModifier * charsPerLine)
            var newTargetRange = targetRange
            if lineModifier > 0 {
                newTargetRange.location = targetRange.location+(lineModifier*Int(ceil(locationOfTouchInLabel.y)))
            }
            return NSLocationInRange(adjustedRange, newTargetRange)
        }
    }
    

    UILabel Code

    let tapAction = UITapGestureRecognizer(target: self, action: #selector(self.tapLabel(gesture:)))
    
    let quote = "For full details please see our privacy policy and cookie policy."
    let attributedString = NSMutableAttributedString(string: quote)
    
    let string1: String = "privacy policy", string2: String = "cookie policy"
    
    // privacy policy
    let rangeString1 = quote.range(of: string1)!
    let indexString1: Int = quote.distance(from: quote.startIndex, to: rangeString1.lowerBound)
    attributedString.addAttributes(
                [.font: <UIfont>,
                 .foregroundColor: <UI Color>,
                 .underlineStyle: 0, .underlineColor:UIColor.clear
            ], range: NSRange(location: indexString1, length: string1.count));
    
    // cookie policy
    let rangeString2 = quote.range(of: string2)!
    let indexString2: Int = quote.distance(from: quote.startIndex, to: rangeString2.lowerBound )
    
    attributedString.addAttributes(
                [.font: <UIfont>,
                 .foregroundColor: <UI Color>,
                 .underlineStyle: 0, .underlineColor:UIColor.clear
            ], range: NSRange(location: indexString2, length: string2.count));
    
    let label = UILabel()
    label.frame = CGRect(x: 20, y: 200, width: 375, height: 100)
    label.isUserInteractionEnabled = true
    label.addGestureRecognizer(tapAction)
    label.attributedText = attributedString
    
    

    Code to recognise the Tap

     @objc
      func tapLabel(gesture: UITapGestureRecognizer) {
         if gesture.didTapAttributedTextInLabel(label: <UILabel>, inRange: termsLabelRange {
                print("Terms of service")
         } else if gesture.didTapAttributedTextInLabel(label:<UILabel> inRange: privacyPolicyLabelRange) {
                print("Privacy policy")
         } else {
                print("Tapped none")
         }
        }
    
    0 讨论(0)
  • 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.

    0 讨论(0)
提交回复
热议问题