Creating a CGRect around a UITextView - Wrong Height

﹥>﹥吖頭↗ 提交于 2020-01-06 08:36:12

问题


I am creating a dynamic column to the left of a UITextview that matches the height of each paragraph. For some reason I have a problem getting the correct height of the ranges. I am using:

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

It’s a single line behind as you keep typing. Examples:

2 Lines

3 Lines

Any ideas what’s wrong?


回答1:


This is an example where the docs could use a little help...

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

Return Value

The first rectangle in a range of text. You might use this rectangle to draw a correction rectangle. The “first” in the name refers the rectangle enclosing the first line when the range encompasses multiple lines of text.

Which is not, in fact, exactly right.

For example, if you select the text:

You don't have a rectangle. Using Debug View Hierarchy:

It's clear that you have two rectangles.

So, func firstRect(for range: UITextRange) -> CGRect actually returns the first rectangle from the set of rectangles needed to contain the range.

To get the actual height of the range of text (the paragraph, for example), you'll need to use:

let rects = selectionRects(for: textRange)

and then loop through the returned array of UITextSelectionRect objects.


Edit:

There are various different approaches to accomplish this, but here is a quick simple example of looping through selection rects and summing their heights:

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

            }
        }

    }

}

Result:




回答2:


Using below code you'll get the correct content size of the text view.

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

Change the height of the dynamic column according to this height. If you want to change the column height while user is typing then do this in UITextViewDelegate method textViewDidChange.

Hope this helps.



来源:https://stackoverflow.com/questions/56579419/creating-a-cgrect-around-a-uitextview-wrong-height

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!