1

I have posts on my app where users can tag other users, the expectation is that these tags should be tappable and trigger a function when tapped.

For example the post: "hello @someUser and @otherUser" the @someUser and @otherUser should be tappable.

This is the code I've tried, but it's only showing text with tagged users, from the example above it is only displaying: "@someUser @otherUser" without the rest of the text. The tapping implementation does work but no other text is displayed, and I don't think an HStack is ideal here

Is there a better way to handle this in SwiftUI?

@ViewBuilder
    func bodyTextView(text: String) -> some View {
        HStack {
            ForEach(attributedText(text: text), id: \.self) { text in
                if text.hasPrefix("@") {
                    Text(text)
                        .onTapGesture {
                            print("TAP: \(text)")
                        }
                } else {
                    Text(text)
                }
            }
        }
        
    }
    
    func attributedText(text: String) -> [String] {
        let inputString = text
        let regexPattern = "@\\w+"

        guard let regex = try? NSRegularExpression(pattern: regexPattern) else {
            return [inputString]
        }
        let range = NSRange(inputString.startIndex..<inputString.endIndex, in: inputString)
        let matches = regex.matches(in: inputString, range: range)
            
        var substrings: [String] = []
            
        for match in matches {
            if let swiftRange = Range(match.range, in: inputString) {
                let substring = String(inputString[swiftRange])
                substrings.append(substring)
            }
        }
            
        return substrings
    }
Noah Iarrobino
  • 1,435
  • 1
  • 10
  • 31
  • Your regex only returns strings that have `@` at the beginning. So, the `ForEach` will *only* show matches. – jnpdx May 22 '23 at 16:05
  • You may want to look at: https://stackoverflow.com/questions/59624838/swiftui-tappable-subtext – jnpdx May 22 '23 at 16:06

1 Answers1

1

You can create a URL using a URLScheme

Something like yourApp://users/username and then use Markdown + LocalizedStringKey and/or AttributedText

Your string should look like "[@sample](yourApp://users/sample)"

Then you can handle the tap with

@Environment(\.openURL) private var openURL

This elaborates some more.

Listen to link click in SwiftUI TextView

You can replace your current function with something like

extension String {
    /// Replaces `"@\\w+"` with `"[@\(username)](yourApp://users/\(username))"`
    func addAppURLToUser() -> String {
        var inputString = self
        let regexPattern = "@\\w+"
        
        guard let regex = try? NSRegularExpression(pattern: regexPattern) else {
            return inputString
        }
        let range = NSRange(inputString.startIndex..<inputString.endIndex, in: inputString)
        let matches = regex.matches(in: inputString, range: range)
        for match in matches.reversed() {
            if let swiftRange = Range(match.range, in: inputString) {
                var username = String(inputString[swiftRange])
                username.removeFirst()
                let url = "[@\(username)](yourApp://users/\(username))"
                inputString.replaceSubrange(swiftRange, with: url)
            }
        }
        
        return inputString
    }
}

Then you can get URL filled text like this

text.addAppURLToUser()

You can use it in any SwiftUI Text with

extension String {
    var key: LocalizedStringKey {
        LocalizedStringKey(self)
    }
}

Then call it like Text(text.addAppURLToUser().key)

@available(iOS 15, *)
struct LinkView: View {        
    let text = "Tagging @sample @today and maybe someone else @test today I am attending with @someoneElse"
    @Environment(\.openURL) private var openURL
    @State var selectedUser: String?
    var body: some View {
        VStack{
            Text(text.addAppURLToUser().key)
                .environment(\.openURL, OpenURLAction { url in
                    if let components = URLComponents(url: url, resolvingAgainstBaseURL: true){
                        
                        var username = components.path
                        username.removeFirst()
                        selectedUser = username
                    }
                    return .handled
                })
            if let selectedUser = selectedUser {
                Text("tapped user = **\(selectedUser)**")
            }
            
        }
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48