Created
September 30, 2013 15:45
-
-
Save rcabaco/6765778 to your computer and use it in GitHub Desktop.
UITextView subclass to handle up/down cursor movement
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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