UndoManager Alert Followed By UIAlertView Problem

Discussion in 'iOS Programming' started by Darkroom, Sep 8, 2009.

  1. Darkroom Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #1
    so i have an NSImageView that can be dragged anywhere on the screen. every time it is finished repositioning (on touchesEnded) it's coordinates are saved to the userdefaults. i've added an observer to the userdefaults for this coordinates key, which allows for a simple implementation of NSUndoManager (undo move / redo move). all works well.

    now for the exciting part! :rolleyes: the NSImageView can be locked into place, unable to move. if the view has moved, is then locked, any attempt to undo/redo the position of the view will present an alert telling the user the NSImageView is locked, and giving them the option to "Cancel" or "Unlock". tapping "Unlock" should either undo or redo the location of the image view, but instead the alert panel continues to pop up, over and over, until the last bit of hair on my head has been all pulled out.

    Code:
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    	{
    	if([keyPath isEqualToString:OriginPositionFromCenterKey])
    		{
    		[COLOR="Green"]//Setup Undo Move[/COLOR]
    		redoMoveFloat = [[change objectForKey:NSKeyValueChangeNewKey] floatValue];
    		undoMoveFloat = [[change objectForKey:NSKeyValueChangeOldKey] floatValue];
    		[[undoManager prepareWithInvocationTarget:self] undoOrRedoMove:@"undo" toPosition:undoMoveFloat withOpposingMoveValue:redoMoveFloat];
    		[undoManager setActionName:[NSString stringWithFormat:NSLocalizedString(MoveButtonLabel, nil)]];
    		}
    	}
    
    - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
    	{
    	if (alertView == undoRedoMoveAlert)
    		{
    		if (buttonIndex == 1)
    			{
    			[self toggleLock];
    
    			if ([undoManager isUndoing])
    				[self undoOrRedoMove:nil toPosition:undoMoveFloat withOpposingMoveValue:redoMoveFloat];
    			if ([undoManager isRedoing])
    				[self undoOrRedoMove:nil toPosition:redoMoveFloat withOpposingMoveValue:undoMoveFloat];
    			}
    		}
    	}
    	
    - (void)undoOrRedoMove:(NSString *)toggle toPosition:(float)floatPositionFromCenter withOpposingMoveValue:(float)redoValue
    	{
    	if (lockIsLocked == YES)
    		{
    		undoRedoMoveAlert = [[UIAlertView alloc]	initWithTitle:[NSString stringWithFormat:NSLocalizedString(AlertViewText, nil)
    									message:nil  
    									delegate:self 
    									cancelButtonTitle:[NSString stringWithFormat:NSLocalizedString(AlertViewCancelButton, nil)] 
    									otherButtonTitles:[NSString stringWithFormat:NSLocalizedString(AlertViewUnlockButton, nil)], nil];
    		[undoRedoMoveAlert show];
    		[undoRedoMoveAlert release];
    		}
    		else
    		{
    		[COLOR="Green"]//Toggle Undo Or Redo[/COLOR]
    		if ([toggle isEqualToString:@"undo"])
    			{
    			[COLOR="Green"]//Setup Redo Move[/COLOR]
    			[[undoManager prepareWithInvocationTarget:self] undoOrRedoMove:nil toPosition:redoMoveFloat withOpposingMoveValue:undoMoveFloat];
    			[undoManager setActionName:[NSString stringWithFormat:NSLocalizedString(MoveButtonLabel, nil)]];
    			}
    
    		[UIView beginAnimations:@"undoOrRedoMove" context:NULL];	
    		[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
    		[UIView setAnimationDuration:kAnimationDuration];
    
    		[COLOR="Green"]//////Animation using floatPositionFromCenter float[/COLOR]
    
    		[UIView commitAnimations];
    		
    		[COLOR="Green"]//Save New Position As Default[/COLOR]
    		[kDefaults setFloat:floatPositionFromCenter forKey:OriginPositionFromCenterKey];
    		}
    	}
    
     
  2. dejo Moderator

    dejo

    Staff Member

    Joined:
    Sep 2, 2004
    Location:
    The Centennial State
    #2
    Isn't this what you really want?:
    Code:
    			if ([undoManager isUndoing])
    				[self undoOrRedoMove:nil toPosition:undoMoveFloat withOpposingMoveValue:redoMoveFloat];
    			[B]else[/B] if ([undoManager isRedoing])
    				[self undoOrRedoMove:nil toPosition:redoMoveFloat withOpposingMoveValue:undoMoveFloat];
    
     
  3. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #3
    well, ok. either way would work (right?). but unfortunately even after updating to your suggestion the problem still persists. :(

    and actually, that part of the code actually reads the following:
    Code:
    			if ([undoManager isUndoing])
    				[self undoOrRedoMove:[B]@"undo"[/B] toPosition:undoMoveFloat withOpposingMoveValue:redoMoveFloat];
    			else if ([undoManager isRedoing])
    				[self undoOrRedoMove:nil toPosition:redoMoveFloat withOpposingMoveValue:undoMoveFloat];
    
    my mistake. but i'm under the assumption that the UIAlertView popup that displays the imageview is locked interupts the original prepareWithInvocationTarget, so i just write it again here. perhaps that's the problem?
     
  4. dejo Moderator

    dejo

    Staff Member

    Joined:
    Sep 2, 2004
    Location:
    The Centennial State
    #4
    Your clickedButtonAtIndex: code is calling undoOrRedoMove: again (the very code that generates the UIAlertView that triggers the call to clickedButtonAtIndex: ). And I would suspect lockIsLocked has not changed value between calls and so the alert is being constantly re-shown.
     
  5. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #5
    that was my first thought, too. but even if instead of calling the [self toggleLock] method and plainly write:

    Code:
    if (buttonIndex == 1)
    	{
    	[B]lockIsLocked = NO;[/B]
    
    	if ([undoManager isUndoing])
    		[self undoOrRedoMove:@"undo" toPosition:undoMoveFloat withOpposingMoveValue:redoMoveFloat];
    	else if ([undoManager isRedoing])
    		[self undoOrRedoMove:nil toPosition:redoMoveFloat withOpposingMoveValue:undoMoveFloat];
    	}
    
    the problem still persists.
     
  6. dejo Moderator

    dejo

    Staff Member

    Joined:
    Sep 2, 2004
    Location:
    The Centennial State
  7. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #7
    It's a bool. Is that what you are asking?
     
  8. dejo Moderator

    dejo

    Staff Member

    Joined:
    Sep 2, 2004
    Location:
    The Centennial State
    #8
    Not really. I'm asking: is it an instance variable? Is it a property? Things like that.
     
  9. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #9
    Oh sorry. It's a property. Nonatomic/retain. Synthisize.

    [EDIT]... actually it's just: @property BOOL lockIsLocked;
     
  10. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #10
    Sample Project

    still unable to solve this crazy mystery. i'm attaching a small sample project with hopes that someone here can figure it out and show me what i'm doing wrong.
     

    Attached Files:

  11. dejo Moderator

    dejo

    Staff Member

    Joined:
    Sep 2, 2004
    Location:
    The Centennial State
    #11
    What are the exact steps to reproduce the recurring alerts? I tried to follow from the first post but things seemed to work fine.
     
  12. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #12
    1. move the broom several times.
    2. undo then redo all positions unlocked
    3. lock the broom and and attempt to undo move.
    4. press "unlock" when the alert panel comes up. (this should both unlock the broom's position and undo it's move.)
    4. repeat step 3 until all undos are complete.

    for some crazy reason it very rarely works for me. but almost everytime, when the object is locked, attempting to undo and pressing "unlock" will make the UIAlertView continue to pop up a seemingly random number of times. sometimes it pops up 4 more times, sometimes just 1, sometimes none, but it never actually accomplishes the undo.
     
  13. dejo Moderator

    dejo

    Staff Member

    Joined:
    Sep 2, 2004
    Location:
    The Centennial State
    #13
    For some reason, undoOrRedoMove:toPosition:withOpposingMoveValue: is being called multiple times before the first alert view is even displayed. I found this out by putting a breakpoint in where it sets up the undoRedoMoveAlert. For my last test, it hit this breakpoint three times before the alert even showed up. It's probably not coincidence that I had three moves I could undo on the stack. So, you need to look into what is triggering the call to undoOrRedoMove:toPosition:withOpposingMoveValue: and what is causing it to be triggered multiple times.
     
  14. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #14
    everytime the object moves position and is let go (touchesEnded), it's position is saved to the userdefaults. there is an observer for this user default that activates the observeValueForKeyPath method, which places it's previous value to the undo stack

    Code:
     ~~~ [defaults addObserver:self forKeyPath:OriginX options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
    
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    	{
    	if([keyPath isEqualToString:OriginX])
    		{
    		//Setup Undo Move
    		redoMoveFloat = [[change objectForKey:NSKeyValueChangeNewKey] floatValue];
    		undoMoveFloat = [[change objectForKey:NSKeyValueChangeOldKey] floatValue];
    		[B][[undoManager prepareWithInvocationTarget:self] undoOrRedoMove:@"undo" toPosition:undoMoveFloat withOpposingMoveValue:redoMoveFloat];[/B]
    		[undoManager setActionName:[NSString stringWithFormat:@"Move"]];
    		}
    	}
    
    from here the undoOrRedoMove:toPosition:withOpposingMoveValue: is being called everytime the userdefaults change, in order to add the new and old values of the key to the undo stack. i believe this is why you are seeing it being called multiple times before seeing any alert.

    the bizarre thing is that all of this works great on it's own, with only the NSUndoManager's alert. but it seems that if the the user has the imageview locked and tries to undo or redo, problems occur when the new UIAlert for the locked button comes up. i feel that the two UIAlertViews are somehow competing with each other and this is what's causing the issue.
     
  15. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #15
    ok... i misunderstood what you meant, but i now see the issue you are talking about. this is quite the pickle!
     
  16. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #16
    i'm warming up to the idea that this is impossible... any thoughts?
     
  17. dejo Moderator

    dejo

    Staff Member

    Joined:
    Sep 2, 2004
    Location:
    The Centennial State
    #17
    It may be just a band-aid (if you don't think you are able to determine why undoOrRedoMove: is being triggered multiple times), but perhaps you can have a class-level boolean that indicates whether you've asked to show the alertView already. If it's YES and you're trying to show another one, don't. That make sense?
     
  18. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #18
    i believe that it was impossible to do what i wanted correctly with the use of the userdefaults notification, so i removed it and did some shifting around. now the shape will properly undo/redo it's location, invoking the undoOrRedoMove: method only once, as it should.

    however, i'm not sure if it's possible to "interrupt" the NSUndoManager with a UIAlertView. by the time the UIAlertView pops up, the undo or redo invocation has already executed, and if the shape is locked into position, the invocation simply pops up the UIAlertView. this pop up of the UIAlertView is being counted as either an undo or redo, depending on what the user requested.

    ideally for this situation, it would be great if there was a method that would allow me to programatically recall (pressed "Unlock"on alert) or reset (pressed "Cancel" on alert) the last invocation by the undo manager. that way i could actually execute the undo/redo again, changing the lockIsLocked boolean before doing so to block the UIAlertView from poping up, and subsequent undoing (or redoing) would happen without messing up the order of the stack.

    any ideas?
     

    Attached Files:

  19. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #19
    I don't know what UI you use to invoke undo/redo but if it's impossible to undo/redo then you should disable that UI.

    In general, having a button that says 'Do this' and then putting up an alert that says 'You can't Do this' when someone taps the button isn't a good UI.
     
  20. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #20
    i understand what you are saying. but after shaking the device to undo or redo the movement of the imageview, the UIAlert that pops up is more of a reminder that the user had previously locked the location of the shape, which gives them the option to continue.

    that being said, you've giving me something to think about. perhaps the best solution would be to simply pop up a message stating that the view had been unlocked, if it was locked when the NSUndoManager is invoked. if they didn't mean to, then they can simply shake to redo the action...

    thanks for that... i think that's what i'll do :)

    yay! i'm finished my app :D
     
  21. wlh99 macrumors 6502

    Joined:
    Feb 7, 2008
    #21
    Even so, I was curious as your logic seemed right and I thought it worth while to download your project and take a look.

    I added a bunch of NSLogs to follow the flow.
    Code:
    - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
    
    never got called. I found that you hadn't set AlertTestsViewController as an AlertViewDelegate as so.
    Code:
    
    @interface AlertTestsViewController : UIViewController <UIAlertViewDelegate>
    
    
    Still didn't work. Next I found
    Code:
    // if ([undoManager isUndoing])
    
    Isn't resolving the way you expect. I've not used an undoManager before so I commented it out so the code always undo's. I think the delegate is what stumped you anyway.

    That worked.
     
  22. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #22
    i'm not sure i follow you. you've managed to move the shape with undoing or redoing after pressing "unlock" on the UIAlertView simply by adding <UIAlertViewDelegate> to the header?

    i admit i wasn't conforming by not including that in the header, but it was calling alert view delegate methods regardless. it seems optional to do so. maybe classes handle this by default when they are created, or maybe its because NSUndoManager sets this up as it too is an alert.
     
  23. wlh99 macrumors 6502

    Joined:
    Feb 7, 2008
    #23
    It wasn't just the delegate, although I just tried again and without the declaration alertview doesn't get called on my device. I'm not using the most current xcode yet, maybe that's why.

    But also if ([undoManager isUndoing]) in alertView isn't working as you expect. I got rid of that so it always undos and never redos. That made undo work so the shape moved back to the old position. You will need to find out why that if block is broken to get redo to work.

    Code:
    - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
    	{
    	if (alertView == undoRedoMoveAlert)
    		{
    		if (buttonIndex == 1)
    			{
    			NSLog(@"Unlock Button Pressed");
    			[self toggleLock];
    
    			// if ([undoManager isUndoing])
    		
    			NSLog(@"Undoing");
    			[self undoOrRedoMove:@"undo" toPosition:undoMoveFloat withOpposingMoveValue:redoMoveFloat];
    		
    		//	else if ([undoManager isRedoing]){NSLog(@"Redoing");
    		//	[self undoOrRedoMove:nil toPosition:redoMoveFloat withOpposingMoveValue:undoMoveFloat];}
    			}
    		}
    	}
    
     
  24. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #24
    yes i too noticed this. i think that isUndoing and isRedoing need to be bundled with the invocations added to the stack. as i mentioned earlier, by the time the UIAlertView pops up, the undoManager already executed, so nothing will happen if the UIAlertView calls isUndoing or isRedoing after the execution.

    i believe the final verdict is that the NSUndoManager can not be intercepted, or put on hold in order to present an alertview, or anything else. as PhoneyDeveloper mentioned, allowing that to happen wouldn't be considered strong UI, which is probably why there are no methods in the NSUndoManager class to allow it.
     

Share This Page