【问题标题】:Get frame of a word in SwiftUI Text在 SwiftUI Text 中获取单词的框架
【发布时间】:2020-04-28 15:16:13
【问题描述】:

我想找到一个单词在句子中的位置,以便为该单词设置一个框架。 比如像这样:

目前是否可以在 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 上的相同代码与故事板

【问题讨论】:

    标签: swiftui swiftui-text


    【解决方案1】:

    您可以通过拆分文本、循环遍历它,然后使用overlay 框住您想要的文字来实现此目的。

    看这个例子:

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

    结果:

    【讨论】:

    【解决方案2】:

    工作了几个小时后,我得出了一个解决方案,这就是它的样子

    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.blue 替换为TextView,我相信它将适用于您现有的代码。您的主要问题是使用position,您通过添加midXmidY 进行调整。您也没有将alignment: .topLeading 用于您的ZStack。这基本上可以转换为 UIKit 坐标。
    【解决方案3】:

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

    1. 您需要在您的.overlay() 中使用带有alignment: .topLeading 的ZStack。一旦 rects 中有多个值,您的 ForEach 将无法始终作为 .overlay() 的根工作,并且 SwiftUI 默认从中心布局。
    2. 您需要使用.offset() 而不是.position()。位置用于框架的中心。
    
    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]))
                        }
                    }
            )
        }
    }
    

    结果:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-09-02
      • 1970-01-01
      • 2015-05-30
      • 1970-01-01
      • 1970-01-01
      • 2020-02-02
      • 2015-09-29
      • 1970-01-01
      相关资源
      最近更新 更多