Advertisement

Finding the Location of the Cursor in UITextView, UITextField

A frequent problem for iOS engineers is to detect that the keyboard is hiding the space made for entry of text. I’ve had to solve this issue at least once in each of iOS 7, 8 and 9. This solution has not been tested with iOS 8, but it works great with iOS 9.

In the viewDidLoad method, setup these NSNotifications:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardMayAdjustView:)
name:UITextViewTextDidChangeNotification object:nil];

Here are the three methods: keyboardWillShow:, keyboardWillHide: and keyboardMayAdjustView:
// *************************************************************************************************
#pragma mark
#pragma mark Keyboard up/down notification delegates

- (void)keyboardWillShow:(NSNotification *)notification {
CGPoint pointed = CGPointZero;

NSDictionary *info = [notification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
self.keyboardHeight = kbSize.height;
self.keyboardOriginY = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].origin.y;
self.isKeyboardUp = YES;
for (UIView *viewa in [self.view subviews]) {
for (UIView *viewb in viewa.subviews) {
if ([viewb isKindOfClass:[LCTextView class]]) { // LCTextView inherits from UITextView
if ([viewb isFirstResponder]) { // got the textView
pointed = [self.view convertPoint:viewb.frame.origin toView:self.view];
// NSLog(@"939 - %.f", pointed.y);
break;
}
}
for (UIView *viewc in viewb.subviews) {
if ([viewc isFirstResponder]) { // got the textField
pointed = [viewc convertPoint:viewc.frame.origin toView:self.view.superview];
// NSLog(@"947 - %.f", pointed.y);
break;
}
}
}
}
CGFloat viewNewY = 0;
if (pointed.y > self.keyboardOriginY) { // move the view
viewNewY = self.keyboardOriginY - pointed.y - 50;
}

[UIView animateWithDuration:[[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue] animations:^{
CGRect framer = self.view.frame;
framer.origin.y = viewNewY;
self.view.frame = framer;
}];
}

- (void)keyboardWillHide:(NSNotification *)notification {
self.isKeyboardUp = NO;
NSDictionary *info = [notification userInfo];
[UIView animateWithDuration:[[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue] animations:^{
CGRect framer = self.view.frame;
framer.origin.y = 0;
self.view.frame = framer;
}];
}

// only required for textViews
-(void)keyboardMayAdjustView:(NSNotification *)notification {
CGPoint cursor = CGPointZero;
CGPoint pointed = CGPointZero;

for (UIView *viewa in [self.view subviews]) {
for (UIView *viewb in viewa.subviews) {
if ([viewb isKindOfClass:[LCTextView class]]) {
if ([viewb isFirstResponder]) { // got the textView
UITextView *tv = (UITextView *)viewb;
pointed = [self.view convertPoint:viewb.frame.origin toView:self.view];
cursor = [tv caretRectForPosition:tv.selectedTextRange.start].origin;
// NSLog(@"996 - %.f", pointed.y);
break;
}
}
}
}
CGFloat viewNewY = 0;
if (self.isKeyboardUp) {
if (pointed.y + cursor.y > self.keyboardOriginY) { // move the view
viewNewY = self.keyboardOriginY - (pointed.y + cursor.y) - 50;

// don't move the view up too high

if (-self.view.frame.origin.y < self.keyboardHeight) { [UIView animateWithDuration:0.2 animations:^{ CGRect framer = self.view.frame; framer.origin.y = viewNewY; self.view.frame = framer; }]; } } } }

Discussion
A few notes: define the private CGFloat objects keyboardHeight and keyboardOriginY. The LCTextView is a custom class which inherits from UITextView. There are no required UITextView or UITextField delegates to make this work. We basically scan through the view.subview hierarchy to find the desired object types, use if ([view isFirstResponder]) to further qualify the object as the appropriate one, use the convertPoint:toView: or caretRectForPosition: methods to determine where, exactly vertically, the cursor is. No need to use caretRectForPosition: in a UITextField, since, by definition, UITextField is a constant y positioned object.