UITextView will shorten text? - ios

How to know wether UITextView will shorten text because lack of space? I know I can calculate with boundingRectWithSize and sizeThatFit, but what if exclusionPaths of the UITextView is changed. I want lay out string in a polygon, and increase polygon size, until string laid out without shortening. Any idea, how can get a bool return, wether current setup will shorten?
self.tv.textContainer.exclusionPaths = myArrayOfBezierPaths;

func isSizeFitForTextView() -> Bool{
let layoutManager = self.textView.layoutManager
let glyphIndex = layoutManager.glyphIndexForCharacter(at:( self.textView.text as NSString).length)
let range = layoutManager.truncatedGlyphRange(inLineFragmentForGlyphAt: glyphIndex)
return range.location != NSNotFound
}

Related

How do you get correct caret size AND position when using NSMutableParagraphStyle and paragraphSpacingBefore in Swift 4

I've been playing around with attributed text in a UITextView (Swift 4.2 and noticed that once I introduced "paragraphSpacingBefore" into my design, the Caret becmae too large on the first line of each new paragraph.
I found this suggested fix on Stackoverflow which seemed to work ok to fix the caret size. The problem I found was the caret itself floats above the target line when that line was the start of a new paragraph.
UITextView lineSpacing make cursor height not same
Caret Floats above the target line
I tried solving it, maintaining the core idea of the original solution and adding some offset logic. During debugging I noticed that the original answer for caret size always adjusts the size even when not required so I added a variance filter (only adjust if variance > 10%). Did this because I think adjusting every time will interfere with my soln. to the floating caret problem.
If someone can take a look at my proposed approach, suggest improvements or a better way etc i'd be grateful:
override func caretRect(for position: UITextPosition) -> CGRect {
var superRect = super.caretRect(for: position)
guard let isFont = self.font else {
return superRect
}
let proposedHeight: CGFloat = isFont.pointSize - isFont.descender
var delta: CGFloat = superRect.size.height - proposedHeight
delta = (delta * delta).squareRoot()
//If the delta is < 10% of the original height just return the original rect
if delta / superRect.size.height < 0.1 {
return superRect
}
superRect.size.height = isFont.pointSize - isFont.descender
// "descender" is expressed as a negative value,
// so to add its height you must subtract its value
superRect.origin.y = superRect.origin.y + delta
// delta is used to correct for resized caret floating above the target line
return superRect
}
I got a solution:
// Fix long cursor height when at the end of paragraph with paragraphspacing and wrong cursor position in titles with paragraph spacing before
override public func caretRect(for position: UITextPosition) -> CGRect {
var superRect = super.caretRect(for: position)
guard let isFont = self.font else { return superRect }
let location = self.offset(from: self.beginningOfDocument, to: position)
if let paragrahStyle = self.storage.attribute(.paragraphStyle, at: location, effectiveRange: nil) as? NSParagraphStyle {
superRect.origin.y += paragrahStyle.paragraphSpacingBefore
}
superRect.size.height = isFont.pointSize - isFont.descender
return superRect
}
The real problem paragraphSpacingBefore. So all you have to do is to get the paragraph styling attributes, get the spacing and move the cursor by that spacing. This works well with all the text.

getting consistent sizing for glyph backgrounds

Hey folks – I'm new to TextKit and trying to draw backgrounds & borders around specific attributes. I've gotten fairly close, but haven't yet found methods that don't generate very inconsistent sizing, which tends to look bad. Here's my first crack at it:
class MyLayout: NSLayoutManager {
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
super.drawBackground(forGlyphRange: glyphsToShow, at: origin)
guard let storage = textStorage else {
return
}
guard let context = UIGraphicsGetCurrentContext() else {
return
}
var codeBlockRect: CGRect? = nil
enumerateLineFragments(forGlyphRange: glyphsToShow) { (rect, usedRect, container, subRange, stop) in
var effectiveRange = NSRange()
let attributes = storage.attributes(at: subRange.location, effectiveRange: &effectiveRange)
storage.enumerateAttribute(.inlineCodeBlock, in: subRange) { (value, attributeRange, stop) in
guard value != nil else {
return
}
var background = self.boundingRect(forGlyphRange: attributeRange, in: container)
background.origin.x += origin.x
background.origin.y += origin.y
context.setFillColor(UIColor.lightGrey.cgColor)
context.setStrokeColor(UIColor.mediumGrey.cgColor)
context.stroke(background)
context.fill(background)
}
}
}
}
That produces these results:
Single line of text:
Multiple lines of text:
As you can see, there's about 3 pixels of difference between the sizes there. I imagine it's because boundingRect, as the documentation says:
Returns the smallest bounding rect which completely encloses the glyphs in the given glyphRange
But I haven't found a method that gives me a number closer to what I'm looking for. My ideal scenario is that every rectangle will have the exact same height.
Let me know if any more information is needed.
Update
It crossed my mind that this could be related to the proprietary font we're using, so I changed everything to use UIFont.systemFont, which didn't make any difference.
I found a workaround here – instead of defining my inlineCodeBlock as a background, i defined it as a custom underline style.
Doing that let me override drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:).
Once I had that, I was able to use baselineOffset to get a consistent positioning.

ios textView.selectedTextRange giving wrong range for textview containing Emojis

My function wants to get cursor current position in textview, but when i have string "😳 😱 😨 " (cursor is at last position)in textview and it return me range (0,6) and current index as 6, expected position is 3,Please let me know if any way to get cursor position
func getcurserforTextView(textView : UITextView ) -> Int {
var cursorPosition = 0
if let selectedRange = textView.selectedTextRange {
cursorPosition = textView.offset(from: textView.beginningOfDocument, to: selectedRange.start)
}
return cursorPosition
}
The selectedRange and offset(from:to:) values are based on the UTF-16 characters for the text in the text view. So the result of 6 for that string is correct since that string contains 6 characters when using the UTF-16 character encoding.
So depending on what you are doing with the obtained cursor position, you may need to convert that UTF-16 offset to a "normal" String index.
Please see Converting scanLocation from utf16 units to character index in NSScanner (Swift) which covers a similar situation and a way to perform the conversion.

How to get the CGRect of the present caret

I want to obtain the location of the present caret in UITextView as CGRect, but there seems to be no information. Even the official documentation, I can't imagine how to utilize this method and find the explanation. Now I could know the way of getting the offset in the UITextView, not CGRect. But, I really want to know the CGRect.
Any comments should be highly appreciated.
Thanks.
Try this:
extension UITextView {
var caret: CGRect? {
guard let selectedTextRange = self.selectedTextRange else { return nil }
return self.caretRect(for: selectedTextRange.end)
}
}
and use it like this:
let textView: UITextView = ...
if let caret = textView.caret {
// Do your thing here.
} else {
// Caret is undefined.
}
By the way, you were on the right track :-) The above solution is based on the UITextInput method you just mentioned:
func caretRect(for position: UITextPosition) -> CGRect
Return a rectangle used to draw the caret at a given insertion point.
The UITextPosition parameter represents a position in a text container; in other words, it is an index into the backing string in a text-displaying view.
//rect is an object of CGRect
var rect = TextView.frame

How to get a selected range of text from a UITextView or UITextField

I am trying to get the selected range of text from a UITextView (and or UITextField) so that I can edit the selected text, or modify an attributed string. The method below is triggered when I make a selection, but the code in the method returns null values.
- (void)textViewDidChangeSelection:(UITextView *)textView {
UITextRange *selectedRange = [textField selectedTextRange];
NSLog(#"Start: %# <> End: %#", selectedRange.start, selectedRange.end);
}
You can try this,
- (void)textViewDidChangeSelection:(UITextView *)textView {
UITextRange *selectedRange = [textView selectedTextRange];
NSString *selectedText = [textView textInRange:selectedRange];
}
Swift
First get the selected text range, and then use that range to get the actual text:
if let textRange = myTextView.selectedTextRange {
let selectedText = myTextView.text(in: textRange)
// ...
}
Notes:
Selecting text from a UITextField is done in the same way.
The range is a UITextRange, not an NSRange. This allows for proper selection of things like emoji and extended grapheme clusters. See this answer for related details about this.
Swift 5.0
here is how I selecte file name Panda from Panda.txt
func textFieldDidBeginEditing(_ textField: UITextField) {
// if textField.text is `Panda.txt`, offset will be 3
let offset = String(textField.text!.split(separator: ".").last!).length
let from = textField.position(from: textField.beginningOfDocument, offset: 0)
let to = textField.position(from: textField.beginningOfDocument,
offset:textField.text!.length - (offset+1) )
//now `Panda` will be selected
textField.selectedTextRange = textField.textRange(from: from!, to: to!)
//note: unwrap with `!` is not recommended, text here is 100% not null,so it's safe
}
Swift 4.1
In your UITextView Extension put the below code in a function, and use that in your controller:
You can call this method with your textView instance in the SelectionDidChange delegate method from your view-Controller. Better to wrap this function call with condition textView.selectedRange.length > 0, to get some text...
let begin = self.beginningOfDocument
let start = self.position(from: begin, offset: selectedRange.location)
let end = self.position(from: position(from: start!, offset: 0)!, offset: selectedRange.length)
let txtRange = self.textRange(from: start!, to: end!)
let txt = self.text(in: txtRange!)
print("Sel Text is \(String(describing: txt))")
We can not use the optional binding to store the selected range, instead you can declare an optional for nsrange type, then use the if- let ... thing.
TextInputComponent has a property to get the selected text range.
let range = textView.selectedRange
Then you can use range.location, range.length values to change attributes of text in the container, etc...

Resources