UITextInput Touch Interactions
As a follow up to yesterday’s post about “Dissecting iPhone OS Touch Actions for Text” here’s a rough outline of the event flow as it would apply to a UITextInput as a child of a UIScrollView. This simply does an NSLog of the various actions that take place as you interact and tap around. For example a single tap logs:
2010-05-02 23:44:05.141 CoreTextEditor[91771:207] #1 end
2010-05-02 23:44:05.143 CoreTextEditor[91771:207] - Close menus
2010-05-02 23:44:05.144 CoreTextEditor[91771:207] - Clear selection & place carat
2010-05-02 23:44:05.144 CoreTextEditor[91771:207] - Enable Scrolling
(gdb)
but a double tap logs:
2010-05-02 23:45:25.965 CoreTextEditor[91771:207] #1 end
2010-05-02 23:45:25.966 CoreTextEditor[91771:207] - Close menus
2010-05-02 23:45:25.967 CoreTextEditor[91771:207] - Clear selection & place carat
2010-05-02 23:45:25.967 CoreTextEditor[91771:207] - Enable Scrolling
2010-05-02 23:45:26.079 CoreTextEditor[91771:207] - Disable Scrolling
2010-05-02 23:45:26.080 CoreTextEditor[91771:207] #5 start
2010-05-02 23:45:26.082 CoreTextEditor[91771:207] - Disable scrolling
2010-05-02 23:45:26.083 CoreTextEditor[91771:207] - Highlight word
2010-05-02 23:45:26.083 CoreTextEditor[91771:207] #3 or #6 end
2010-05-02 23:45:26.084 CoreTextEditor[91771:207] - Close loop
2010-05-02 23:45:26.085 CoreTextEditor[91771:207] - Show menu for appropriate selection
2010-05-02 23:45:26.086 CoreTextEditor[91771:207] - Enable Scrolling
(gdb)
It is far from complete and is only a rough guide. I also haven’t implemented all this yet so there’s probably a few glitches. Once I have it fully implemented I’ll post a working project example:
Header:
//
// EditorScrollView.h
// CoreTextEditor
//
// Created by Jeffrey Sambells on 10-05-02.
// Copyright 2010 TropicalPixels. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface EditorView : UIScrollView {
bool _dragEditingActive;
bool _hasMoved;
}
@end
Class:
//
// EditorScrollView.m
// CoreTextEditor
//
// Created by Jeffrey Sambells on 10-05-02.
// Copyright 2010 TropicalPixels. All rights reserved.
//
#import "EditorView.h"
#import "EditorDocumentView.h"
@implementation EditorView
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
self.autoresizesSubviews = NO;
//self.delegate = self;
self.userInteractionEnabled = YES;
self.minimumZoomScale = 1.0f;
self.maximumZoomScale = 1.0f;
self.scrollEnabled = YES;
self.bounces = YES;
self.bouncesZoom = NO;
self.contentSize = CGSizeMake(frame.size.width, frame.size.height * 2); // will auto rezies baed on content.
self.scrollsToTop = YES;
self.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
_dragEditingActive = NO;
}
return self;
}
- (void)dealloc {
[super dealloc];
}
#pragma mark -
#pragma mark Touch Events
/*
Observed touch interactions in iPhone OS 3.2 on text editng fields:
1. Single quick tap without a drag, on release:
- Close any menus.
- Clear any selections.
- Place carat before or after the word depending on tap proximity in UITextInputTokenizer.
2. Single quick tap without a drag repeated in same location after a "long"
pause (not a double tap, basically another tap in the same location later):
- If the calculated carat placement is the same then open the menu.
3. Long single tap:
- Disable scrolling.
- Close any menus.
- Clear any selections.
- Show the carat placement loop.
- Place the carat at the tap location.
- On end show the menu.
4. Single tap and drag:
- Move the scroll view, release does nothing.
5. Double tap (with release):
- Highlight the word.
- Show cut/copy/paste/replace menu.
6. Double tap with hold/drag:
- Highlight word.
- Disable scrolling.
- Show selection loop (square) with short delay that could be cancelled by the end touch.
- Resulting selection must include full word as either first or last in selection.
- On release show cut/copy/paste/replace menu.
7. Triple (or more) tap
- Cancel everything and results in #1.
*/
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Set movement flag.
_hasMoved = NO;
UITouch *touch = [touches anyObject];
if ([touch tapCount] == 1) {
// Show the loop in 0.45 sec.
[self performSelector:@selector(_startSingleTapDelayEditingAction:) withObject:touches afterDelay: 0.45];
} else if([touch tapCount] == 2) {
// Cancel the delayed action if it hasn't fired.
[EditorView cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startSingleTapDelayEditingAction:) object:touches];
// Double Tapping immediately cancells scrolling.
NSLog(@"- Disable Scrolling");
[self setScrollEnabled:NO];
// Start the action immediately.
[self _startDoubleTapEditingAction:touches];
}
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
{
// Set movement flag.
_hasMoved = YES;
// Cancel the delayed action if it hasn't fired.
[EditorView cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startSingleTapDelayEditingAction:) object:touches];
// If editing is active then do the move actions.
if ( _dragEditingActive ) {
[self _moveEditingAction:touches];
}
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
// Cancel the delayed action if it hasn't fired.
[EditorView cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startSingleTapDelayEditingAction:) object:touches];
// If we're in editing mode or we haven't moved
if (_dragEditingActive || !_hasMoved) {
[self _endEditingAction:touches];
}
// Reset moved flag.
_hasMoved = NO;
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
{
[super touchesCancelled:touches withEvent:event];
}
#pragma mark Touch Results
- (void)_startSingleTapDelayEditingAction:(NSSet *)touches;
{
NSLog(@"#3 start");
NSLog(@"- Disable Scrolling");
[self setScrollEnabled:NO];
_dragEditingActive = YES;
NSLog(@"- Close any menus");
NSLog(@"- Clear selections and place carat");
}
- (void)_startDoubleTapEditingAction:(NSSet *)touches;
{
NSLog(@"#5 start");
NSLog(@"- Disable scrolling");
[self setScrollEnabled:NO];
_dragEditingActive = YES;
NSLog(@"- Highlight word");
[self performSelector:@selector(_startDoubleTapDelayEditingAction:) withObject:touches afterDelay: 0.15];
}
- (void)_startDoubleTapDelayEditingAction:(NSSet *)touches;
{
NSLog(@"- Show selection loop (square) after a short delay");
}
- (void)_moveEditingAction:(NSSet *)touches;
{
UITouch *touch = [touches anyObject];
if ([touch tapCount] == 1) {
NSLog(@"#3 move");
NSLog(@"- Place carat at the tap location");
NSLog(@"Show carat placement loop");
} else if([touch tapCount] == 2) {
NSLog(@"#6 move");
NSLog(@"- Select must include full word as either first or last in selection");
}
}
- (void)_endEditingAction:(NSSet *)touches;
{
UITouch *touch = [touches anyObject];
if (!_dragEditingActive) {
NSLog(@"#1 end");
NSLog(@"- Close menus");
NSLog(@"- Clear selection & place carat");
} else {
[EditorView cancelPreviousPerformRequestsWithTarget:self selector:@selector(_startDoubleTapDelayEditingAction:) object:touches];
NSLog(@"#3 or #6 end");
NSLog(@"- Close loops");
NSLog(@"- Show menu for appropriate selection");
}
// reset flags;
NSLog(@"- Enable Scrolling");
[self setScrollEnabled:YES];
_dragEditingActive = NO;
}
@end