randomor
randomor

Reputation: 5663

Make a subtext tappable with dropdown?

We have previously solved similar issues like: SwiftUI tappable subtext

But how do you make subtext tappable, and also pop a dropdown at it's location when tapped? Do we need to use a webview? Split the strings with these timestamps and stitch them back?

Prefer a SwiftUI and AttributedString solution but is not required.

See below screenshot on how Apple solved this within Apple Podcast description, on tapping, the timestamp was highlighted, and a dropdown menu pops up:

Apple Podcast doing this

Upvotes: 1

Views: 118

Answers (1)

Sweeper
Sweeper

Reputation: 271105

You'd need to wrap a UITextView for now.

  • Override the menuConfigurationFor delegate method to return the desired menu you want.
  • Also override primaryActionFor so that it returns nil. This causes the menu to show up on tap, instead of on long tap.

Here is a simple example:

struct ContentView: View {
    var body: some View {
        TextViewWrapper(
            text: try! .init(markdown: "Hello World! This is some text, with a [link](https://xyz/abc)!"),
            actions: [
                MenuAction(title: "Some Action") { url in
                    print("Do something with", url)
                },
                MenuAction(title: "Another Action") { url in
                    print("Do something else with", url)
                },
            ]
        )
    }
}

struct MenuAction {
    let title: String
    let action: (URL) -> Void
}

struct TextViewWrapper: UIViewRepresentable {
    let text: AttributedString
    let actions: [MenuAction]
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.isEditable = false
        textView.delegate = context.coordinator
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.attributedText = .init(text)
        context.coordinator.actions = actions
    }
    
    func makeCoordinator() -> Coordinator {
        .init()
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        var actions = [MenuAction]()
        
        func textView(_ textView: UITextView, primaryActionFor textItem: UITextItem, defaultAction: UIAction) -> UIAction? {
            nil
        }
        
        func textView(_ textView: UITextView, menuConfigurationFor textItem: UITextItem, defaultMenu: UIMenu) -> UITextItem.MenuConfiguration? {
            guard case let .link(url) = textItem.content else { return nil }
            return .init(preview: nil, menu: UIMenu(children: actions.map { action in
                UIAction(title: action.title) { _ in action.action(url) }
            }))
        }
    }
}

You need to construct an AttributedString with some links, and encode whatever information you need into the URLs. Then in menuConfigurationFor, you can access the URL and create UIActions that do whatever you need to do.

In this example I only created a very simple MenuAction struct to represent UIActions, but of course you can add a lot more properties to it to represent the whole range of things that a UIAction can do, or even other subclasses of UIMenuElements.

Upvotes: 1

Related Questions