4

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.
}
Ethan Kay
  • 657
  • 6
  • 24

2 Answers2

15

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
                })
        }
    }
}
  • This is great. I've modified it a bit in the below answer to show what I'm using for my solution, but this effectively did it. Thanks! – Ethan Kay Jan 16 '22 at 04:08
  • 1
    Thanks. It should be noted however that this solution is available only on iOS 15 and above. https://developer.apple.com/documentation/swiftui/openurlaction/init(handler:) – Natanel Jan 18 '22 at 10:58
  • yes, correct, like on 72% of all devices introduced in the last four years use iOS 15. ref: https://developer.apple.com/support/app-store/ – workingdog support Ukraine Jan 19 '22 at 06:14
0

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

Ethan Kay
  • 657
  • 6
  • 24