获取SwiftUI文本中的单词框架

获取SwiftUI文本中的单词框架,swiftui,swiftui-text,Swiftui,Swiftui Text,我想找出一个单词在句子中的位置,这样我就可以为这个单词设置一个框架。 例如: 目前是否可以在SwiftUI中执行此操作 我写了一些代码,这就是我得到的 import SwiftUI struct ContentView: View { @State var text = "I usually get ----- around nine o'clock every morning" @State var rects = [CGRect.zero] var body:

我想找出一个单词在句子中的位置,这样我就可以为这个单词设置一个框架。 例如:

目前是否可以在SwiftUI中执行此操作

我写了一些代码,这就是我得到的

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()
    }
}
UIKit上的代码与情节提要相同


您可以通过拆分文本,在其上循环,然后使用覆盖来框显所需的单词来实现这一点

请参见此示例:

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))
            }
        }
    }
}
结果:


您可以通过拆分文本,在其上循环,然后使用覆盖来框显所需的单词来实现这一点

请参见此示例:

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))
            }
        }
    }
}
结果:


我们无法直接测试您的代码,因为我们没有UITextView。firstRectfor:。但是,有两件事是显而易见的:

您需要在.overlay中使用对齐为:.topLeading的ZStack。如果在RECT中有多个值,并且默认情况下SwiftUI从中心布局,则ForEach将无法作为.overlay的根始终工作。 您需要使用.offset而不是.position。“位置”用于框架的中心。 结构ContentView:View{ 变量rects:[CGRect]=[ 零 CGRectx:10,y:10,宽度:10,高度:50, CGRectx:20,y:20,宽度:10,高度:50, CGRectx:30,y:30,宽度:10,高度:50 ] func sizefor rect:CGRect->CGSize{ CGSizewidth:rect.minX,高度:rect.minY } var body:一些观点{ 颜色。蓝色。不透明度0.125 .框架宽度:200,高度:400 覆盖 ZStackalignment:。顶部引导{ Color.clear//最大化ZStack的大小
ForEach0..我们无法直接测试您的代码,因为我们没有UITextView.firstRectfor:。但是,有两件事是显而易见的:

您需要在.overlay中使用对齐方式为:.topLeading的ZStack。如果在rects中有多个值,并且默认情况下SwiftUI从中心布局,则ForEach将无法作为.overlay的根始终工作。 您需要使用.offset而不是.position。position用于帧的中心。 结构ContentView:View{ 变量rects:[CGRect]=[ 零 CGRectx:10,y:10,宽度:10,高度:50, CGRectx:20,y:20,宽度:10,高度:50, CGRectx:30,y:30,宽度:10,高度:50 ] func sizefor rect:CGRect->CGSize{ CGSizewidth:rect.minX,高度:rect.minY } var body:一些观点{ 颜色。蓝色。不透明度0.125 .框架宽度:200,高度:400 覆盖 ZStackalignment:。顶部引导{ Color.clear//最大化ZStack的大小
在工作了几个小时后,我找到了一个解决方案,这就是它的样子

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

我想看到一个更优雅的解决方案

在工作了几个小时后,我找到了一个解决方案,这就是它的外观

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

我希望看到一个更优雅的解决方案

如果您可以结合使用多行,则该解决方案无法正常工作:如果您可以结合使用多行,则该解决方案无法正常工作:您是否尝试过我的解决方案?如果您只需替换Color.blu,我相信它将与您现有的代码一起工作在您的原始代码中使用TextView。您的主要问题是通过添加midX和midY来使用您正在调整的位置。您也没有在ZStack中使用对齐:.topLeading。这基本上可以转换为UIKit坐标。您尝试过我的解决方案吗?我相信如果您只替换颜色,它将适用于您现有的代码.blue,在原始代码中使用TextView。您的主要问题是通过添加midX和midY来使用正在调整的位置。您也没有在ZStack中使用对齐:.topLeading。这基本上可以转换为UIKit坐标。