Programmatically toggle resize of NSSplitViewItems - objective-c

I am trying to find a way to programmatically resize NSSplitViewItems.
Say the NSWindow frame size is 500x500px.
masterViewItem has a width of 100px, while subViewItem has a width of 400px.
When FOO, I want masterViewItem to collapse to 0px (towards the left),
while subViewItem expands to fill the entire window (500px).
When BAR, I want masterViewItem to expand back to 100px, while subViewItem collapses back to the original 400px.
- (IBAction) onTapOnSomeButton:(NSButton *)sender {
NSWindow *window = [[NSApplication sharedApplication] mainWindow];
NSSplitViewController *splitViewController = (NSSplitViewController *)[window contentViewController];
NSSplitViewItem *masterViewItem =[splitViewController.splitViewItems firstObject];
NSSplitViewItem *subViewItem = [splitViewController.splitViewItems lastObject];
if (FOO) {
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
// Expand subViewItem
} completionHandler:^{
}];
} else if (BAR) {
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
// Expand masterViewItem while shrinking subViewItem
} completionHandler:^{
}];
}
}
I am able to animate each item so that it seems to collapse/expand,
i.e.
subViewItem.viewController.view.animator.frame = CGRectMake(0, 0, window.frame.size.width, subViewItem.viewController.view.frame.size.height);
but the divider will not move, making the two items just move individually, and does not look like the entire screen is collapsing/expanding.
How would I implement this? Thanks in advance.
UPDATE
So I have done a bit more research, and found this:
How to animate the NSSplitView while resizing?
So I made code like this:
- (IBAction) onTapOnSomeButton:(NSButton *)sender {
NSWindow *window = [[NSApplication sharedApplication] mainWindow];
NSSplitViewController *splitViewController = (NSSplitViewController *)[window contentViewController];
NSSplitViewItem *masterViewItem =[splitViewController.splitViewItems firstObject];
NSSplitViewItem *subViewItem = [splitViewController.splitViewItems lastObject];
if (FOO) {
NSMutableDictionary *collapseMainAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
[collapseMainAnimationDict setObject: subView forKey:NSViewAnimationTargetKey];
NSRect newRightSubViewFrame = subView.frame;
newRightSubViewFrame.size.width = splitViewController.splitView.frame.size.width;
[collapseMainAnimationDict setObject:[NSValue valueWithRect:newRightSubViewFrame] forKey:NSViewAnimationEndFrameKey];
NSMutableDictionary *collapseInspectorAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
[collapseInspectorAnimationDict setObject: masterView forKey:NSViewAnimationTargetKey];
NSRect newLeftSubViewFrame = masterView.frame;
newLeftSubViewFrame.size.width = 0.0f;
newLeftSubViewFrame.origin.x = splitViewController.splitView.frame.size.width;
[collapseInspectorAnimationDict setObject:[NSValue valueWithRect:newLeftSubViewFrame] forKey:NSViewAnimationEndFrameKey];
NSViewAnimation *collapseAnimation = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObjects: collapseMainAnimationDict, collapseInspectorAnimationDict, nil]];
[collapseAnimation setDuration:0.3f];
[collapseAnimation startAnimation];
[splitViewController.splitView adjustSubviews];
[splitViewController.splitView setNeedsDisplay: YES];
} else if (BAR) {
// Not yet implemented
}
}
But to no effect. Any suggestions?

You have to resize the panes using [NSSplitView setPosition:ofDividerAtIndex:].
I have no idea what NSSplitViewController is, but presumably you can get access to the NSSplitView via its view property, or some such.

Rather than looking at it as a resize, it sounds like you're simply trying to collapse/uncollapse the master view item. You can just use NSSplitViewItem's collapsed property with its animator proxy to trigger the collapse:
if (FOO) {
// Expand subViewItem (collapse the master view item)
masterViewItem.animator.collapsed = YES;
And uncollapse:
} else if (BAR) {
// Expand masterViewItem while shrinking subViewItem
masterViewItem.animator.collapsed = NO;
}
}
With 10.11, there's API to designate your master view item as a "sidebar", which has additional collapse/uncollapse behavior for that item (such as overlays).

Related

Disabling NSView fade animation for NSView `setHidden:`

I am working on a project that has the concept of draggable controls, everything is working fine except that NSView seems to employ a fade in/out animation when calling setHidden:.
I have been able to work around the problem by changing the line session.animatesToStartingPositionsOnCancelOrFail = YES; to NO and implementing the image snapback myself with a custom animated NSWindow subclass. it looks great, but I know there must be an easier way.
I have tried:
using NSAnimationContext grouping with duration of 0 around the setHidden: calls
setting the view animations dictionary using various keys (alpha, hidden, isHidden) on the control and superview
overriding animationForKey: for both the control and its superview
I am not using CALayers and have even tried explicitly setting wantsLayer: to NO.
Does anybody know how to either disable this animation, or have a simpler solution then my animated NSWindow?
here is my stripped down altered code with the bare minimum to see what I'm talking about.
#implementation NSControl (DragControl)
- (NSDraggingSession*)beginDraggingSessionWithDraggingCell:(NSActionCell <NSDraggingSource> *)cell event:(NSEvent*) theEvent
{
NSImage* image = [self imageForCell:cell];
NSDraggingItem* di = [[NSDraggingItem alloc] initWithPasteboardWriter:image];
NSRect dragFrame = [self frameForCell:cell];
dragFrame.size = image.size;
[di setDraggingFrame:dragFrame contents:image];
NSArray* items = [NSArray arrayWithObject:di];
[self setHidden:YES];
return [self beginDraggingSessionWithItems:items event:theEvent source:cell];
}
- (NSRect)frameForCell:(NSCell*)cell
{
// override in multi-cell cubclasses!
return self.bounds;
}
- (NSImage*)imageForCell:(NSCell*)cell
{
return [self imageForCell:cell highlighted:[cell isHighlighted]];
}
- (NSImage*)imageForCell:(NSCell*)cell highlighted:(BOOL) highlight
{
// override in multicell cubclasses to just get an image of the dragged cell.
// for any single cell control we can just make sure that cell is the controls cell
if (cell == self.cell || cell == nil) { // nil signifies entire control
// basically a bitmap of the control
// NOTE: the cell is irrelevant when dealing with a single cell control
BOOL isHighlighted = [cell isHighlighted];
[cell setHighlighted:highlight];
NSRect cellFrame = [self frameForCell:cell];
// We COULD just draw the cell, to an NSImage, but button cells draw their content
// in a special way that would complicate that implementation (ex text alignment).
// subclasses that have multiple cells may wish to override this to only draw the cell
NSBitmapImageRep* rep = [self bitmapImageRepForCachingDisplayInRect:cellFrame];
NSImage* image = [[NSImage alloc] initWithSize:rep.size];
[self cacheDisplayInRect:cellFrame toBitmapImageRep:rep];
[image addRepresentation:rep];
// reset the original cell state
[cell setHighlighted:isHighlighted];
return image;
}
// cell doesnt belong to this control!
return nil;
}
#pragma mark NSDraggingDestination
- (void)draggingEnded:(id < NSDraggingInfo >)sender
{
[self setHidden:NO];
}
#end
#implementation NSActionCell (DragCell)
- (void)setControlView:(NSView *)view
{
// this is a bit of a hack, but the easiest way to make the control dragging work.
// force the control to accept image drags.
// the control will forward us the drag destination events via our DragControl category
[view registerForDraggedTypes:[NSImage imagePasteboardTypes]];
[super setControlView:view];
}
- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp
{
BOOL result = NO;
NSPoint currentPoint = theEvent.locationInWindow;
BOOL done = NO;
BOOL trackContinously = [self startTrackingAt:currentPoint inView:controlView];
BOOL mouseIsUp = NO;
NSEvent *event = nil;
while (!done)
{
NSPoint lastPoint = currentPoint;
event = [NSApp nextEventMatchingMask:(NSLeftMouseUpMask|NSLeftMouseDraggedMask)
untilDate:[NSDate distantFuture]
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
if (event)
{
currentPoint = event.locationInWindow;
// Send continueTracking.../stopTracking...
if (trackContinously)
{
if (![self continueTracking:lastPoint
at:currentPoint
inView:controlView])
{
done = YES;
[self stopTracking:lastPoint
at:currentPoint
inView:controlView
mouseIsUp:mouseIsUp];
}
if (self.isContinuous)
{
[NSApp sendAction:self.action
to:self.target
from:controlView];
}
}
mouseIsUp = (event.type == NSLeftMouseUp);
done = done || mouseIsUp;
if (untilMouseUp)
{
result = mouseIsUp;
} else {
// Check if the mouse left our cell rect
result = NSPointInRect([controlView
convertPoint:currentPoint
fromView:nil], cellFrame);
if (!result)
done = YES;
}
if (done && result && ![self isContinuous])
[NSApp sendAction:self.action
to:self.target
from:controlView];
else {
done = YES;
result = YES;
// this bit-o-magic executes on either a drag event or immidiately following timer expiration
// this initiates the control drag event using NSDragging protocols
NSControl* cv = (NSControl*)self.controlView;
NSDraggingSession* session = [cv beginDraggingSessionWithDraggingCell:self
event:theEvent];
// Note that you will get an ugly flash effect when the image returns if this is set to yes
// you can work around it by setting NO and faking the release by animating an NSWindowSubclass with the image as the content
// create the window in the drag ended method for NSDragOperationNone
// there is [probably a better and easier way around this behavior by playing with view animation properties.
session.animatesToStartingPositionsOnCancelOrFail = YES;
}
}
}
return result;
}
#pragma mark - NSDraggingSource Methods
- (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
switch(context) {
case NSDraggingContextOutsideApplication:
return NSDragOperationNone;
break;
case NSDraggingContextWithinApplication:
default:
return NSDragOperationPrivate;
break;
}
}
- (void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation
{
// now tell the control view the drag ended so it can do any cleanup it needs
// this is somewhat hackish
[self.controlView draggingEnded:nil];
}
#end
There must be a layer enabled somewhere in your view hierarchy, otherwise there wouldn't be a fade animation. Here is my way of disabling such animations:
#interface NoAnimationImageView : NSImageView
#end
#implementation NoAnimationImageView
+ (id)defaultAnimationForKey: (NSString *)key
{
return nil;
}
#end
The solution you already tried by setting the view animations dictionary should work. But not for the keys you mention but for the following. Use it somewhere before the animation is triggered the first time. If you have to do it on the window or view or both, I don't know.
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderIn];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Or also just remove the keys if they are there (might not be the case as they are implicit / default):
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations removeObjectForKey:NSAnimationTriggerOrderIn];
[animations removeObjectForKey:NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Ok. I figured out that the animation I'm seeing is not the control, the superview, nor the control's window. It appears that animatesToStartingPositionsOnCancelOrFail causes NSDraggingSession to create a window (observed with QuartzDebug) and put the drag image in it and it is this window that animates back to the origin and fades out before the setHidden: call is executed (i.e. before the drag operation is concluded).
Unfortunately, the window that it creates is not an NSWindow so creating a category on NSWindow doesn't disable the fade animation.
Secondly, there is no public way that I know of to get a handle on the window, so I can't attempt directly manipulating the window instance.
It looks like maybe my workaround is the best way to do this, after all its not far from what AppKit does for you anyway.
If anybody knows how to get a handle on this window, or what class it is I would be interested to know.

How to reload a custom NSTableRowView in NSOutlineView?

I'm trying to modify the appearance of a custom NSTableRowView used in my NSOutlineView when the row expands/collapses. My NSOutlineViewDelegate includes:
- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
{
...
return rowView;
}
- (void)itemDidExpand:(NSNotification *)notification
{
NSDictionary *userInfo = [notification userInfo];
id item = [userInfo objectForKey:#"NSObject"];
NSOutlineView *outlineView = [notification object];
[outlineView reloadItem:item];
}
This unfortunately doesn't reload the custom row view.
Reloading data for the entire NSOutlineView works:
- (void)itemDidExpand:(NSNotification *)notification
{
[outlineView reloadData];
}
But with large amounts of data this can be time consuming, and often leads to a spinning beach ball.
Is it possible to reload the custom row view for just a single top-level item?
Try the following method:
-(void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row {
aColor = [NSColor colorWithCalibratedRed:(r/255.0f) green:(g/255.0f) blue:(b/255.0f) alpha:1.0];
[view setBackgroundColor:aColor];
}
You can advance this further to get the row object, and then determine which color you want for a particular row.
-Edit-
If you wish to alter the parent cell view to show that it has been expanded you have implemented the wrong method for the NSOutlineview. Implement the method like the following:
-(void)outlineViewItemWillExpand:(NSNotification *)notification {
id theObject = [[notification userInfo] objectForKey:#"NSObject"];
NSInteger row = [theOutlineView rowForItem:theObject];
NSTableRowView *theRowView = [theOutlineView rowViewAtRow:row makeIfNecessary:NO];
int r = 110;
int g = 110;
int b = 110;
NSColor *aColor = [NSColor colorWithCalibratedRed:(r/255.0f) green:(g/255.0f) blue:(b/255.0f) alpha:1.0];
[theRowView setBackgroundColor:aColor];
}
Try this in your rowViewForItem method.
It reloads and redisplays the data for item.
- (void)reloadItem:(id)item;

UICollectionView Views Layer Priority

I'm seeing a very odd behavior with UICollectionViews.
Here is the scenario.
I have a UIViewController that has been pushed on to a UINavigationController stack.
The UIViewController view has nav bar and UICollectionView in grid layout. 3 cells wide by unlimited tall.
Just below extent of screen, I also have a UIToolbar hidden. The UIToolbar is on top of UICollectionView in layer hierarchy.
I then allow the user to put view in to "edit mode" and I animate UIToolbar on to the screen and covers bottom portion of UICollectionView. If user leaves "edit mode" I move UIToolbar back off screen.
While in "edit mode" I allow the user to multi select cells with check boxes that appear and uitoolbar has delete button.
Delete does the following:
- (void)deletePhotos:(id)sender {
if ([[self.selectedCells allKeys] count] > 0) {
[[DataManager instance] deletePhotosAtIndexes:[self.selectedCells allKeys]];
[self.selectedCells removeAllObjects];
[self.collectionview reloadData];
[self.collectionview performBatchUpdates:nil completion:nil];
}
}
// Data Manager method in singleton class:
- (void)deletePhotosAtIndexes:(NSArray *)indexes {
NSMutableIndexSet *indexesToDelete = [NSMutableIndexSet indexSet];
for (int i = 0; i < [indexes count]; i++) {
[indexesToDelete addIndex:[[indexes objectAtIndex:i] integerValue]];
NSString *filePath = [self.photosPath stringByAppendingPathComponent:[self.currentPhotos objectAtIndex:[[indexes objectAtIndex:i] integerValue]]];
NSString *thumbnailPath = [self.thumbPath stringByAppendingPathComponent:[self.currentPhotos objectAtIndex:[[indexes objectAtIndex:i] integerValue]]];
if ([[NSFileManager defaultManager] fileExistsAtPath: filePath]) {
[[NSFileManager defaultManager] removeItemAtPath: filePath error:NULL];
[[NSFileManager defaultManager] removeItemAtPath: thumbnailPath error:NULL];
}
}
[self.currentPhotos removeObjectsAtIndexes:indexesToDelete];
}
The data manager contains photo objects and are used in cell creation like so.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
ImageCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"imageCell" forIndexPath:indexPath];
NSString *pngfile = [[[DataManager instance] thumbPath] stringByAppendingPathComponent:[[[DataManager instance] currentPhotos] objectAtIndex:indexPath.row]];
if ([[NSFileManager defaultManager] fileExistsAtPath:pngfile]) {
NSData *imageData = [NSData dataWithContentsOfFile:pngfile];
UIImage *img = [UIImage imageWithData:imageData];
[cell.imageView setImage:img];
}
if ([self.selectedCells objectForKey:[NSString stringWithFormat:#"%d", indexPath.row]] != nil) {
cell.checkbox.hidden = NO;
} else {
cell.checkbox.hidden = YES;
}
return cell;
}
So here is where I'm finding issues:
When deleting enough cells so that number of visible rows changes, UIToolbar is disappearing. In case of a full row of 3, if I only delete 1 or 2 items, the UIToolbar doesn't disappear. I am not doing any animation on the UIToolbar in delete method and only when hitting a Done button that ends edit mode. I've confirmed that this method isn't being called.
I've also confirmed that the UIToolbar isn't actually moving. If I add "self.collectionview removeFromSuperView" on hitting delete in cases where UIToolbar would normally disappear, the UIToolbar is exactly where expected on the screen. This gives me the impression the UICollectionView is somehow changing layer hierarchy in draw of parent view.
I've attempted trying to bringSubviewToFront for UIToolbar and sendSubviewToBack for collectionview and has no affect.
Re-iniating open toolbar causes uitoolbar to animate back in. Oddly, however, it seems to animate from below screen! This makes no sense unless the UICollectionView is somehow pushing the UIToolbar off the screen due after the point where I would be calling the removeFromSuperview call so I can't re-create.
One "solution" I have is to force the UIToolbar to come back in to position but without animation after a 0.01 second delay
[self performSelector:#selector(showToolbarNoAnimation) withObject:nil afterDelay:0.01];
This works.
Here is the question:
Any idea why UICollectionView causing this behavior to push UIToolbar offscreen after a full row is deleted? The hack works but doesn't explain the issue.
Thanks,
James
When you use auto layout, and your views are loaded from a storyboard (or xib), you can't set the frames of your views. Doing so may seem to work initially, but at some point auto layout will reset the view's frame based on the constraints, and you won't understand what happened, and then you'll post a question to stack overflow.
If you need to change the layout of a view, you need to update the view's constraints instead.
There is a constraint specifying the distance between the bottom edge of your toolbar and the bottom edge of its superview. Presumably that distance is -44 (where 44 is the height of the toolbar).
You need to connect that constraint to an outlet in your view controller. The outlet will have type NSLayoutConstraint *. Call it toolbarBottomEdgeConstraint.
When you want to animate the toolbar onto the screen, set constraint's constant to zero and call layoutIfNeeded in an animation block:
- (void)showToolbarAnimated {
[UIView animateWithDuration:0.25 animations:^{
self.toolbarBottomEdgeConstraint.constant = 0;
[self.toolbar layoutIfNeeded];
}];
}
To hide the toolbar, set the constraint's constant back to its original value:
- (void)hideToolbarAnimated {
[UIView animateWithDuration:0.25 animations:^{
self.toolbarBottomEdgeConstraint.constant = -toolbar.bounds.size.height;
[self.toolbar layoutIfNeeded];
}];
}

How to commit a UIButton state change before starting other UIView animations?

I have a UIButton which fires a target action causing one view to transition to another on touchupinside.
When this happens the I also change the button's state to selected but the (different) background image for the selected state doesn't actually draw until the UIView transition is complete (or sometimes it also seems to transition with the UIView transition.)
How can I force the button to redraw the new background image before starting the view transition?
The code that runs on UIControlEventTouchUpInside:
RhSubTab *activeTab;
for(RhSubTab *tab in self.tabViews.copy) {
tab.selected = NO;
[self.view bringSubviewToFront:tab];
if(tab.tag == type) activeTab = tab;
}
[self.view bringSubviewToFront:activeTab];
[activeTab setSelected:YES];
UIView *oldView;
UIView *newView;
for(UIView *view in self.contentViews.copy) {
if([self.view.subviews containsObject:view]) {
oldView = view;
}
if(view.tag == type) {
newView = view;
}
}
if(!oldView) [self.view addSubview:newView];
else [UIView transitionFromView:oldView toView:newView duration:0.5 options:(UIViewAnimationOptionTransitionCrossDissolve|UIViewAnimationCurveEaseInOut) completion:nil];
And the custom (RhSubTab) button class:
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if(self) {
[self setBackgroundImage:[[UIImage imageNamed:#"SubContentTabBg.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 9, 0, 40)] forState:UIControlStateNormal];
[self setBackgroundImage:[[UIImage imageNamed:#"SubContentTabBg-selected.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 9, 0, 40)] forState:UIControlStateSelected];
}
return self;
}
Try breaking out the second part of your method (after setting the button to the selected state) into a second method.
Then use performSelector:withObject:afterDelay: to call that second method
So your method might look like this:
RhSubTab *activeTab;
for(RhSubTab *tab in self.tabViews.copy) {
tab.selected = NO;
[self.view bringSubviewToFront:tab];
if(tab.tag == type) activeTab = tab;
}
[self.view bringSubviewToFront:activeTab];
[activeTab setSelected:YES];
[self performSelector: #selector(doTransition) withObject: nil afterDelay: 0];
And your second method might look like this:
- (void) doTransition;
{
UIView *oldView;
UIView *newView;
for(UIView *view in self.contentViews.copy) {
if([self.view.subviews containsObject:view]) {
oldView = view;
}
if(view.tag == type) {
newView = view;
}
}
if(!oldView) [self.view addSubview:newView];
else [UIView transitionFromView: oldView
toView:newView duration:0.5
options: (UIViewAnimationOptionTransitionCrossDissolve|UIViewAnimationCurveEaseInOut)
completion:nil];
}
Even with a delay value of 0, the performSelector:withObject:afterDelay: returns immediately, and doesn't trigger the specified method until the next path through the event loop, after checking for user events, processing screen updates, etc.
If you want the transition to wait until the button change is complete, you might want to add a small non-zero delay value.
The method performSelector:withObject:afterDelay: will let you pass 0 or 1 objects as parameters. At a glance, it did not look like the second half of your transition method needed any parameters.
Try using UIButton control state highlighted instead along with your selected button.

detect long press on UINavigationItem's back button

I want to add functionality to my back buttons through my UINavigationController-based app where long-pressing the back button will pop to root. However, I can't figure out where to attach the gesture recognizer. Do I subclass UINavigationBar and try and detect if the long press is in the left button region?
I've heard of people adding similar functionality before. Anyone have any ideas?
I know this question is old, but I came up with a solution. Instead of trying to add the gesture recognizer to the button itself (which would be ideal), I added it to the self.navigationController.navigationBar and then in the action method, use the locationInView to see if I'm over the back button. I wasn't entirely sure about how to identify the back button precisely, so I'm clumsily just grabbing the the first subview with an x coordinate less than some arbitrary value, but it seems promising. If someone has a better way to identify the frame of the back button, let me know.
- (void)longPress:(UILongPressGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
// set a default rectangle in case we don't find the back button for some reason
CGRect rect = CGRectMake(0, 0, 100, 40);
// iterate through the subviews looking for something that looks like it might be the right location to be the back button
for (UIView *subview in self.navigationController.navigationBar.subviews)
{
if (subview.frame.origin.x < 30)
{
rect = subview.frame;
break;
}
}
// ok, let's get the point of the long press
CGPoint longPressPoint = [sender locationInView:self.navigationController.navigationBar];
// if the long press point in the rectangle then do whatever
if (CGRectContainsPoint(rect, longPressPoint))
[self doWhatever];
}
}
- (void)addLongPressGesture
{
if (NSClassFromString(#"UILongPressGestureRecognizer"))
{
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
[self.navigationController.navigationBar addGestureRecognizer:longPress];
[longPress release];
}
}
I believe UIGestureRecognizers can only be added to UIViews and subclasses of UIViews.
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIView_Class/UIView/UIView.html
The back button is a UIBarButtonItem that descends from NSObject. Therefore, you won't be able to attach a gesture recognizer to a standard back button using
UILongPressGestureRecognizer *longPressGesture =
[[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(longPress:)] autorelease];
[self.navigationItem.backBarButtonItem addGestureRecognizer:longPressGesture];
You can however add a custom view to a UIBarButtonItem. A custom view could just as easily be a UIView, UIButton, UILabel, etc.
Example:
UIView *myTransparentGestureView = [[UIView alloc] initWithFrame:CGRectMake(0,0,40,30)];
[myTransparentGestureView addGestureRecognizer:longPressGesture];
[self.navigationItem.backBarButtonItem setCustomView:myTransparentGestureView];
// Or you could set it like this
// self.navigationItem.backBarButtonItem.customView = myTransparentGestureView;
[myTransparentGestureView release];
You have to be careful however, since setting properties on backBarButtonItem applies to the next view that you push. So if you have view A that pushes to view B and you want the gesture to be recognized when you tap back in view B. You must set it up in view A.
I followed a slightly different path, figured I'd share it. The above answers are fine, but really, if the long press is in the leading 1/3 of the nav bar, that's good enough for me:
- (void)longPress:(UILongPressGestureRecognizer *)gr
{
NSLog(#"longPress:");
UINavigationBar *navBar = [self navigationBar];
CGFloat height = navBar.bounds.size.height;
CGPoint pt = [gr locationOfTouch:0 inView:navBar];
//NSLog(#"PT=%# height=%f", NSStringFromCGPoint(pt), height);
if(CGRectContainsPoint(CGRectMake(0,0,100,height), pt)) {
[self popToViewController:self.viewControllers[0] animated:YES];
}
}
Here's my solution:
In appDelegate (the "owner" of the nav bar in my app), In applicationDidFinishLaunchingWithOptions:
Get the nav bar view and add the gesture recognizer to the whole view:
// Get the nav bar view
UINavigationBar *myNavBar = nil;
for (UIView *view in [self.window.rootViewController.view subviews]) {
if ([view isKindOfClass:[UINavigationBar class]]) {
NSLog(#"Found Nav Bar!!!");
myNavBar = (UINavigationBar *)view;
}
}
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(backButtonLongPress:)];
[myNavBar addGestureRecognizer:longPress];
NSLog(#"Gesture Recognizer Added.");
Then in appDelegate, in -(void) backButtonLongPress:(id) sender
Check to see if the gesture occurs within the frame of the back button:
if ([sender state] == UIGestureRecognizerStateBegan) {
// Get the nav bar view
UINavigationBar *myNavBar = nil;
for (UIView *view in [self.window.rootViewController.view subviews]) {
if ([view isKindOfClass:[UINavigationBar class]]) {
NSLog(#"Found Nav Bar!!!");
myNavBar = (UINavigationBar *)view;
}
}
// Get the back button view
UIView *backButtonView = nil;
for (UIView *view in [myNavBar subviews]) {
if ([[[view class] description] isEqualToString:#"UINavigationItemButtonView"]) {
backButtonView = view;
NSLog(#"Found It: %#", backButtonView);
NSLog(#"Back Button View Frame: %f, %f; %f, %f", backButtonView.frame.origin.x, backButtonView.frame.origin.y, backButtonView.frame.size.width, backButtonView.frame.size.height);
}
}
CGPoint longPressPoint = [sender locationInView:myNavBar];
NSLog(#"Touch is in back button: %#", CGRectContainsPoint(backButtonView.frame, longPressPoint) ? #"YES" : #"NO");
if (CGRectContainsPoint(backButtonView.frame, longPressPoint)) {
// Place your action here
}
// Do nothing if outside the back button frame
}