1

I'm trying to setup a UITextView with multiple links within the text. My implementation is based on the suggestion described here. Most links work as expected, however tapping on some of them makes the app crash with the following error EXC_BREAKPOINT (code=1, subcode=0x185646694):
Crash error
Call stack

My UITextView configuration code:

private var actionableLinks = [(String, ()->Void)]() // each link = (actionableString, tapAction)

private func setupMessageText() {
    guard messageTextView != nil else { return }
        
    let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.paragraphSpacingBefore = 16
        
    let attributedText = NSMutableAttributedString(string: messageText, attributes: [
        .font: messageTextView.font!,
        .foregroundColor: messageTextView.textColor!,
        .paragraphStyle: paragraphStyle
    ])
        
    addActionableLinks(to: attributedText)
        
    messageTextView?.attributedText = attributedText
}
    
private func addActionableLinks(to attributedText: NSMutableAttributedString) {
    actionableLinks.forEach {
        let actionableString = $0.0
            
        if let nsRange = messageText.nsRange(of: actionableString) {
            attributedText.addAttribute(.link, value: actionableString, range: nsRange)
        }
    }
}

To handle the tap action, I've imlpemented the proper UITextViewDelegate method:

func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
    let tappedLinkString = URL.absoluteString

    if let link = actionableLinks.first(where: { $0.0 == tappedLinkString }) {
        let tapAction = link.1
        tapAction()

        return false
    }

    return true
}

My storyboard configuration for this screen (I have setup the UITextView delegate in the storyboard):
Storyboard Configuration

Any insights would be much appreciated! Thanks.

6
  • I suspect the link to be invalid. if let nsRange = messageText.nsRange(of: actionableString), URL(string: actionableString) != nil { might be the fix. If you can reproduce, could you provide the actionableString which is causing the issue? Commented Jun 9, 2021 at 10:23
  • See stackoverflow.com/questions/65656646/… & stackoverflow.com/questions/48154641/… since that might be the issue (there are explanations) Commented Jun 9, 2021 at 10:25
  • Thanks for your comment, and nice insight. The string I use as a link is in fact just a normal string and not a URL. I based my implementation on this previous answer and nothing was mentioned there. The string for the link that is causing the crash is "new terms and conditions", and the one that succeeds is "support". Commented Jun 9, 2021 at 10:27
  • 3
    And as said (in the explaination see both links), if the "link" is "new terms and conditions", converted to URL, with URL(string: thatString) internally by Apple, just before calling textView(_:shouldInteractWith:in:), it's nil and make it crash., because space are invalid. Percent escape them (see second linked question), and maybe add a scheme to them: yourApp:// as prefix. Commented Jun 9, 2021 at 10:29
  • Nice catch indeed! I had that suspicion after reading the questions you linked. I'll test it right away Commented Jun 9, 2021 at 10:31

1 Answer 1

5

Issue solved! Thanks to Larme for the quick insights and resources.

It was indeed a case of trying to use a bad string as a link within the UITextView that internally was being converted to a URL. Since this was a string with spaces in it, internal conversion to URL by Apple was failing.

My string "support" was linking properly and it worked, but a different string "new terms and conditions" was failing.

The solution

To solve the issue, I used percent encoding when adding the link attribute to the UITextView's attributed text.

private func addActionableLinks(to attributedText: NSMutableAttributedString) {
    actionableLinks.forEach {
        let actionableString = $0.0
            
        if let nsRange = messageText.nsRange(of: actionableString) {
            let escapedActionableString = escapedString(actionableString)
            attributedText.addAttribute(.link, value: escapedActionableString, range: nsRange)
        }
    }
}
    
private func escapedString(_ string: String) -> String {
    return string.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? string
}

I also changed the delegate method to check for a match to an escaped string:

func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
    let tappedLinkString = URL.absoluteString
        
    for (actionableString, tapAction) in actionableLinks {
        let escapedActionableString = escapedString(actionableString)
            
        if escapedActionableString == tappedLinkString {
            tapAction()
            return false
        }
    }

    return true
}

Thanks!

Sign up to request clarification or add additional context in comments.

2 Comments

You could have kept your shouldInteractLogic with just: let tappedLinkString = URL.aboluteString.removingPercentEncoding and your first(where:).
Oh, cool! I'll use that, it's way cleaner. Thanks a lot!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.