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