Skip to content

Instantly share code, notes, and snippets.

@rcabaco
Created September 30, 2013 15:45
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rcabaco/6765778 to your computer and use it in GitHub Desktop.
Save rcabaco/6765778 to your computer and use it in GitHub Desktop.
UITextView subclass to handle up/down cursor movement
// Code to handle up/down cursor motion in a UITextView.
//
// Based on code from OmniGroup's OUITextView
// https://github.com/omnigroup/OmniGroup/blob/master/Frameworks/OmniUI/iPad/OUITextView.m
//
#import "TextView.h"
@interface TextView ()
@property (strong, nonatomic) NSArray *textCommands;
@property (nonatomic) UITextLayoutDirection verticalMoveDirection;
@property (nonatomic) CGRect verticalMoveStartCaretRect;
@property (nonatomic) CGRect verticalMoveLastCaretRect;
@end
@implementation TextView
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super initWithCoder:decoder];
if (self) {
self.verticalMoveStartCaretRect = CGRectZero;
self.verticalMoveLastCaretRect = CGRectZero;
}
return self;
}
- (NSArray *)keyCommands
{
if (!self.textCommands) {
UIKeyCommand *upCommand = [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:0 action:@selector(moveUp:)];
UIKeyCommand *downCommand = [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:0 action:@selector(moveDown:)];
self.textCommands = @[upCommand, downCommand];
}
return self.textCommands;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (action == @selector(moveUp:) || action == @selector(moveDown:)) {
return YES;
}
return [super canPerformAction:action withSender:sender];
}
#pragma mark -
- (void)moveUp:(id)sender
{
UITextPosition *p0 = self.selectedTextRange.start;
if ([self isNewVerticalMovementForPosition:p0 inDirection:UITextLayoutDirectionUp]) {
self.verticalMoveDirection = UITextLayoutDirectionUp;
self.verticalMoveStartCaretRect = [self caretRectForPosition:p0];
}
if (p0) {
UITextPosition *p1 = [self closestPositionToPosition:p0 inDirection:UITextLayoutDirectionUp];
if (p1) {
self.verticalMoveLastCaretRect = [self caretRectForPosition:p1];
UITextRange *r = [self textRangeFromPosition:p1 toPosition:p1];
self.selectedTextRange = r;
}
}
}
- (void)moveDown:(id)sender
{
UITextPosition *p0 = self.selectedTextRange.end;
if ([self isNewVerticalMovementForPosition:p0 inDirection:UITextLayoutDirectionDown]) {
self.verticalMoveDirection = UITextLayoutDirectionDown;
self.verticalMoveStartCaretRect = [self caretRectForPosition:p0];
}
if (p0) {
UITextPosition *p1 = [self closestPositionToPosition:p0 inDirection:UITextLayoutDirectionDown];
if (p1) {
self.verticalMoveLastCaretRect = [self caretRectForPosition:p1];
UITextRange* r = [self textRangeFromPosition:p1 toPosition:p1];
self.selectedTextRange = r;
}
}
}
- (UITextPosition *)closestPositionToPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction
{
// Currently only up and down are implemented.
NSParameterAssert(direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionDown);
// Translate the vertical direction to a horizontal direction.
UITextLayoutDirection lookupDirection = (direction == UITextLayoutDirectionUp) ? UITextLayoutDirectionLeft : UITextLayoutDirectionRight;
// Walk one character at a time in `lookupDirection` until the next line is reached.
UITextPosition *checkPosition = position;
UITextPosition *closestPosition = position;
CGRect startingCaretRect = [self caretRectForPosition:position];
CGRect nextLineCaretRect;
BOOL isInNextLine = NO;
while (YES) {
UITextPosition *nextPosition = [self positionFromPosition:checkPosition inDirection:lookupDirection offset:1];
if (!nextPosition || [self comparePosition:checkPosition toPosition:nextPosition] == NSOrderedSame) {
// End of line.
break;
}
checkPosition = nextPosition;
CGRect checkRect = [self caretRectForPosition:checkPosition];
if (CGRectGetMidY(startingCaretRect) != CGRectGetMidY(checkRect)) {
// While on the next line stop just above/below the starting position.
if (lookupDirection == UITextLayoutDirectionLeft && CGRectGetMidX(checkRect) <= CGRectGetMidX(self.verticalMoveStartCaretRect)) {
closestPosition = checkPosition;
break;
}
if (lookupDirection == UITextLayoutDirectionRight && CGRectGetMidX(checkRect) >= CGRectGetMidX(self.verticalMoveStartCaretRect)) {
closestPosition = checkPosition;
break;
}
// But don't skip lines.
if (isInNextLine && CGRectGetMidY(checkRect) != CGRectGetMidY(nextLineCaretRect)) {
break;
}
isInNextLine = YES;
nextLineCaretRect = checkRect;
closestPosition = checkPosition;
}
}
return closestPosition;
}
- (BOOL)isNewVerticalMovementForPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction
{
CGRect caretRect = [self caretRectForPosition:position];
BOOL noPreviousStartPosition = CGRectEqualToRect(self.verticalMoveStartCaretRect, CGRectZero);
BOOL caretMovedSinceLastPosition = !CGRectEqualToRect(caretRect, self.verticalMoveLastCaretRect);
BOOL directionChanged = self.verticalMoveDirection != direction;
BOOL newMovement = noPreviousStartPosition || caretMovedSinceLastPosition || directionChanged;
return newMovement;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment