Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a big paragraph with clickable Text in SwiftUI

Tags:

ios

swiftui

I received an assignment to fetch text from the cloud database and convert it into a Text view

the rules are as follows: each word that starts with $ and ends with $ needs to be bold:

let str = "$random$"
let extractStr = "random"
Text(extractStr).bold()

each word that starts with ~ and ends with ~ needs to be clickable

let str = "~random~"
let extractStr = "random"
Text(extractStr).onTapGesture { print("tapping")}

each word that starts with % and ends with % needs to be red

let str = "%random%"
let extractStr = "random"
Text(extractStr).foregroundColor(Color.red)

and so on. different kinds of rules.

the overall target is to combine all the text into one paragraph.

It is possible when summing the Texts views without any gestures, But when I try to sum a Textview with a tap gesture, everything falls apart

for example, I created a long text

let text1 = "$Hello$ my dear fellows. I want to provide #you# this
             %awesome% long Text. I shall $talk$ a bit more to show
             @you@ that when the text is $very very long$ , $it
             doesn’t behave as I wanted to$. \n\n Lets #investigate a
             bit more# and see how this text can behave. \n\n ~you can
             click me~ and ~you can click me also~ and if you need
             to, you have ~another clickable text~"

and my desired outcome is this:

enter image description here

So this is what I tried

I converted it into an array:

let array1 = [$Hello$, my, dear, fellows., I, want, to, provide,
              #you#, this, %awesome, long, text. ......]

after that, I did some checking and created an array of Texts:

var arrayOfText: [Text] = []

I did some logic and ended up with this:

arrayOfText = [Text("Hello").bold(), Text("my"),
       Text("dear").underline(),
       Text("fellows."), Text("I"), Text("want"), Text("to")
       Text("provide"), Text("you").foregroundColor(.blue),
       Text("this"), Text("awesome").foregroundColor(.red),
       Text("long"), Text("text"), ... ... ...
       Text("another clickable text").onTapGesture{print("tap")}] // <-- Text with tap gesture, this is not valid...

Now I can loop through the texts and sum them all:

var newText: Text = Text("")
for text in arrayOfText {
    newText = newText + text
}

but It fails... I cant assign Text with tap gesture into an array of Text

I got this error:

Could not cast value of type 'SwiftUI.ModifiedContent<SwiftUI.Text, SwiftUI.AddGestureModifier<SwiftUI._EndedGesture<SwiftUI.TapGesture>>>' (0x7fc8c08e9758) to 'SwiftUI.Text' (0x7fff8166cd30).

So I tried to do some workaround and set the array to be an array of AnyView

var arrayOfText: [AnyView] = []

but then when I cast Text into AnyView

I receive this error:

Cast from 'AnyView' to unrelated type 'Text' always fails

any ideas how can I pull this off?

UPDATE

I tried using Asperi method:

struct StringPresenter: View {
   let text1 = "hello $world$ I am %a% &new&\n\n~robot~"
   var body: some View {
       ForEach(text1.split(separator: "\n"), id: \.self) { line in
           HStack(spacing: 4) {
               ForEach(line.split(separator: " "), id: \.self) { part in
                  self.generateBlock(for: part)
               }
           }
       }
   }

   private func generateBlock(for str: Substring) -> some View {
       Group {
           if str.starts(with: "$") {
               Text(str.dropFirst().dropLast(1))
                   .bold()
           } else
           if str.starts(with: "&") {
               Text(str.dropFirst().dropLast(1))
                   .font(Font.body.smallCaps())
           } else
           if str.starts(with: "%") {
               Text(str.dropFirst().dropLast(1))
                   .italic().foregroundColor(.red)
           } else
           if str.starts(with: "~") {
               Text(str.dropFirst().dropLast(1))
                   .underline().foregroundColor(.blue).onTapGesture { print("tapping ")}
           }
           else {
               Text(str)
           }
       }
   }

}

But it doesn't work with long text. the whole views collapse on each other

This is how it looks Like:

enter image description here

like image 406
Kevin Avatar asked May 27 '20 06:05

Kevin


2 Answers

Starting from iOS 15 you can use AttributedString or Markdown with Text. And so you get multiline formatted text.

An example of using markdown:

Text("This text is **bold**, this is *italic*. This is a tappable link: [tap here](https://stackoverflow.com)")

The advantage of AttributedString over Markdown is that it gives you more control over formatting. For example, you can set a link color or underline color:

var string = AttributedString("")

// you can use markdown with AttributedString
let markdownText = try! AttributedString(markdown: "This is **bold** text. This is *italic* text.")

var tappableText = AttributedString(" I am tappable! ")
tappableText.link = URL(string: "https://stackoverflow.com")
tappableText.foregroundColor = .green
    
var underlinedText = AttributedString("This is underlined text.")
underlinedText.underlineStyle = Text.LineStyle(pattern: .solid, color: .red)
    
string.append(markdownText)
string.append(tappableText)
string.append(underlinedText)

Text(string)

Here is what it looks like:

formatted text

A side note: if you want your tappable text to have a different behavior from opening a URL in a browser, you can define a custom URL scheme for your app. Then you will be able to handle tap events on a link using onOpenURL(perform:) that registers a handler to invoke when the view receives a url for the scene or window the view is in.

like image 73
Alexander Poleschuk Avatar answered Oct 31 '22 09:10

Alexander Poleschuk


You are pushing this version of SwiftUI beyond its current capabilities!

Something like this would more easily be done using the advanced text handling in UIKit, or by thinking outside the box and converting the text to something Like HTML.

If you MUST use SwiftUI, your best bet would probably be to layout the formatted text first onto a tappable paragraph/block, and then use gesture recognition at the block level to detect where in the block the tap took place - indirectly determining if the tap position coincided with the “tappable” text.

Update #1:

Example: To use a UITextView (which supports attributed text), you could use the UIViewRepresentable protocol to wrap the UIKit view and make it accessible from within SwiftUI. e.g. Using Paul Hudson's UIViewRepresentable example for the code...

struct TextView: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UITextView {
        return UITextView()
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
    }
}

The TextView can then be used directly within SwiftUI.

Now, while Textview gives you formatting, it does not give you the clickability you need without a lot of extra work, but a WKWebView used to render an HTML version of your text would allow you to convert the clickable text into HTML links that could be handled internal to your new SwiftUI view.

Note: The reason I say that you are pushing SwiftUI to its limits is that the current version of SwiftUI hides a lot of the configurability that is exposed in UIKit and forces you to do cartwheels to get to a solution that is often already present in UIKit.

Update #2:

Here's a clickable version that uses UITextField and a NSAttributedString:

class MyTextView: UITextView, UITextViewDelegate {
    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        print(URL)
    
        return false
    }
}

struct SwiftUIView: UIViewRepresentable {
    @Binding var text: NSAttributedString

    func makeUIView(context: Context) -> MyTextView {
        let view = MyTextView()
        
        view.dataDetectorTypes = .link
        view.isEditable        = false
        view.isSelectable      = true
        view.delegate          = view
        
        return view
    }

    func updateUIView(_ uiView: MyTextView, context: Context) {       
        uiView.attributedText = text
    }
}

All you need to do now is convert the downloaded text into a suitable attributed string format and you have attributed formatting and clickability

like image 25
woneill1701 Avatar answered Oct 31 '22 08:10

woneill1701