Home > Software engineering >  SwiftUI concatenate multiline tappable Text
SwiftUI concatenate multiline tappable Text

Time:07-28

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.

  • Related