Приложение iOS аварийно завершает работу при нажатии URL-адреса в UITextView

Я пытаюсь настроить UITextView с несколькими ссылками в тексте. Моя реализация основана на предложении, описанном здесь. Большинство ссылок работают должным образом, однако нажатие на некоторые из них приводит к сбою приложения со следующей ошибкой EXC_BREAKPOINT (code=1, subcode=0x185646694):
Ошибка сбоя
Стек вызовов

Мой код конфигурации UITextView:

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)
        }
    }
}

Чтобы обработать действие касания, я реализовал правильный метод UITextViewDelegate:

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
}

Моя конфигурация раскадровки для этого экрана (я настроил делегата UITextView в раскадровке):
Конфигурация раскадровки

Любые идеи будут высоко оценены! Спасибо.


person Ruben Dias    schedule 09.06.2021    source источник
comment
Подозреваю, что ссылка недействительна. if let nsRange = messageText.nsRange(of: actionableString), URL(string: actionableString) != nil { может быть решением. Если вы можете воспроизвести, не могли бы вы указать actionableString, который вызывает проблему?   -  person Larme    schedule 09.06.2021
comment
См. stackoverflow.com/questions/65656646/ & stackoverflow.com/questions/48154641/ так как это может быть проблемой (есть объяснения)   -  person Larme    schedule 09.06.2021
comment
Спасибо за ваш комментарий и хорошее понимание. Строка, которую я использую в качестве ссылки, на самом деле является обычной строкой, а не URL-адресом. Я основывал свою реализацию на этом предыдущем ответе, и там ничего не упоминалось. Строка для ссылки, которая вызывает сбой, — это новые условия, а та, которая успешна, — это поддержка.   -  person Ruben Dias    schedule 09.06.2021
comment
И, как сказано (в объяснении см. обе ссылки), если ссылка "new terms and conditions", преобразованная в URL, с URL(string: thatString) внутри Apple, непосредственно перед вызовом textView(_:shouldInteractWith:in:), она равна нулю и приводит к сбою, потому что пробелы недействительны. Процент избегает их (см. второй связанный вопрос) и, возможно, добавляет к ним схему: yourApp:// в качестве префикса.   -  person Larme    schedule 09.06.2021
comment
Действительно хороший улов! У меня возникло такое подозрение после прочтения вопросов, которые вы связали. сразу протестирую   -  person Ruben Dias    schedule 09.06.2021
comment
Что такое фактический парень actionableString?   -  person El Tomato    schedule 09.06.2021


Ответы (1)


Проблема решена! Спасибо Larme за быстрые идеи и ресурсы.

Это действительно был случай попытки использовать неверную строку в качестве ссылки в UITextView, которая внутренне преобразовывалась в URL-адрес. Поскольку это была строка с пробелами, внутреннее преобразование Apple в URL не удалось.

Моя поддержка строк была правильно связана, и это работало, но другая строка новых условий и положений не удалась.

Решение

Чтобы решить эту проблему, я использовал процентное кодирование при добавлении атрибута ссылки в атрибутированный текст UITextView.

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
}

Я также изменил метод делегата, чтобы проверить совпадение с экранированной строкой:

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
}

Спасибо!

person Ruben Dias    schedule 09.06.2021
comment
Вы могли бы сохранить свой shouldInteractLogic только с: let tappedLinkString = URL.aboluteString.removingPercentEncoding и вашим first(where:). - person Larme; 09.06.2021
comment
О, круто! Я воспользуюсь этим, он намного чище. Большое спасибо! - person Ruben Dias; 09.06.2021