I should create a text from a set of texts with an action. An example:
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec metus urna, mollis sit amet tincidunt a, elementum in tellus. Donec placerat pharetra scelerisque. ")
.onTapGesture {
print("action")
}
Text("Integer eleifend sit amet eros sed elementum. Integer eleifend aliquam mi. Integer enim diam, scelerisque vel dui eu, tincidunt feugiat odio. Duis posuere imperdiet lobortis. Cras faucibus cursus ipsum sed consectetur.")
.onTapGesture {
print("action")
}
I tried various ways, but without success
For example I tried the ( )
Text("test").onTapGesture { print("action")}
Text("test").onTapGesture { print("action")}
I tried to concatenate Text
, but text with gesture is type View
.
I tried a series of buttons and with a UIViewRepresentable
but without success.
Anyone have an idea on how to fix it or if there is a possible solution?
Thanks!
CodePudding user response:
thanks to @Asperi
Text("[Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec metus urna, mollis sit amet tincidunt a, elementum in tellus. Donec placerat pharetra scelerisque](action1) [Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis ultrices elementum orci, gravida lacinia dui congue quis. Vivamus at augue velit.](action2)")
.environment(\.openURL, OpenURLAction { url in
print("\(url)")
return .handled
})
It works since iOS 14
CodePudding user response:
You can achieve that by combining the components of the texts (word by word) using custom alignment rules. Here is the code:
enum Content {
case staticText(String)
case responsiveText(String, onTapGesture: () -> ())
}
struct ContentText: View {
@State private var splitText: [String]
let count: Int
init(_ text: String, separator: Character = " ") {
var splitText = text.split(separator: separator).map { "\($0) " }
if text.hasPrefix(String(separator)) {
splitText = [String(separator)] splitText
}
self._splitText = State(initialValue: splitText)
self.count = splitText.count
}
var body: some View {
ForEach(splitText.indices, id: \.self) { index in
Text(splitText[index])
}
}
}
struct ResponsiveText: View {
@State private var height: CGFloat = 0
@State private(set) var content: [Content]
// MARK: - body
var body: some View {
VStack {
GeometryReader { geometry in
// Needs to be .topLeading so we can modify alignments on top and leading
ZStack(alignment: .topLeading) {
self.zStackViews(geometry)
}
.background(calculateHeight($height))
}
}.frame(height: height)
}
// MARK: - Private API
private func zStackViews(_ geometry: GeometryProxy) -> some View {
// These are used to track the current horizontal and vertical position
// in the ZStack. As a new text or link is added, horizontal is decreased.
// When a new line is required, vertical is decreased & horizontal is reset to 0.
var horizontal: CGFloat = 0
var vertical: CGFloat = 0
// Determine the alignment for the view at the given index
func forEachView(_ index: Int) -> some View {
let numberOfViewsInContent: Int
let view: AnyView
// Determine the number of views in the Content at the given index
switch content[index] {
case .staticText(let text):
let label = ContentText(text)
numberOfViewsInContent = label.count
view = AnyView(label)
case .responsiveText(let text, let onTapGesture):
let label = ContentText(text)
numberOfViewsInContent = label.count
view = AnyView(label.onTapGesture(perform: onTapGesture))
}
var numberOfViewsRendered = 0
// Note that these alignment guides can get called multiple times per view
// since ContentText returns a ForEach
return view
.alignmentGuide(.leading, computeValue: { dimension in
numberOfViewsRendered = 1
let viewShouldBePlacedOnNextLine = geometry.size.width < -1 * (horizontal - dimension.width)
if viewShouldBePlacedOnNextLine {
// Push view to next line
vertical -= dimension.height
vertical -= 6
horizontal = -dimension.width
return 0
}
let result = horizontal
// Set horizontal to the end of the current view
horizontal -= dimension.width
return result
})
.alignmentGuide(.top, computeValue: { _ in
let result = vertical
// if this is the last view, reset everything
let isLastView = index == content.indices.last && numberOfViewsRendered == numberOfViewsInContent
if isLastView {
vertical = 0
horizontal = 0
numberOfViewsRendered = 0
}
return result
})
}
return ForEach(content.indices, id: \.self, content: forEachView)
}
// Determine the height of the view containing our combined Text and Link views
private func calculateHeight(_ binding: Binding<CGFloat>) -> some View {
GeometryReader { geometry -> Color in
DispatchQueue.main.async {
binding.wrappedValue = geometry.frame(in: .local).height
}
return .clear
}
}
}
And here is an example of the usage:
@main
struct ResponsiveTextViewApp: App {
let text1 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec metus urna, mollis sit amet tincidunt a, elementum in tellus. Donec placerat pharetra scelerisque. "
let text2 = "Integer eleifend sit amet eros sed elementum. Integer eleifend aliquam mi. Integer enim diam, scelerisque vel dui eu, tincidunt feugiat odio. Duis posuere imperdiet lobortis. Cras faucibus cursus ipsum sed consectetur."
let text3 = "Etiam in odio eu velit faucibus ultricies. Etiam porttitor aliquam lectus in eleifend. Quisque varius suscipit arcu ut rutrum. Nulla quis turpis ullamcorper, molestie ex at, sagittis turpis. Vestibulum molestie mauris quis enim feugiat consequat. Etiam ultrices felis id aliquet sagittis. Nulla vel iaculis felis, vitae vulputate eros."
var body: some Scene {
WindowGroup {
ResponsiveText(content: [
.staticText(text1),
.responsiveText(text2, onTapGesture: { print("action 1") }),
.responsiveText(text3, onTapGesture: { print("action 2") })
])
.foregroundColor(.red)
.font(.body)
}
}
}
Of course, it's not a very effective approach, but it does the job. Also, I must confess that it's not entirely my idea; I've just modified the approach described in this article.