Reputation: 457
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
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
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
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
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