【问题标题】:Creating a CGRect around a UITextView - Wrong Height在 UITextView 周围创建 CGRect - 高度错误
【发布时间】:2019-06-13 11:27:39
【问题描述】:

我正在 UITextview 左侧创建一个动态列,以匹配每个段落的高度。出于某种原因,我在获取范围的正确高度时遇到了问题。我正在使用:

let test = textView.firstRect(for: models.first!.range)

当您继续输入时,它后面只有一行。例子:

2 行

3 行

有什么想法吗?

【问题讨论】:

    标签: ios swift uitextview


    【解决方案1】:

    这是一个文档可以使用一些帮助的示例...

    来自https://developer.apple.com/documentation/uikit/uitextinput/1614570-firstrect

    返回值

    文本范围内的第一个矩形。您可以使用此矩形来绘制校正矩形。当范围包含多行文本时,名称中的“第一个”是指包围第一行的矩形。

    事实上,这并不完全正确。

    例如,如果您选择文本:

    您没有矩形。使用调试视图层次结构:

    很明显,您有 两个 矩形。

    所以,func firstRect(for range: UITextRange) -> CGRect 实际上从 矩形集 中返回了第一个矩形,其中需要包含范围。

    要获取文本范围(例如段落)的实际高度,您需要使用:

    let rects = selectionRects(for: textRange)
    

    然后循环遍历返回的 UITextSelectionRect 对象数组。


    编辑:

    有多种不同的方法可以实现这一点,但这里有一个快速简单的示例,循环选择矩形并将它们的高度相加:

    //
    //  ParagraphMarkerViewController.swift
    //
    //  Created by Don Mag on 6/17/19.
    //
    
    import UIKit
    
    extension UITextView {
    
        func boundingFrame(ofTextRange range: Range<String.Index>?) -> CGRect? {
    
            guard let range = range else { return nil }
            let length = range.upperBound.encodedOffset-range.lowerBound.encodedOffset
            guard
                let start = position(from: beginningOfDocument, offset: range.lowerBound.encodedOffset),
                let end = position(from: start, offset: length),
                let txtRange = textRange(from: start, to: end)
                else { return nil }
    
            // we now have a UITextRange, so get the selection rects for that range
            let rects = selectionRects(for: txtRange)
    
            // init our return rect
            var returnRect = CGRect.zero
    
            // for each selection rectangle
            for thisSelRect in rects {
    
                // if it's the first one, just set the return rect
                if thisSelRect == rects.first {
                    returnRect = thisSelRect.rect
                } else {
                    // ignore selection rects with a width of Zero
                    if thisSelRect.rect.size.width > 0 {
                        // we only care about the top (the minimum origin.y) and the
                        // sum of the heights
                        returnRect.origin.y = min(returnRect.origin.y, thisSelRect.rect.origin.y)
                        returnRect.size.height += thisSelRect.rect.size.height
                    }
                }
    
            }
            return returnRect
        }
    
    }
    
    class ParagraphMarkerViewController: UIViewController, UITextViewDelegate {
    
        var theTextView: UITextView = {
            let v = UITextView()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = .yellow
            v.font = UIFont.systemFont(ofSize: 17.0)
            return v
        }()
    
        var paragraphMarkers: [UIView] = [UIView]()
    
        let colors: [UIColor] = [
            .red,
            .green,
            .blue,
            .cyan,
            .orange,
        ]
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.addSubview(theTextView)
    
            NSLayoutConstraint.activate([
    
                theTextView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 60.0),
                theTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -60.0),
                theTextView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 80.0),
                theTextView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20.0),
    
                ])
    
            theTextView.delegate = self
    
            // start with some example text
            theTextView.text = "This is a single line." +
            "\n\n" +
            "After two embedded newline chars, this text will wrap." +
            "\n\n" +
            "Here is another paragraph. It should be enough text to wrap to multiple lines in this textView. As you enter new text, the paragraph marks should adjust accordingly."
    
        }
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
    
            // update markers on viewDidAppear
            updateParagraphMarkers()
        }
    
        func textViewDidChange(_ textView: UITextView) {
            // update markers when text view is edited
            updateParagraphMarkers()
        }
    
        @objc func updateParagraphMarkers() -> Void {
    
            // clear previous paragraph marker views
            paragraphMarkers.forEach {
                $0.removeFromSuperview()
            }
    
            // reset paraMarkers array
            paragraphMarkers.removeAll()
    
            // probably not needed, but this will make sure the the text container has updated
            theTextView.layoutManager.ensureLayout(for: theTextView.textContainer)
    
            // make sure we have some text
            guard let str = theTextView.text else { return }
    
            // get the full range
            let textRange = str.startIndex..<str.endIndex
    
            // we want to enumerate by paragraphs
            let opts:NSString.EnumerationOptions = .byParagraphs
    
            var i = 0
    
            str.enumerateSubstrings(in: textRange, options: opts) {
                (substring, substringRange, enclosingRange, _) in
    
                // get the bounding rect for the sub-rects in each paragraph
                if let boundRect = self.theTextView.boundingFrame(ofTextRange: enclosingRange) {
    
                    // create a UIView
                    let v = UIView()
    
                    // give it a background color from our array of colors
                    v.backgroundColor = self.colors[i % self.colors.count]
    
                    // init the frame
                    v.frame = boundRect
    
                    // needs to be offset from the top of the text view
                    v.frame.origin.y += self.theTextView.frame.origin.y
    
                    // position it 48-pts to the left of the text view
                    v.frame.origin.x = self.theTextView.frame.origin.x - 48
    
                    // give it a width of 40-pts
                    v.frame.size.width = 40
    
                    // add it to the view
                    self.view.addSubview(v)
    
                    // save a reference to this UIView in our array of markers
                    self.paragraphMarkers.append(v)
    
                    i += 1
    
                }
            }
    
        }
    
    }
    

    结果:

    【讨论】:

    • 好的!我应该如何遍历返回的数组并将它们合并?
    • @cookie.mink - 有多种方法可以完成您正在尝试做的事情......请参阅我的答案的编辑以获取一个应该让您上路的简单示例。
    • 哇,这太棒了。 2 个简单的问题: 1. 我删除数组中所有内容的原因是什么,我可能有 100 段,这可能是个问题还是文本解析很快? 2. 我将在每个“段落”视图中都有按钮,了解以上所有内容后,您能否给我一些快速建议,告诉我应该如何将段落链接到侧视图组件。我需要他们有每个段落的链接吗?所以如果段落被删除,视图/按钮会知道要删除哪个?
    • @cookie.mink - 如果你可能会有 "100's of paragraphs" 你有一个 lot 更多工作要做。您可能在任何时候都只有几个段落可见,因此解析所有不可见文本的效率将非常低。您需要开发逻辑来缓存不可见的文本...添加和操作 100 个子视图可能不可行,因此您可能想要创建自己的控件并自己绘制矩形... 可能 将它们放在表格视图中并同步滚动...但这远远超出了您最初问题的范围。
    【解决方案2】:

    使用下面的代码,您将获得文本视图的正确内容大小。

    let newSize = self.textView.sizeThatFits(CGSize(width: self.textView.frame.width, height: CGFloat.greatestFiniteMagnitude))
            print("\(newSize.height)")
    

    根据这个高度改变动态列的高度。如果您想在用户输入时更改列高,请在UITextViewDelegate 方法textViewDidChange 中执行此操作。

    希望这会有所帮助。

    【讨论】:

    • 他要的是height of the each paragraph,而不是整个textView的高度。
    • @TheTiger 正确!我需要得到每个段落的高度,所以需要使用firstRect或类似的东西!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多