Создание большого абзаца с интерактивным текстом в SwiftUI - PullRequest
3 голосов
/ 27 мая 2020

Я получил задание получить текст из облачной базы данных и преобразовать его в текстовое представление

правила следующие: каждое слово, которое начинается с $ и заканчивается на $, должно быть выделено жирным шрифтом:

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

каждое слово, которое начинается с ~ и заканчивается на ~, должно быть активным

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

каждое слово, которое начинается с% и заканчивается на%, должно быть красным

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

и так далее. различные виды правил.

общая цель - объединить весь текст в один абзац.

Это возможно при суммировании представлений текстов без каких-либо жестов, но когда я пытаюсь суммировать текстовое представление жестом касания все разваливается

например, я создал длинный текст

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~"

, и мой желаемый результат таков:

enter image description here

Вот что я пробовал

Я преобразовал его в массив:

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

после этого я провел некоторую проверку и создал массив текстов :

var arrayOfText: [Text] = []

Я сделал несколько лог c и в итоге получил следующее:

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...

Теперь я могу oop просмотреть тексты и просуммировать их все:

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

но это не удается ... Я не могу назначить текст с жестом касания в массив текста

Я получил эту ошибку:

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

Итак, я попытался сделать несколько обходной путь и установите массив как массив AnyView

var arrayOfText: [AnyView] = []

, но затем, когда я передаю текст в AnyView

, я получаю эту ошибку:

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

любые идеи, как я могу это осуществить?

ОБНОВЛЕНИЕ

Я пробовал использовать метод Аспери:

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)
           }
       }
   }

}

Но это не работает с длинным текстом. все просмотры сворачиваются друг на друга

Вот как это выглядит Пример:

enter image description here

Ответы [ 2 ]

2 голосов
/ 31 мая 2020

Вы продвигаете эту версию SwiftUI за пределы ее текущих возможностей!

Что-то подобное было бы проще сделать с помощью расширенной обработки текста в UIKit или нестандартного мышления и преобразования текста во что-то вроде HTML.

Если вы ДОЛЖНЫ использовать SwiftUI, лучше всего будет сначала разместить форматированный текст на нажимаемом абзаце / блоке, а затем использовать распознавание жестов на уровне блока, чтобы определить, где находится блок касание имело место - косвенно определяя, совпало ли положение касания с «нажимаемым» текстом.

Обновление # 1:

Пример: чтобы использовать UITextView (который поддерживает текст с атрибутами), вы можете используйте протокол UIViewRepresentable, чтобы обернуть представление UIKit и сделать его доступным из SwiftUI. например, Использование примера UIViewRepresentable Пола Хадсона для кода ...

struct TextView: UIViewRepresentable {
@Binding var text: String

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

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

TextView затем можно использовать непосредственно в SwiftUI.

Теперь, хотя Textview дает вам форматирование, он не дает вам нужной кликабельности без большой дополнительной работы, но WKWebView, используемый для рендеринга HTML версии вашего текста, позволит вам преобразовать интерактивный текст в HTML ссылки, которые могут обрабатываться внутри вашего нового представления SwiftUI.

Примечание. Я говорю, что вы используете SwiftUI до его пределов, потому что текущая версия SwiftUI скрывает большую часть настраиваемость, которая предоставляется в UIKit и вынуждает вас использовать колеса тележки, чтобы добраться до решения, которое часто уже присутствует в UIKit.

Обновление # 2:

Вот интерактивная версия, которая использует UITextField и 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
}
}

Все, что вам нужно сделать сейчас, это преобразовать загруженный текст в подходящий строковый формат с атрибутами, а также у вас есть атрибутивное форматирование и возможность нажатия

1 голос
/ 27 мая 2020

Вот демонстрация возможного подхода (конечно, синтаксический анализ и верстка тривиальны - только для демонстрации). Протестировано с Xcode 11.4 / iOS 13.4

demo

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)
            }
        }
    }
}
...