Why is characterRange(at: .zero) == nil? - ios

I have a UITextView with some text that exceeds its bounds. Thus, vertical scrolling is enabled.
Now I want to find the position of the first visible character. This is what I tried:
let firstCharacterPosition = characterRange(at: contentOffset)?.start
This works in most cases. However, when I scroll up and get close to the beginning of the text, the characterRange(at:) function suddenly returns nil.
In the beginning I thought it was only because of bouncing (when the contentOffset.y value shortly becomes < 0). But that's not the (only) reason.
I tried some other values and was surprised to find that
characterRange(at: .zero)
returns nil as well – just as text positions with a low positive y value.
Why is that?
How can I get a reliable UITextPosition for the first visible character?

Please, try this code (remove textView if you are calling it inside UITextView subclass):
let firstVisibleCharacterIndex = textView.layoutManager.characterIndex(for: textView.contentOffset, in: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)

Related

How to set UITextField width constraint to have room for a certain string

I have a UITextField that will display floating point values between 0 and 1.0 with 3 digits after the decimal point. So the widest text it will show is something like "0.000". I'd like to set the auto layout width constraint so that the text field always has just enough room to display this value.
The code below is close, but does not work.
let biggestString = "0.000"
let textAttrs = [NSAttributedStringKey.font: myField.font]
let size = (biggestString as NSString).size(withAttributes: textAttrs)
myField.widthAnchor.constraint(equalToConstant: size.width).isActive = true
It ends up displaying "1.0..." I'm guessing this is because a UITextField has some kind of padding around the text, so so I need to set the width to be the string width + the padding. But, I don't see a property from which I can read this padding amount. Is there a way to get it?
Try searching for the 'intrinsicContentSize'. According to the documentation this is what has to be set to indicate to the auto-layout how big the content is.
There was also a more elaborate discussion on how this can actually if the layout settings do not allow the resizing to work, see other question here:
How to increase width of textfield according to typed text?

Prevent UITextView from offsetting its text container

I am tying to modify the height of a UITextView dynamically (up to a max height) while the user enters text. I am experiencing a very strange behavior when there are an even number of lines in the text view.
I am using autolayout and the text view has a height constraint. I respond to calls to the text view's delegate (textViewDidChange(_:)), where I calculate and adjust the height constraint based on the contentSize.
Here is the code:
func textViewDidChange(_ textView: UITextView) {
let newHeight = textView.contentSize.height
let newConstraintConst = max(MinTextViewHeight, min(MaxTextViewHeight, newHeight))
self.textViewHeightConstraint.constant = newConstraintConst
}
This works well, it resizes the frame up to MaxTextViewHeight and then the text view can scroll. However, when there are an even number of lines in the text view, the text view adds a kind of offset to the bottom of its NSTextContainer, causing the top line to be cut off:
However, when there are odd lines the NSTextContainer is no longer offset:
At first I thought it was somehow being controlled by the text view's textContainerInset but that is only used to pad the space inside the NSTextContainer, as setting it to .zero removes the space inside but does not affect the offset (and incidentally makes it even worse, as the top line almost completely disappears):
I have looked through the UITextView class reference and I don't see any property that would let me manipulate or even get the value of this offset.
As a workaround I am increasing the text container's top inset and removing the bottom inset:
textView.textContainerInset = UIEdgeInsetsMake(10, 0, 0, 0)
This works so far, but I arrived at a value of 10 by trial-and-error, and so far I've only tested it on a single device.
I am not interested in more hacky workarounds that require fragile, fixed values; I am trying to understand how this offset is being set and a proper way to fix it. I'm hoping that someone can provide some insight, thanks!
Just a speculation, but I think the problem is that the text view assumes that the height of itself does not change while calling textViewDidChange, so it scrolls when it thinks it has to, regardless of you changing its frame.
Not sure if you think my solution is too hacky, but this will stop it from scrolling when you don't want it. I simply pin the content offset to the top as long as the wanted content size is smaller than your max size.
Just add this:
func scrollViewDidScroll(scrollView: UIScrollView) {
if textView.contentSize.height <= MaxTextViewHeight && textView.contentOffset.y > 0.0 {
textView.contentOffset.y = 0.0;
}
}

Scroll to a specific part of a UITextView in Swift

In the Swift app I'm creating, the user can type some text into a text view and then search for a specific string within it, just like in Pages (and numerous others).
I have the index of the character they are searching for as an Integer (i.e., they are on the third instance of "hello" in the massive text block they typed in, and I know that's the 779th letter of the text view) and I am trying to automatically scroll the text view to the string they're on, so that it pops out (like it would in Pages).
I am trying to jump to the applicable string with this command:
self.textview.scrollRangeToVisible(NSMakeRange(index, 0))
but it never jumps to the right place (sometimes too far down, sometimes way too far up), often depending on screen size, so I know that index doesn't belong right there.
How can I correctly allow the user to jump to a specific string in a text view, by making the text view scroll to a certain character?
You can get string, which is before index 779, and then calculate the height of this string and then scroll to this point.
let string = textView.text.substringWithRange(Range<String.Index>(start: textView.text.startIndex, end: textView.text.startIndex.advancedBy(779)))// your <779 string
let size = string.sizeWithAttributes([NSFontAttributeName:yourFont])
let point = CGPointMake(0, size.height)
scrollView.setContentOffset(point, animated:true)
The other answer doesn't seem to work anymore (Jan 2021). But there is a simpler way now:
// Range specific to the question, any other works
let rangeStart = textView.text.startIndex
let rangeEnd = textView.text.index(rangeStart, offsetBy: 779)
let range = rangeStart..<rangeEnd
// Convert to NSRange
let nsrange = NSRange(range, in: textView.text)
// ... and scroll
textView.scrollRangeToVisible(nsrange)

UITextView bounces uncontrollably while typing

I have been having this problem with a few of my UITextViews on my most recent project. All I have done is added a UITextView onto a ViewController and added some constraints to it. When I run my program and attempted to type in the UITextView it begins to bounce out of control. For instance, when I type the first character, the view will slide down and out of the screen. Then when I type the next few characters the textview will bounce back up to where I can see what I typed. Then I type a few more, and once again it bounces out of the screen. Has anyone else experienced this?
Also, sometimes the text will start in the middle of the UITextView.
Try add this code to textViewDidChange: method
func textViewDidChange(textView: UITextView) {
if textView.text?.characters.count ?? 0 > 0 {
let range = NSMakeRange(textView.text!.characters.count - 1, 1)
textView.scrollRangeToVisible(range);
}
}
It helped me.

iOS 7 Text Kit: When has NSLayoutManager filled its last NSTextContainer?

I'm using Text Kit in combination with UIPageViewController to create a book. The original text is stored in a text file (.html) and placed into a NSTextStorage.
The [NSLayoutManager addTextContainer:(NSTextContaner*)txt] method works great, but there is no way to know when the NSLayoutManager has filled the last NSTextContainer - it just keep returning NSTextContainer even after all the next is displayed. As result you get an all bank pages after the NSLayoutManager is done.
I've tried using the [NSLayoutManagerDelegate didCompleteLayoutForTextContainer: atEnd:] callback method but it isn't working properly. It returns atEnd flag = YES after filling each NSTextContainer, not just when the last NSTextContainer is filled. I have set UITextView.scrollable = NO (suggested elsewhere) but that doesn't help.
I also tried to check the text by calling UITextView.text when no text is displayed, but that method always return the contents of the entire NSTextStorage that lays behind the NSTextContainer/NSLayoutManager.
If I can't tell when the last container is filled I don't know when all the pages are laid out. Is there a way to test the UITextView to see if its empty or NSContainer, or NSLayoutManager to see done laying out text?
I was never able to get the call back method be called correctly - I think its a bug - but I found a way to check if the next container is going to be empty or not..
[_layoutManager addTextContainer:textContainer];
NSRange range = [_layoutManager glyphRangeForTextContainer:textContainer];
if (range.length == 0) {
return nil;// top adding container because this last one is empty
}
Check whether container is last by calling glyphRangeForTextContainer method. If it exceeds number of glyphs, that's the last container.
layoutManager.addTextContainer(container)
let glyphRange = NSMaxRange((layoutManager.glyphRangeForTextContainer(container)))
if glyphRange >= layoutManager.numberOfGlyphs {
// last container
}

Resources