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