Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITextView alignment, caret goes left

I'm using a UITextView where I want the text to be centered. However, this doesn't work fully. When adding a new line at the end of another line that is not the last line, or pressing on an empty line, the caret positions itself to the left of the textview, and when you start typing it usually jumps to the center. But sometimes it stays left-aligned and the text itself gets left aligned as well.

Creating the UITextView and setting the alignment programmatically or via the storyboard does not seem to matter. You can test it for yourself easily by adding a UITextView and setting the alignment to centered or right aligned, as that seems to have the same problem.

A gif to illustrate the problem: http://i.stack.imgur.com/aZxrK.gif

like image 354
Kebabpizza Avatar asked Oct 23 '14 08:10

Kebabpizza


2 Answers

This is the closest I've come to a solution for this:

I implement UITextViewDelegate's textView:shouldChangeTextInRange:replacementText: method and check if the entered text is a linebreak not at the end of the text. If that's the case, I append a space to it and put it into the text view. This results in the caret appearing in the center as it should.

The only downside to this is that you have a space the user did not enter and if he deletes it, then the caret moves to the left side again. Still, it's better than nothing.

This is the code I used:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    BOOL isMidTextLinebreak = [text isEqualToString:@"\n"] && range.location != textView.text.length;
    if (isMidTextLinebreak) {
        NSMutableString *updatedText = [[NSMutableString alloc] initWithString:textView.text];
        text = [text stringByAppendingString:@" "];
        [updatedText insertString:text atIndex:range.location];
        textView.text = updatedText;

        // Set new caret position
        NSRange newRange = range;
        newRange.location += text.length;
        textView.selectedRange = newRange;
    }

    return !isMidTextLinebreak;
}
like image 90
mattsson Avatar answered Oct 16 '22 23:10

mattsson


BEFORE:

enter image description here

AFTER

enter image description here

This is clearly a bug, still around in iOS12. As it was not an option to use some hacks, like pasting an invisible space and then replacing it again, I finally found the best solution for this that works like a charm.

Setting the textAlignment to center on every textDidChange: hasn't felt right and wasn't helpful either. I rather set the alignment once and then override the caretRectForPosition: in the UITextView subclass, returning the centered caret rect.

Please note that the bug happens only if you are in the middle of the multiline text in UITextView and there is a newline before and after the caret. So we look for exactly that situation and help iOS with positioning the caret rightly.

Here is the snippet in ObjC:

-(CGRect)caretRectForPosition:(UITextPosition *)position {
    CGRect superclassRect = [super caretRectForPosition:position];
    NSRange selectedTextRange = [self selectedRange];
    NSRange rangeOfCaret = NSMakeRange(selectedTextRange.location + selectedTextRange.length, 0);
    NSRange rangeOfCharBeforeCaret = NSMakeRange(rangeOfCaret.location - 1, 1);
    NSRange rangeOfCharAfterCaret = NSMakeRange(rangeOfCaret.location, 1);
    NSRange fullRange = NSMakeRange(0, self.text.length);

    if (NSLocationInRange(rangeOfCharBeforeCaret.location, fullRange) && NSLocationInRange(NSMaxRange(rangeOfCharAfterCaret), fullRange) && /* This check is sufficient to safely call substringWithRange. In general case we would have to check both endpoints of both ranges */
    [[self.text substringWithRange:rangeOfCharBeforeCaret] isEqualToString:@"\n"] && [[self.text substringWithRange:rangeOfCharAfterCaret] isEqualToString:@"\n"]) {
        // Newline before and after the caret. Make it centered
        CGFloat currentCenterXOfCaret = CGRectGetMidX(superclassRect);
        CGFloat centerXOfCenteredCaret = CGRectGetMidX(self.bounds);
        CGFloat correction = centerXOfCenteredCaret - currentCenterXOfCaret;
        CGRect rectOfCenteredCaret = CGRectOffset(superclassRect, correction, 0);

        return rectOfCenteredCaret;
    }

    return superclassRect;
}
like image 26
boweidmann Avatar answered Oct 17 '22 00:10

boweidmann