4

In Keynote (and other apps), I've noticed the "standard" interface of doing Undo/Redo is by providing an Undo button on the tool bar.

Clicking the button (that is always enabled) Undos the recent operation. (If there is not recent operation to undo, it will show the Undo/Redo menu).

Long-clicking the Undo button opens an Undo/Redo menu.

I searched for methods of implementing this, and the best answer I found so far is at the following link.

I wonder if anyone knows of a simpler way?

Thanks!

Community
  • 1
  • 1
Reuven
  • 2,142
  • 3
  • 18
  • 30
  • The iDevice undo gesture is to shake the device. – tripleee Mar 21 '12 at 05:35
  • Yes, but shaking an iPad is a lot less convenient than shaking a phone, so Apple's apps have introduced this convention. – rickster Mar 21 '12 at 05:46
  • 2
    tripleee, if you look at iPad apps, you'll see the common undo method is an undo menu, not a gesture. when you think about it, shaking to undo may look cool, but is totally un-practicle, especially if you have numerous undos in a row... – Reuven Mar 21 '12 at 05:47
  • -1 for rejecting valid solutions with follow on requirements that were not specific up front. – NSProgrammer May 21 '12 at 00:58
  • Actually, I thought (and still do) that given that I provided a link (http://stackoverflow.com/questions/2655630/how-can-you-add-a-uigesturerecognizer-to-a-uibarbuttonitem-as-in-the-common-undo) to a question and specified I'm looking further, then of-course I won't accept answers that appear in that link... But as I don't want this to be a flame-war, and I actually didn't mean to reject your answer (only to not-accept it... since it was only there, I'll +1 it if you edit the answer [my vote is locked otherwise]...) – Reuven May 21 '12 at 04:12

4 Answers4

4

After reviewing all methods and discussing with friends, below is the solution I used, for a UIBarButtonItem the responds to both taps and long-press (TapOrLongPressBarButtonItem).

It is based on the following principals:

  1. Subclass UIBarButtonItem
  2. Use a custom view (so it's really trivial to handle the long-press - since our custom view has no problem responding to a long-press gesture handler...)

... So far - this approach was in the other SO thread - and I didn't like this approach since I couldn't find and easy enough way of making the custom view appear like an iPad navigation bar button... Soooo...

Use UIGlossyButton by Water Lou (thanks water!). This use is encapsulated within the subclass...

The resulting code is as follows:

@protocol TapOrPressButtonDelegate;
@interface TapOrPressBarButtonItem : UIBarButtonItem {  
    UIGlossyButton* _tapOrPressButton;
    __weak id<TapOrPressButtonDelegate> _delegate;
}
- (id)initWithTitle:(NSString*)title andDelegate:(id<TapOrPressButtonDelegate>)delegate;
@end

@protocol TapOrPressButtonDelegate<NSObject>
- (void)buttonTapped:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem;
- (void)buttonLongPressed:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem;
@end

@implementation TapOrPressBarButtonItem
- (void)buttonLongPressed:(UILongPressGestureRecognizer*)gesture {
    if (gesture.state != UIGestureRecognizerStateBegan)
        return;
    if([_delegate respondsToSelector:@selector(buttonLongPressed:withBarButtonItem:)]) {
        [_delegate buttonLongPressed:_tapOrPressButton withBarButtonItem:self];
    }
}

- (void)buttonTapped:(id)sender {
    if (sender != _tapOrPressButton) {
        return;
    }

    if([_delegate respondsToSelector:@selector(buttonTapped:withBarButtonItem:)]) {
        [_delegate buttonTapped:_tapOrPressButton withBarButtonItem:self];
    }   
}

- (id)initWithTitle:(NSString*)title andDelegate:(id<TapOrPressButtonDelegate>)delegate {
    if (self = [super init]) {
        // Store delegate reference
        _delegate = delegate;

        // Create the customm button that will have the iPad-nav-bar-default appearance 
        _tapOrPressButton = [UIGlossyButton buttonWithType:UIButtonTypeCustom];
        [_tapOrPressButton setTitle:title forState:UIControlStateNormal];
        [_tapOrPressButton setNavigationButtonWithColor:[UIColor colorWithRed:123.0/255 green:130.0/255 blue:139.0/255 alpha:1.0]];
        // Calculate width...
        CGSize labelSize = CGSizeMake(1000, 30);
        labelSize = [title sizeWithFont:_tapOrPressButton.titleLabel.font constrainedToSize:labelSize lineBreakMode:UILineBreakModeMiddleTruncation];
        _tapOrPressButton.frame = CGRectMake(0, 0, labelSize.width+20, 30);

        // Add a handler for a tap
        [_tapOrPressButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
        // Add a handler for a long-press
        UILongPressGestureRecognizer* buttonLongPress_ = [[UILongPressGestureRecognizer alloc] initWithTarget:self
                                                                                                       action:@selector(buttonLongPressed:)];
        [_tapOrPressButton addGestureRecognizer:buttonLongPress_];

        // Set this button as the custom view of the bar item...
        self.customView = _tapOrPressButton;
    }
    return self;
}

// Safe guards...
- (id)initWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
    NSLog(@"%s not supported!", __FUNCTION__);
    return nil;
}

- (id)initWithImage:(UIImage *)image landscapeImagePhone:(UIImage *)landscapeImagePhone style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
    NSLog(@"%s not supported!", __FUNCTION__);
    return nil;
}

- (id)initWithTitle:(NSString *)title style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
    NSLog(@"%s not supported!", __FUNCTION__);
    return nil;
}
- (id)initWithBarButtonSystemItem:(UIBarButtonSystemItem)systemItem target:(id)target action:(SEL)action {
    NSLog(@"%s not supported!", __FUNCTION__);
    return nil;
}

- (id)initWithCustomView:(UIView *)customView {
    NSLog(@"%s not supported!", __FUNCTION__);
    return nil;
}

@end

And all you need to do is:

1. Instantiate is as follows:

TapOrPressBarButtonItem* undoMenuButton = [[TapOrPressBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Undo", @"Undo Menu Title") andDelegate:self];

2. Connect the button to the navigation bar:

[self.navigationItem setLeftBarButtonItem:undoMenuButton animated:NO];

3. Implement the TapOrPressButtonDelegate protocol, and you're done...

-(void)buttonTapped:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem {
    [self menuItemUndo:barButtonItem];
}

-(void)buttonLongPressed:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem {
    [self undoMenuClicked:barButtonItem];
}

Hope this helps anyone else...

Community
  • 1
  • 1
Reuven
  • 2,142
  • 3
  • 18
  • 30
0

If you are using IB (or in Xcode4 the designer...i guess it is called) then you can select "Undo" from the First responder and drag that action to a button. I can give you more specific instructions if that doesn't cover it.

Here's what it looks like Undo Interface Builder image

It's on the left underneath the column "Received actions" at the bottom

Coder404
  • 742
  • 2
  • 7
  • 21
  • Thanks - but what I'm looking for is the sample code (or at least a solid direction/lead) on how to get a callback for press _and_ a long-press for a toolbar item. – Reuven May 11 '12 at 08:17
  • @Reuven I found that in the [ios developer library](https://developer.apple.com/library/ios/#samplecode/SimpleUndo/Introduction/Intro.html#//apple_ref/doc/uid/DTS40008408) – Coder404 May 11 '12 at 10:47
  • I'm sorry for being unclear. What I am looking for is not ow to handle Undo - it's how to _imitate_ the behavior of the Undo UIBarButton, as implemented (for example) in Apple's own Keynote app. – Reuven May 14 '12 at 04:24
  • @Reuven What are you trying to undo? Text? drawing on a picture? – Coder404 May 14 '12 at 10:28
  • @coder04, I have the whole undo/redo working. And I even have a user-interface around it. All works. BUT, what I am looking for is Keynote-for-iPad-like user-experience. Did you see how Keynote works? – Reuven May 14 '12 at 14:43
  • @Reuven I have keynote for ios and mac. I guess you could use... [Undo manager?](http://developer.apple.com/library/ios/#documentation/General/Conceptual/Devpedia-CocoaApp/UndoManager.html) – Coder404 May 14 '12 at 20:26
  • Undo manager only handles the Model changes (referring to the MVC model) - I'm looking for the View - I want the same GUI experience. Please re-read the original question - I hope it will clarify the gap to you. – Reuven May 14 '12 at 20:49
0

I believe the key is actually in the UINavigationBar itself. Unlike UIButtons or other normal touch tracking objects, I suspect UIBarItems don't handle their own touches. They don't inherit UIResponder or UIControl methods. However UINavigationBar of course does. And I've personally added gestures straight to a UINavigationBar many times.

I suggest you override touch handling in a UINavigationBar subclass and check the touches against its children. If the child is your special Undo button you can handle it accordingly.

Ryan Poolos
  • 18,421
  • 4
  • 65
  • 98
  • Thanks - but what I'm looking for is the sample code (or at least a solid direction/lead) on how to get a callback for press _and_ a long-press for a toolbar item. – Reuven May 11 '12 at 08:18
  • So add a long press gesture recognizer to the navigation bar and when the button is long pressed open the menu? – Justin Paulson May 16 '12 at 16:46
  • Yep basically you would add the gesture to the navBar and in the gesture selector check if the touch point is within the rect of the button in question. Then do what needs to be done :) – Ryan Poolos May 16 '12 at 16:51
  • Well, this goes back to the same problem - since when the GR fires, and I need to check if the touch point is within the rect of the button in question - I don't know the rect of the button in question (since the frame is not an attribute of the UIBarButtonItem... – Reuven May 17 '12 at 08:45
  • You don't have to check the rect of the button you just have to check the side of the Navbar. The button doesn't have a frame because its not handling its own hittest. If you look carefully you can touch outside the button and it will still get hit. This is how the NavBar is handling it. – Ryan Poolos May 17 '12 at 11:54
0
UIButton* undoButton = [UIButton buttonWithType:UIButtonTypeCustom];
[undoButton addTarget:self action:@selector(undoPressStart:) forControlEvents:UIControlEventTouchDown];
[undoButton addTarget:self action:@selector(undoPressFinish:) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem* navButton = [[[UIBarButtonItem alloc] initWithCustomView:undoButton] autorelease];
self.navigationItem.rightBarButtonItem = navButton;

You don't necessarily have to add the UIBarButtonItem as the rightBarButtonItem, this is just and easy way to show you how to create your UIBarButtonItem with a custom view that is the UIButton you want to handle events.

You'll need to implement the undoPressStart: and undoPressFinish: by maintaining state. I'd say on start, store the current NSDate or some granular representation of the time. On finish, if check the time elapsed and if it is beyond a certain threshold, show the menu - otherwise (as well as if the start date was never captured) perform the undo.

As an improvement, you'll likely want to observe the UIControlEventTouchDragExit event as well to cancel the long press.

NSProgrammer
  • 2,387
  • 1
  • 24
  • 27
  • the approach of creating a button (as the custom view of the UIBarButtonItem) is valid, and in some ways even simpler than what you outlined - since the button's view can be used as the target of a long-press gesture recognizer, and we're all set... BUT this isn't a complete enough direction as the created custom button has no appearance to it - and as a result doesn't look like the other Bar Button Items... So I can't accept this answer. – Reuven May 17 '12 at 05:10
  • Seriously? Because you're unwilling to put in the energy to design the custom button you are going to reject the valid answer? The question should be rephrased to point out this artificial limitation youre imposing. – NSProgrammer May 21 '12 at 00:57
  • Actually, I thought (and still do) that given that I provided a link (http://stackoverflow.com/questions/2655630/how-can-you-add-a-uigesturerecognizer-to-a-uibarbuttonitem-as-in-the-common-undo) to a question and specified I'm looking further, then of-course I won't accept answers that appear in that link... But as I don't want this to be a flame-war, and I actually didn't mean to reject your answer (only to not-accept it... since it was only there, I'll +1 it if you edit the answer [my vote is locked otherwise]...) – Reuven May 21 '12 at 04:13
  • No war, I appreciate the offer for the +1 but I it's not what you were looking for, I can understand that. I've made my point that if you don't want to implement a valid and widely used way then that should be specified in the question (and perhaps with an explanation). We all appreciate referencing other SO threads that relevant though so I can understand your choice to use a link as a requirement of what is not desired. So no flame. Best of luck in your quest for a solution. – NSProgrammer May 21 '12 at 04:34