Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

Darkroom

Guest
Original poster
Dec 15, 2006
2,445
0
Montréal, Canada
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];
		}
	}
 
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];
 
Isn't that 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];

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?
 
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.
 
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.

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.
 
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.
 

Attachments

  • AlertTests.zip
    29.3 KB · Views: 72
  • 1.jpg
    1.jpg
    50.6 KB · Views: 84
What are the exact steps to reproduce the recurring alerts? I tried to follow from the first post but things seemed to work fine.

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.
 
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.
 
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.

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.
 
any thoughts?
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?
 
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?
 

Attachments

  • AlertTestsRevised.zip
    26.6 KB · Views: 67
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.
 
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.

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
 
yay! i'm finished my app :D

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.
 
That worked.

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.
 
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];}
			}
		}
	}
 
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.
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.