skip to Main Content

In Swift 5.5 SwiftUI it’s now possible to have links in markdown text. Is it possible to use this to add a tap handler for subtext in a Text view? For example, I’m imagining doing something like the following, but I haven’t figured out how to construct a link that would work for this (I just get URL errors each time). Would setting up some kind of custom url schema work? Is there a better solution that doing that, like something I can add to the Text view that acts similar to UITextView‘s shouldInteractWith? The goal is to solve similar problems to what is mentioned here but without having to fall back to UITextView or non-wrapping HStacks or GeometryReaders with ZStacks.

let baseText = "apple banana pear orange lemon"
let clickableText = baseText.split(separator: " ")
                            .map{ "[($0)](domainThatImAllowedToCapture.../($0))" }
Text(.init(clickableText)).onOpenLink { link in
  print(link.split(separator: "/").last) // prints "pear" if word pear is tapped.
}

2

Answers


  1. Chosen as BEST ANSWER

    Using the method described by @workingdog, I've cleaned this up into the following working solution:

    import SwiftUI
    
    struct ClickableText: View {
      @Environment(.colorScheme) var colorScheme: ColorScheme
      
      private var text: String
      private var onClick: (_ : String) -> Void
      
      init(text: String, _ onClick: @escaping (_ : String) -> Void) {
        self.text = text
        self.onClick = onClick
      }
    
      var body: some View {
        Text(.init(toClickable(text)))
          .foregroundColor(colorScheme == .dark ? .white : .black)
          .accentColor(colorScheme == .dark ? .white : .black)
          .environment(.openURL, OpenURLAction { url in
            let trimmed = url.path
              .replacingOccurrences(of: "/", with: "")
              .trimmingCharacters(in: .letters.inverted)
            withAnimation {
              onClick(trimmed)
            }
            return .discarded
          })
      }
      
      private func toClickable(_ text: String) -> String {
        // Needs to be a valid URL, but otherwise doesn't matter.
        let baseUrl = "https://a.com/"
        return text.split(separator: " ").map{ word in
          var cleaned = String(word)
          for keyword in ["(", ")", "[", "]"] {
            cleaned = String(cleaned.replacingOccurrences(of: keyword, with: "\(keyword)"))
          }
          return "[(cleaned)]((baseUrl)(cleaned))"
        }.joined(separator: " ")
      }
    }
    
    

  2. you could try something like this example code. It loops over your baseText, creates the appropriate links, and when the link is tapped/actioned you can put some more code to deal with it.

    struct ContentView: View {
        let baseText = "apple banana pear orange lemon"
        let baseUrl = "https://api.github.com/search/repositories?q="
        
        var body: some View {
            let clickableText = baseText.split(separator: " ").map{ "[($0)]((baseUrl)($0))" }
            ForEach(clickableText, id: .self) { txt in
                let attributedString = try! AttributedString(markdown: txt)
                Text(attributedString)
                    .environment(.openURL, OpenURLAction { url in
                        print("---> link actioned: (txt.split(separator: "=").last)" )
                        return .systemAction
                    })
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search