Undo/redo with a UITextView (iOS/iPHone) - iphone

I have a view where a UITextView always has focus. What I want to do is extend the built-in undo/redo behavior to support undo/redo for when I programmatically set the text (e.g., for when I clear it, by setting it to #"").
Since only the firstResponder gets undo/redo events, I thought I'd simply use the UITextView's undoManager property to create my invocations. e.g.,
// Before clearing the text...
[[self.myTextView.undoManager prepareWithInvocationTarget:self] undoClear:self.myTextView.text];
[self.myTextView.undoManager setActionName:#"Clear"];
// ...
-(void)undoClear:(NSString *)textToRestore
{
self.myTextView.text = textToRestore;
if ([self.myTextView.undoManager isUndoing])
{
// Prepare the redo.
[[self.myTextView.undoManager prepareWithInvocationTarget:self] undoClear:#""];
}
}
Unfortunately, this is not working. It:
Introduces an empty item into the undo stack ("Undo")
The "Undo Clear" gets added after that item (if I tap "Undo", I see "Undo Clear")
Undo Clear and Redo Clear work, however, then I see "Undo Clear" again and it doesn't work from there on.
Any ideas? Am I approaching this wrong?
Update: It seems like I've figured out the empty undo item issue: it happens when I set the text of the UITextView after I've called prepareWithInvocationTarget. If I call it before, it doesn't happen. Funny thing is, the empty item isn't pushed onto the undo stack if I don't call prepareWithInvocationTarget (i.e., normally, when I set the text of a UITextView).

OK, figured it out:
The issue with #1 is as outlined in the update to my original post.
Issues #2 and #3 were just me using the NSUndoManager incorrectly. My final unClear method (which gets called on undos is as follows:
-(void)undoClear:(NSString *)textToRestore
{
NSString *textBackup = [self.myTextView.text copy];
self.myTextView.text = textToRestore;
if ([self.myTextView.undoManager isUndoing])
{
// Prepare the redo.
[[self.myTextView.undoManager prepareWithInvocationTarget:self] undoClear:#""];
}
else if ([self.myTextView.undoManager isRedoing])
{
[[self.myTextView.undoManager prepareWithInvocationTarget:self] undoClear:textBackup];
}
[textBackup release];
}
It's working as it should now.

You can change your textview to whatever text you want and preserve undo and redo manager's memory stacks. self.content is UITextView.
-(void) yourClearAllButtonIsPressed
{
[[self.content.undoManager prepareWithInvocationTarget:self] undoClear:self.content.text];
[self.content setText:#""];//Or something else you want to change your textview to
}
}
//just one, my method I created
-(void) undoClearBlablabla:(NSString*) text
{
[[self.content.undoManager prepareWithInvocationTarget:self] undoClear:self.content.text];
[self.content setText:text];
}

I'm not sue how many Text Fields you are working with, though textBackup is not getting retained by prepareWithInvocationTarget: How is it still in the Undo Stack several clear's later? Seems like it might still be there after the first one, though not after the 2nd one or after the Autorelease pool flushes.

Related

UITextField in cell that is not visible has userInteractionEnabled set to No

I am trying to skip over text fields that are disabled when a user navigates through my tableview. However, when they reach the bounds of the visible cells, everything gets out of whack because I am trying to detect if a text field is disabled, and if so, then recursively call my method again to navigate one more time. ie. if user presses a button to navigate to the right, and that text field is disabled, recursively call a right press again.
It seems, any text fields in cells outside what is visible are disabled. once the user reaches the edges of the table I go into infinite loops, or things just break.
here is my section of code where I make my enabled check and if not make my recursive call. This really shouldn't be that complicated. Logically all I want to do is detect if the text field we just moved to is disabled and if so, just initiate the same button press again. Nothing fancy.
edit with some playtesting it has become apparent that the nextTextFieldSelection is coming back as null, although the destinationIndexPath and newTag values are correct. Is it possible requesting an indexPath not visible is causing a null return?
//logic to move to next text field and manually scroll the tableViewbased on button input is here
nextTextFieldSelection = (UITextField *)[[_tableView cellForRowAtIndexPath:destinationIndexPath] viewWithTag:newTag];
if (nextTextFieldSelection.userInteractionEnabled == NO) {
switch (arrowButton.tag) {
case NumericKeyboardViewLeftArrow:
currentTextField = nextTextFieldSelection;
[self numericKeyboardView:(VS_NumericKeyboardView *)numericKeyboardView DidSelectArrowButton:(UIButton *)arrowButton];
return;
case NumericKeyboardViewRightArrow:
currentTextField = nextTextFieldSelection;
[self numericKeyboardView:(VS_NumericKeyboardView *)numericKeyboardView DidSelectArrowButton:(UIButton *)arrowButton];
return;
default:
break;
}
}
cellForRowAtIndexPath returns nil for not visible cells. Thats probably why you are getting into that infinite loop.
See: http://developer.apple.com/library/ios/documentation/uikit/reference/UITableView_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40006943-CH3-SW16

iPhone iOS 4 UIButton toggle highlighted state on and off

I got a UIButton with style "Info Dark" set up in interface builder in my iPhone 4 app. One of the properties of the button is "Highlighted", which displays a white highlight around the button.
I would like to toggle this white highlight on and off, indicating if the button function is active or not.
The button is linked for "Touch up inside" event in the interface builder with this callback:
infoButton.highlighted = !infoButton.highlighted;
After the first touch, the highlight disappears and does not toggle as I expect it to. What else do I need to do to make the highlight toggle and display the state of the button?
Thank you!
Update:
When loaded from the interface builder, the button stays highlighted, even as the view appears/disappears. What causes this to happen is the "shows touch on highlight" interface builder property. If I assign the code above to another button, the info button highlights on and off as expected. However, the touches of the info button itself interfere with the above code, causing the button to lose the "touch" highlight
Update 2: I added another info button, directly below the first info button, in the interface builder and made it glow permanently. To create the appearance of the toggle, I hide and unhide the glowInfoButton below the real one. This works as expected:
infoButton.highlighted = NO;
glowInfoButton.highlighted = YES;
glowInfoButton.enabled = NO;
glowInfoButton.hidden = YES;
- (IBAction)toggleInfoMode:(id)sender {
// infoButton.selected = !infoButton.selected;
glowInfoButton.hidden = !glowInfoButton.hidden;
}
The Highlighted image is what displays when the UIButton is being pressed, and is controlled within the UIButton itself.
You're looking for the Selected property. You can set a Selected Image in IB, and then put a infoButton.selected = !infoButton.isSelected; in your TouchUpInside callback.
The highlighted property doesn't work like that, buttons aren't toggles.
It's just to know if the button is being pressed, if I'm correct.
If you want to implement that functionality, I recommend you subclass UIButton or UIControl.
Now that I see what you really were after I would advise subclass UIButton and check for a call to an event then toggle highlight state accordingly. You can do this without adding the dummy button.
in a custom button class implementation file place the following code, or similar:
#import "HighlightedButton.h"
#implementation HighlightedButton
BOOL currentHighlightState;
-(void)toggleHighlight:(id)sender {
self.highlighted = currentHighlightState;
}
-(void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
//get the string indicating the action called
NSString *actionString = NSStringFromSelector(action);
//get the string for the action that you want to check for
NSString *touchUpInsideMethodName = [[self actionsForTarget:target forControlEvent:UIControlEventTouchUpInside] lastObject];
if ([touchUpInsideMethodName isEqualToString:actionString]){
//toggle variable
currentHighlightState = !currentHighlightState;
//allow the call to pass through
[super sendAction:action to:target forEvent:event];
//toggle the property after a delay (to make sure the event has processed)
[self performSelector:#selector(toggleHighlight:) withObject:nil afterDelay:.2];
} else {
//not an event we are interested in, allow it pass through with no additional action
[super sendAction:action to:target forEvent:event];
}
}
#end
That was a quick run at a proper solution, there is a flicker on toggle that you may not like. I am sure if you play around with some changes that can be corrected. I tried it and actually like it for your stated case.
The highlighted state of a UIButton is simply setting the button's alpha to 0.5f. So if you set the button to not change on highlight, then just toggle the alpha between 0.1 and 0.5.
For example:
- (void)buttonPressed:(id)sender {
if((((UIButton*)sender).alpha) != 1.0f){
[((UIButton*)sender) setAlpha:1.0f];
} else {
[((UIButton*)sender) setAlpha:0.5f];
}
}
Perhaps what you really want is
infoButton.enabled = NO;
This will dim the button and disable touches when set to no, allow normal operation when set to YES.
or in your case:
infoButton.enabled = !infoButton.isEnabled;
to toggle the availability of same.
If you put this in your touchupinside event, of course it will work only the first time. After that is disabled and does not receive touch events. You would put it in another method that decides whether or not the button should be enabled.
If you truly want it to change each time it is pressed then you probably should use a switch or you may look at the -imageForState, -setTitle:forState and/or -setTitleColor:forState methods. If you want to toggle the appearance each time it is touched, you could change these.

Drag from NSOutlineView never accepted

I'm trying to implement dragging from an NSOutlineView and although the drag starts OK it is never accepted by another app. The pertinent code is:
- (BOOL) outlineView:(NSOutlineView*)pOutlineView writeItems:(NSArray*)pItems toPasteboard:(NSPasteboard*)pBoard
{
CItem* theItem = [pItems objectAtIndex:0];
BOOL canDrag = ([theItem subItems] == 0);
if (canDrag) {
[pBoard clearContents];
[pBoard writeObjects:[NSArray arrayWithObject:[theItem name]]];
}
return canDrag;
}
[theItem name] returns an NSString*. At some point I'll want to add more to the pasteboard contents but until I can get it to work with a simple string there doesn't seem to be much point in getting into that.
The drag looks fine but the receiver doesn't show any highlighting when being hovered over and the drag image 'flies back' when released.
Any help gratefully received!
Rev. Andy
Turns out that draggingSourceOperationMaskForLocal: is never called for the delegate of NSOutlineView (or NSTableView possibly) so that drag operation is never allowed. Subclassing NSOutlineView just to override this method fixes everything.

iOS - Navigation Title Not Updating

What I'm doing with this bit of code is grabbing an image sequence, grabbing it's name. Then setting the view's title as the sequences name playing the sequence then setting the title back to the old title.
Problem is the title doesn't seem to be changing on the navigation bar. The NSLogs are outputting the correct values though.
I remember having this issue before and solving it with some "refresh" method.
Here is the pertinent code.
-(void)playSequence
{
if (([animatorViewController isAnimating] == FALSE) && (btnDisable == FALSE))
{
Sequence *tempSequence;
tempSequence = [fullStepList objectAtIndex:lastPlayedSequence];
self.title = [NSString stringWithFormat:#"Assembly - %#", tempSequence.subName];
NSLog(self.title);
[self startAnimator:tempSequence.imageNameScheme forNumFrames:tempSequence.numberOfFrames playInReverse:FALSE];
tempSequence = nil;
tempSequence = [fullStepList objectAtIndex:queuedSequence];
self.title = [NSString stringWithFormat:#"Assembly - %#", tempSequence.subName];
NSLog(self.title);
tempSequence = nil;
}
}
EDIT: This method of changing the title is working else where in this class. The problem seems to come in when trying to set it twice that is causing the issue.
EDIT2: It's actually running both title changes one after another... Fact that it was setting it back to previous was throwing me off.
A navigation item may contains four things: leftBarButtonItem, rightBarButtonItem, title, and titleView. When you need to change the title you should assign your title to navigationItem's title property, not to your view's title property. So it would look like:
self.navigationItem.title = [NSString stringWithFormat:#"Assembly - %#", tempSequence.subName];
self.title points to your viewControllers title which don't shows up in the navigationBar.
The title won't appear to update until after the main run loop executes. Unless -startAnimator:forNumFrames: invokes the main run loop to execute before it returns, you won't see the change.
If you are hosting the UINavigationController inside another view, you need to do this:
self.yourNavigationController.title = #"title";
instead of
self.title = #"blah"
Otherwise you are just changing the title of the view hosting the view navigator. Just a guess.
I recently found myself in a similar situation. My view's title was comprised of a label with an NSMutableAttributedString. Calling [self reloadInputViews] whenever I updated my title worked for me.
The dreaded P.E.B.K.A.C. error was the cause of this confusion. It's actually running both title changes one after another... Fact that it was setting it back to previous was throwing me off.

How do i properly discard a subview?

I have a problem with my app where the code for which is far too long to go into, but suffice to say when i'm removing a UIView and replacing it with a new one like so:
NSLog(#" .. %#", (Icon *)[self viewWithTag:index]);
Icon *icon = (Icon *)[self viewWithTag:index];
CGRect frame = icon.frame;
int tag = icon.tag;
[icon removeFromSuperview];
[icon release];
Icon *icon2 = [[Icon alloc] init];
icon2.frame = frame;
[icon2 makeIconStandardWithTag:(int)tag];
[self addSubview:icon2];
It does some weird thing where that NSLog the first time (because the view is already there) shows that the object is an icon, but the second time after running this code shows that it's a UIImageView for some reason now, and it displays what i presume to be the original icon at some odd position on the screen. It's very erratic behaviour. But what i do know is this:
Removing the [icon removeFromSuperview]; line, although keeping the object there, stops this behaviour and causes the NSLog to return an Icon, as it should.
So my guess is that it's not removing icon correctly. Is there a way to completely remove icon, or is removeFromSuperview as far as i can go. What i could do is just have it set to alpha = 0 but this is more of a patch-over solution and not how i want to solve it.
"Is there a way to completely remove
icon, or is removeFromSuperview as far
as i can go"
You can set the object to nil:
icon = nil;
Can you verify what "self" is in this line of code:
It might not be what you think.
[self addSubview:icon2];
NSLog(#" Self is %#", self);
This is a guess, but try setting self.tag to -1 or some other value that doesn't collide with the tags you're setting on your Icon objects. The viewWithTag: method searches the current view and its subviews for a match, so if self.tag == 0 and you call [self viewWithTag:0], you'll get self.
Did you retain icon somewhere prior to this? If not, no need to release it after the call to removeFromSuperview. Similarly, unless you need the reference to icon2 elsewhere, you can release that after calling addSubview.
Views retain views added via addSubview, and they release views removed via removeFromSuperview.