问题
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