Quiero encontrar la posición de una palabra en una oración para poder establecer un marco para esa palabra. Como así, por ejemplo: ingrese la descripción de la imagen aquí

¿Actualmente es posible hacer esto en SwiftUI?

Escribí un código y esto es lo que obtuve

import SwiftUI

struct ContentView: View {

    @State var text = "I usually get ----- around nine o'clock every morning"
    @State var rects = [CGRect.zero]

    var body: some View {
        ZStack {
            TextView(text: $text, rects: $rects)
                .overlay(
                    ForEach(0..<self.rects.count, id: \.self) { index in
                        RoundedRectangle(cornerRadius: 6)
                            .frame(width: self.rects[index].size.width, height: self.rects[index].size.height)
                            .position(self.rects[index].origin)
                            .foregroundColor(Color.red)
                    }
            )

        }
    }
}

struct TextView: UIViewRepresentable {

    @Binding var text: String
    @Binding var rects: [CGRect]

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UITextView {

        let textView = UITextView()
        textView.delegate = context.coordinator
        textView.isEditable = true
        textView.isUserInteractionEnabled = true
        textView.font = UIFont.systemFont(ofSize: 24)
        DispatchQueue.main.async {
            let ranges = self.searchRanges(in: textView.text)
            self.rects = self.viewRects(for: ranges, textView: textView)
        }
        return textView
    }

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

    class Coordinator : NSObject, UITextViewDelegate {

        var parent: TextView

        init(_ uiTextView: TextView) {
            self.parent = uiTextView
        }

        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            return true
        }

        func textViewDidChange(_ textView: UITextView) {
            self.parent.text = textView.text
        }
    }

    func searchRanges(in text: String) -> [Range<String.Index>] {
        var ranges = [Range<String.Index>]()
        var searchRange = text.startIndex ..< text.endIndex
        var range = text.range(
            of: "-----",
            options: .caseInsensitive,
            range: searchRange,
            locale: nil
        )
        while let findedRange = range {
            ranges.append(findedRange)
            searchRange = findedRange.upperBound ..< text.endIndex
            range = text.range(
                of: "-----",
                options: .caseInsensitive,
                range: searchRange,
                locale: nil
            )
        }

        return ranges
    }

    func viewRects(for rnges: [Range<String.Index>], textView: UITextView) -> [CGRect] {
        var rects = [CGRect]()
        for range in rnges {
            let upperBound = range.upperBound.utf16Offset(in: textView.text)
            let lowerBound = range.lowerBound.utf16Offset(in: textView.text)
            let length = upperBound - lowerBound

            if let start = textView.position(from: textView.beginningOfDocument, offset: lowerBound),
                let end = textView.position(from: start, offset: length),
                let txtRange = textView.textRange(from: start, to: end) {
                var rect = textView.firstRect(for: txtRange)
                rect.origin.x = rect.origin.x
                rect.origin.y = rect.midY
                rects.append(rect)
            }
        }
        return rects
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

enter image description here

Mismo código en UIKit con guión gráfico

enter image description here

0
Beslan Tularov 28 abr. 2020 a las 18:16

3 respuestas

No podemos probar directamente su código ya que no tenemos UITextView.firstRect(for:). Pero, dos cosas son evidentes:

  1. Debe usar un ZStack con alignment: .topLeading en su .overlay(). Su ForEach no funcionará de manera consistente como la raíz de .overlay() una vez que tenga más de un valor en rects, y SwiftUI se despliega desde el centro de manera predeterminada.
  2. Debe usar .offset() no .position(). La posición se usa para el centro de su marco.

struct ContentView: View {
    var rects: [CGRect] = [
        .zero,
        CGRect(x: 10, y: 10, width: 10, height: 50),
        CGRect(x: 20, y: 20, width: 10, height: 50),
        CGRect(x: 30, y: 30, width: 10, height: 50)
    ]

    func size(for rect: CGRect) -> CGSize {
        CGSize(width: rect.minX, height: rect.minY)
    }

    var body: some View {
        Color.blue.opacity(0.125)
            .frame(width: 200, height: 400)
            .overlay(
                ZStack(alignment: .topLeading) {
                    Color.clear // Maximize the size of the ZStack
                    ForEach(0..<self.rects.count, id: \.self) { idx in
                        Color.red
                            .frame(width: self.rects[idx].width, height: self.rects[idx].height)
                            .offset(self.size(for: self.rects[idx]))
                    }
                }
        )
    }
}

Resultado:

Result

0
arsenius 29 abr. 2020 a las 05:49

Después de trabajar durante varias horas, llegué a una solución y así es como se ve

import SwiftUI

struct TextView: View {

    @State var text = ""
    @State var gapText = ""
    @State var rects = [CGRect.zero]
    @State var pattern: String = "-----"

    var body: some View {
        ZStack {
            Representable(text: $text, rects: $rects, pattern: $pattern)
            ForEach(0..<self.rects.count, id: \.self) { index in
                ZStack {
                    Button(action: {

                    }) {
                        Text(self.gapText)
                            .foregroundColor(Color("SMTitle"))
                            .font(.system(size: 30, weight: .medium))
                            .multilineTextAlignment(.center)
                            .padding()
                    }
                    .frame(width: self.rects[index].size.width, height: self.rects[index].size.height)
                    .background(Color.white, alignment: .center)
                    .position(x: self.rects[index].origin.x, y:  self.rects[index].origin.y)
                    .overlay(
                        RoundedRectangle(cornerRadius: 6)
                            .stroke(Color("SMTitle"), lineWidth: 2)
                            .frame(width: self.rects[index].size.width, height: self.rects[index].size.height)
                            .position(x: self.rects[index].origin.x, y:  self.rects[index].origin.y)
                    )
                }

            }
        }
    }
}

struct Representable: UIViewRepresentable {

    @Binding var text: String
    @Binding var rects: [CGRect]
    @Binding var pattern: String

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UITextView {

        let view = UITextView()
        view.delegate = context.coordinator
        view.isEditable = false
        view.isUserInteractionEnabled = true
        view.textColor = UIColor(red: 0.325, green: 0.207, blue: 0.325, alpha: 1)
        view.font = UIFont.systemFont(ofSize: 30, weight: .medium)
        DispatchQueue.main.async {
            let ranges = self.searchRanges(in: view.text)
            self.rects = self.viewRects(for: ranges, textView: view)
        }
        return view
    }

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

    class Coordinator : NSObject, UITextViewDelegate {

        var parent: Representable

        init(_ uiTextView: Representable) {
            self.parent = uiTextView
        }

        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            return true
        }

        func textViewDidChange(_ textView: UITextView) {
            self.parent.text = textView.text
        }
    }

    func searchRanges(in text: String) -> [Range<String.Index>] {
        var ranges = [Range<String.Index>]()
        var searchRange = text.startIndex ..< text.endIndex
        var range = text.range(
            of: self.pattern,
            options: .caseInsensitive,
            range: searchRange,
            locale: nil
        )
        while let findedRange = range {
            ranges.append(findedRange)
            searchRange = findedRange.upperBound ..< text.endIndex
            range = text.range(
                of: self.pattern,
                options: .caseInsensitive,
                range: searchRange,
                locale: nil
            )
        }

        return ranges
    }

    func viewRects(for ranges: [Range<String.Index>], textView: UITextView) -> [CGRect] {
        var rects = [CGRect]()
        for range in ranges {
            let upperBound = range.upperBound.utf16Offset(in: textView.text)
            let lowerBound = range.lowerBound.utf16Offset(in: textView.text)
            let length = upperBound - lowerBound

            if let start = textView.position(from: textView.beginningOfDocument, offset: lowerBound),
                let end = textView.position(from: start, offset: length),
                let txtRange = textView.textRange(from: start, to: end) {
                var rect = textView.firstRect(for: txtRange)
                rect.origin.x = rect.midX
                rect.origin.y = rect.midY
                rect.size.height = rect.size.height
                rects.append(rect)
            }
        }
        return rects
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        TextView(text: "I usually get ----- around nine o'clock every morning", gapText: "up")
    }
}

Me gustaría ver una solución más elegante

0
Beslan Tularov 29 abr. 2020 a las 11:37

Puede lograr esto dividiendo el texto, girando sobre él y luego usando un overlay para enmarcar las palabras que desee.

Vea este ejemplo:

struct HighlightView: View {
    var words: [FramableWord] = []

    struct FramableWord: Identifiable {
        let id = UUID()
        let text: String
        let isFramed: Bool
    }

    func frame(word: String, in text: String) -> [FramableWord] {
        return text.split(separator: " ").map(String.init).map {
            FramableWord(text: $0, isFramed: $0 == word)
        }
    }

    init() {
        words = frame(word: "up", in: "I get up at 9")
    }

    var body: some View {
        HStack(spacing: 2) {
            ForEach(words) { word -> AnyView in
                if word.isFramed {
                    return AnyView(
                        Text(word.text)
                            .padding(2)
                            .overlay(RoundedRectangle(cornerRadius: 2).stroke(Color.blue, lineWidth: 2))
                    )
                }

                return AnyView(Text(word.text))
            }
        }
    }
}

Resultado:

example

1
bonky fronk 28 abr. 2020 a las 16:18