SwiftUI: Link phone numbers, emails, URLs in Text without Markdown

SwiftUI provides an easy way to link URLs and emails inside of a Text view using AttributedString’s markdown initializer:

let text = "Jenny's at 867-5309."
    + " Email her at jenny@tutone.net."
    + " Find out more: https://en.wikipedia.org/wiki/867-5309/Jenny"

VStack {
    Text(try! AttributedString(markdown: text))
}

However, this has a few shortcomings:

  • the phone number isn’t linked/tappable
  • we may not want all the markdown rendering: bullets (*) and brackets ([]) in the text may end up rendering with unexpected results

We can use NSDataDetector instead to help us construct our own AttributedString that can detect phone numbers and also prevent the user from accidentally injecting markdown:

private let dataDetector: NSDataDetector = {
    let types: NSTextCheckingResult.CheckingType = [.link, .phoneNumber]
    return try! .init(types: types.rawValue)
}()

struct JennyView: View {
    var body: some View {
        VStack {
            Text(attributedString(from: text)!)
        }
    }
    
    private func attributedString(from text: String) -> AttributedString? {
        var attributed = AttributedString(text)
        let fullRange = NSMakeRange(0, text.count)
        let matches = dataDetector.matches(in: text, options: [], range: fullRange)
        guard !matches.isEmpty else { return nil }
        
        for result in matches {
            guard let range = Range<AttributedString.Index>(result.range, in: attributed) else {
                continue
            }
            
            switch result.resultType {
            case .phoneNumber:
                guard
                    let phoneNumber = result.phoneNumber,
                    let url = URL(string: "sms://\(phoneNumber)")
                else {
                    break
                }
                attributed[range].link = url
                
            case .link:
                guard let url = result.url else {
                    break
                }
                attributed[range].link = url

            default:
                break
            }
        }
        
        return attributed
    }
}

Here, I’ve used the sms:// scheme to open the Messaging app when the user taps on a phone number. You could also use the tel:// scheme as well.

Now go forth, tap that phone number, and make Jenny your own.

Additional Resources