JacobCXDev
JacobCXDev

Reputation: 457

Is it possible to add Button<Text> to Text in SwiftUI?

Normally, adding two Text views will produce a single Text view retaining the properties of each individual Text view. For example:

Text(“Hello ”) + Text(“world!”).foregroundColor(.blue)

This should produce Hello world!, with world! in blue. Now, what should I do if I want world! to be tappable and act as a button? Well, logically it would make sense to do this:

Text(“Hello ”) + Button(action: {}) { Text(“world!”).foregroundColor(.blue) }

However, the code directly above does not compile. Is it possible to create Text views with tappable portions as described? My use cas is for Tweets with @ mentions or # hashtags where you would tap on either the mention or the hashtag to be shown a detail view.

EDIT: The reason why using a Text view and a Button in an HStack is unsuitable is because this is within a function which returns a Text view for the content of a tweet:

func styledText(text: String) -> Text {
    var output = Text("")
    let components = text.tokenize("@#. ")
    for component in components {
        if component.rangeOfCharacter(from: CharacterSet(charactersIn: "@#")) != nil {
            output = output + Text(component).foregroundColor(.accentColor)
        } else {
            output = output + Text(component)
        }
    }
    return output
}

Where String.tokenize is the following:

extension String {
    func tokenize(_ delimiters: String) -> [String] {
        var output = [String]()
        var buffer = ""
        for char in self {
            if delimiters.contains(char) {
                output.append(buffer)
                buffer = String(char)
            } else {
                buffer += String(char)
            }
        }
        output.append(buffer)
        return output
    }
}

EDIT 2: If it helps, I'm trying to create a SwiftUI version of this: https://github.com/optonaut/ActiveLabel.swift.

Upvotes: 8

Views: 4735

Answers (4)

laxrajpurohit
laxrajpurohit

Reputation: 11

If you want to add Button to Text elements to be part of a single clickable area, you can nest them within a single Button like this

                Button(action: {
                    // Your action here
                    print("Combined button clicked")
                }) {
                    HStack {
                        Text("Don't have an account?")
                            .font(.system(size: 12))

                        Text("Sign up")
                            .font(.system(size: 16))
                    }
                }//SignUp

This way, clicking either "Don't have an account?" or "Sign up" will trigger the same action because they are part of the same button.

Upvotes: 1

Schottky
Schottky

Reputation: 2024

Here's another solution, however this is only really useful for link-type styles. In other words, I don't think there's an easy solution to do any custom action if so desired.

My solution proposes to create a UIViewRepresentable that can display an attributed string:

struct AttributedText: UIViewRepresentable {
    
    var attributedString: NSAttributedString
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.attributedText = self.attributedString
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {}
    
}

you could then create an Attributed String like so:

var attributedString: NSAttributedString = {
    let attrString = NSMutableAttributedString(string: "Hello, ")
    let blueWorld = NSMutableAttributedString(string: "world!")
    let attributes: [NSAttributedString.Key : Any ] = [
        .link: NSURL(string: "https://twitter.com")!
    ]
    let fullRange = NSRange(location: 0, length: blueWorld.length)
    blueWorld.setAttributes(attributes, range: fullRange)
    attrString.append(blueWorld)
    return attrString
}()

or use any good library until there is a better option from apple for this. This allows you to simply display the text as it was a regular Text:

struct MyText {

    var body: some View {
        AttributedText(attributedString: myCustomAttributedString)
    }

}

This solution has potential when you're only interested in links and are okay with the default display of them (as far as I can tell, you can't change this).

Upvotes: 1

E.Coms
E.Coms

Reputation: 11539

You can extend the AnyView to get what you need. If you are VERY good at programming, you can introduce more complicated auto layout for your goal.

extension AnyView{
    static func + (left: AnyView, right: AnyView) -> AnyView{
        return AnyView(HStack{left.fixedSize(horizontal: true, vertical: false)
                              right.fixedSize(horizontal: true, vertical: false)})
    }
}


func styledText(text: String) -> AnyView {
    var output = AnyView(Text(""))
    let components = text.tokenize("@#. ")
    for component in components {
        if component.rangeOfCharacter(from: CharacterSet(charactersIn: "@#")) != nil {
            output = output + AnyView(Text(component).foregroundColor(.accentColor).onTapGesture {
                print(component)
            })
        } else {
            output = output + AnyView(Text(component))
        }
    }

    return output
}

Upvotes: 3

Chris
Chris

Reputation: 8091

I am not sure whether you want this or you want explicitly to use + but your goal you can reach by this

 HStack {
                Text("Hello ")
                Button(action: {
                    // do something
                    print("tapped")
                }, label: {
                    Text("world!").foregroundColor(.blue)
                })
            }

Upvotes: 3

Related Questions