swiftswiftuiswiftui-viewswiftui-textswiftui-ontapgesture

How to make a specific subtext of Text view tappable and open a popover


I have a long label that I want to make into a Text view. For example:

"This is a really long text that is guaranteed to wrap around into 2 lines. There might be a solution. Click me to find out!"

For this Text, I want the "Click me to find out!" to be (1) tappable and (2) display a popover focused on it.

My current solution uses an HStack. This is fine if the label used in the text isn't that long but it becomes a problem when the label is too long, like in the example above.

  @State private var showPopover = false

...

    HStack(spacing: 0) {
        Text(LocalizationUtils.getMsg("This is a really long text that is guaranteed to wrap around into 2 lines. There might be a solution."))

        Text("Click me to find out")
          .underline()
          .onTapGesture {
            // display popover on tap
            showPopover = true

          }
          .popover(isPresented: $showPopover, arrowEdge: .bottom) {
            Text("The popover displayed!")
          }

Solution

  • I guess you do not want the gap between the two text elements. In that case try this approach using AttributedString.

    Example code:

    
    struct ContentView: View {
        @State private var showPopover = false
        @State private var attributed = AttributedString()
        
        var body: some View {
            Text(attributed)
                .tint(.black)
                .popover(isPresented: $showPopover, arrowEdge: .bottom) {
                    Text("The popover displayed!").padding()
                }
                .environment(\.openURL, OpenURLAction { url in
                    if url.scheme == "app", url.host == "show" {
                        showPopover = true
                        return .handled
                    }
                    return .systemAction
                })
                .padding()
                .onAppear {
                    let clickString = "Click me to find out"
                    let markdown = """
                    This is a really long text that is guaranteed to wrap around into 2 lines. There might be a solution. [\(clickString)](app://show)
                    """
                    do {
                        attributed = try AttributedString(markdown: markdown)
                    } catch {
                        attributed = AttributedString(markdown)
                    }
                    if let aRange = attributed.range(of: clickString) {
                        attributed[aRange].underlineStyle = .single
                        attributed[aRange].foregroundColor = .black
                    }
                }
        }
    }