Reputation: 630
I need to make a specific word within a paragraph TAPPABLE AND UNDERLINED within a SwiftUI view that will be used in a UIKit app via UIHostingController.
I have tried the .onOpenURL { } / custom URL approach and this will not work in my case because it causes side effects in the UIKIT side of our app AND when passing a function defined in the UIKit ViewController I get the error "unrecognized selector sent to instance...."
I have found a work around that makes the WHOLE paragraph tappable by applying onTapGesture to the HStack containing the text but this is not ideal.....
Is there some way to use Swift's AttributedString to make a specific word tappable or call a specific function??
I have also tried various ways of making Text inside an HStack but the results never produce a paragraph that is uniform if different lengths / combinations of words are passed to the view...
I have also tried adding .onTapGesture to the specific Text view within the HStack but get the error...
ERROR **Cannot convert value of type 'some View' to expected argument type 'Text'
Is there a pure SwiftUI solution without using .onOpenURL to make a single word tappable within a paragraph?
Is it possible to use AttributedString in combination with .onTapGesture to call a function??
Bad results from using HStack combinations with varying different string lengths
**
private struct Paragraph: View {
let actionLabel: String = "Action" // <- Underlined tappable word
let actionLabelSecondary: String = "end of first sentence."
let textBody: String = "Beginning of second sentence lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum"
var body: some View {
HStack {
Text(actionLabel)
.underline()
.onTapGesture { // <- Cannot convert value of type 'some View' to expected argument type 'Text'
action()
}
+ Text(" \(actionLabelSecondary) ")
+ Text(textBody)
}
}
func action() {
print("Word tapped")
}
}
Upvotes: 3
Views: 601
Reputation: 12115
Initially I would have gone with AttributedString and CustomURL, but you pointed out that this won't work in your case.
So here is another approach. I admit this feels like a complete overkill, but it works. It's based on a custom FlowLayout
(thanks to Majid) and uses that to display the first label as a button followed by all single words of the other text labels, so they can naturally wrap depending on the view size.
I didn't test the integration into UIKit, but it should work. Just tell me.
struct ContentView: View {
let actionLabel: String = "Action" // <- Underlined tappable word
let actionLabelSecondary: String = "end of first sentence."
let textBody: String = "Beginning of second sentence lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum."
var body: some View {
FlowLayout {
Button {
action()
} label: {
Text(actionLabel + " ")
.underline()
}
ForEach(Array(zip(0..., actionLabelSecondary.components(separatedBy: " "))), id: \.0) { word in
Text(word.1 + " ")
}
ForEach(Array(zip(0..., textBody.components(separatedBy: " "))), id: \.0) { word in
Text(word.1 + " ")
}
}
.padding()
}
func action() {
print("Word tapped")
}
}
struct FlowLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
var totalHeight: CGFloat = 0
var totalWidth: CGFloat = 0
var lineWidth: CGFloat = 0
var lineHeight: CGFloat = 0
for size in sizes {
if lineWidth + size.width > proposal.width ?? 0 {
totalHeight += lineHeight
lineWidth = size.width
lineHeight = size.height
} else {
lineWidth += size.width
lineHeight = max(lineHeight, size.height)
}
totalWidth = max(totalWidth, lineWidth)
}
totalHeight += lineHeight
return .init(width: totalWidth, height: totalHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
var lineX = bounds.minX
var lineY = bounds.minY
var lineHeight: CGFloat = 0
for index in subviews.indices {
if lineX + sizes[index].width > (proposal.width ?? 0) {
lineY += lineHeight
lineHeight = 0
lineX = bounds.minX
}
subviews[index].place(
at: .init(
x: lineX + sizes[index].width / 2,
y: lineY + sizes[index].height / 2
),
anchor: .center,
proposal: ProposedViewSize(sizes[index])
)
lineHeight = max(lineHeight, sizes[index].height)
lineX += sizes[index].width
}
}
}
Upvotes: 2