Reputation: 630
I need to make a specific underlined word be tappable within a paragraph of text for a SwiftUI view.
Currently my onTapGesture applies to the whole paragraph but I need it only on Text(labelOne) (AKA Action).
I cannot use onTapGesture AND .underline on Text(labelOne) because I get the error "Cannot convert value of type 'some View' to expected argument type 'Text'" if underline() is placed under onTapGesture {} OR "Cannot convert value of type 'some View' to expected argument type 'Text'" if I put onTapGesture{} under .underline().
In this case I am combining Text views, Text("Action") + Text("end of first sentence.") + Text("Body Text") so this is what prevents me from combining .onTapGesture with .underline()
It would be preferable to use a Button inside the paragraph so the user gets visual feedback that Action was pressed but not sure if that is possible without it being separate from the text?
If put Text in an HStack (which would allow me to add .underline() & .onTapGesture{} to a specific view) it looks bad if the sentence grows for Text(labelTwo), see below
struct TextView: View {
let labelOne: String = "Action"
let labelTwo: String = "end of first sentence."
let text: String = "Begining of second sentence lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem"
var body: some View {
HStack {
Text(labelOne) // <- Needs to be tappable, solely.
.underline()
+ Text(" \(labelTwo) ")
+ Text(text)
}
.onTapGesture { // <- Applies to whole paragraph (BAD)
print("Action pressed")
}
}
}
Upvotes: 5
Views: 2325
Reputation: 61
I was working on a similar problem, essentially trying to embed Button views (with string labels) into a sentence (or paragraph). Ashley's answer helped me figure out a solution that'll take an array of elements and produce a single Text view with embedded text "buttons". Here's the code.
extension Array where Element == InlineButtonElement {
@ViewBuilder func makeButtons (prepend: String, append: String) -> some View {
Group {
self.enumerated().reduce(Text("\(prepend) "), { last, next in
let (idx, spec) = next
var elementPrepend: String
if idx == self.indices.lowerBound {
elementPrepend = ""
} else if idx == self.indices.upperBound - 1 {
elementPrepend = " and "
} else {
elementPrepend = ", "
}
var label = spec.label
label.link = URL(string: "url://resource/\(idx)")!
return last + Text(elementPrepend) + Text(label)
}) + Text(" \(append)")
}
.environment(\.openURL, OpenURLAction(handler: { url in
if let idx = Int(url.lastPathComponent) {
self[idx].action()
}
return .handled
}))
}
}
struct InlineButtonElement {
var label: AttributedString
var action: (() -> Void)
init (label: String, attributes: AttributeContainer = AttributeContainer(), _ action: @escaping () -> Void) {
self.action = action
var attrLabel = AttributedString(stringLiteral: label)
self.label = attrLabel.settingAttributes(attributes)
}
}
At the call site:
struct Content: View {
@State var path: NavigationPath = .init()
@State var intElements = [1,2,3,4,5]
@State var strElements = ["One", "Two", "Three", "Four", "Five"]
var body: some View {
NavigationStack(path: $path) {
VStack {
intElements
.map { element in
var attr = AttributeContainer()
attr.foregroundColor = .blue
return InlineButtonElement(label: "Label \(element)", attributes: attr) {
path.append(element)
}
}
.makeButtons(prepend: "Inline buttons ", append: " built in SwiftUI")
strElements
.map { element in
var attr = AttributeContainer()
attr.foregroundColor = .blue
return InlineButtonElement(label: "Label \(element)", attributes: attr) {
path.append(element)
}
}
.makeButtons(prepend: "Inline buttons ", append: " built in SwiftUI")
}
.navigationDestination(for: Int.self) { newInt in
IntView_destination(newInt)
}
.navigationDestination(for: String.self) { newStr in
StrView_destination(newStr)
}
}
}
}
struct IntView_destination: View {
@State var int: Int
var body: some View {
Text("I am an int \(int)")
}
init (_ int: Int) {
_int = State(initialValue: int)
}
}
struct StrView_destination: View {
@State var str: String
var body: some View {
Text("I am a string \(str)")
}
init (_ str: String) {
_str = State(initialValue: str)
}
}
Produces this:
The "InlineButtonElement" struct creates a Button-like element that can be styled using AttributedString modifiers created using an AttributedContainer, which'll give you options for styling the link string.
Maybe a little overkill to get the commas and "ands" correct... :)
Upvotes: 1
Reputation: 53082
You can solve this in three steps:
First, define a custom URL scheme for your app, e.g.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myappurl</string>
</array>
</dict>
</array>
This can be done in your target's Info tab:
Secondly, change your first text to use Markdown, using Action
as the link text and a url that uses your newly defined URL scheme, e.g.
Text("[\(labelOne)](myappurl://action)").underline()
Finally, add the .onOpenURL
modifier. When you tap the link the app will try to open itself, and it can be handled by this modifier.
HStack {
Text("[\(labelOne)](myappurl://action)").underline() +
Text(" \(labelTwo) ") +
Text(text)
}.onOpenURL { link in
// do whatever action you want on tapping the link
}
Upvotes: 6