iOS: Sliding UIView on/off screen

mbm29414 picture mbm29414 · Jul 29, 2011 · Viewed 12.8k times · Source

I'm working on an app where having a "drawer" on the left-hand side would be very beneficial. I'm doing some initial testing to see how I would best accomplish this, and I'm having some very basic trouble.

My Setup
1. I am using a single-view application template in Xcode 4.
2. To the xib's "main/border" view, I've added 2 UIViews (LeftPanel and RightPanel) and a UIButton (ShowHideButton).
3. I've colored the LeftPanel green and the RightPanel blue for easier visibility.
4. When the view is loaded, both panels are visible and the UIButton has the text "Hide Panel".
5. Upon pressing the button, LeftPanel should slide off the screen (to the left) and RightPanel should expand to take up its original space plus the space vacated by LeftPanel.
6. At this point, ShowHideButton should change its text to "Show Panel".
7. Upon pressing the button again, LeftPanel should slide back onto the screen (from the left) and RightPanel should shrink to "give it back" its original space.
8. At this point, ShowHideButton should change its text back to "Hide Panel".

I'm implementing the animation using animateWithDuration:animations:completion:. So far, the transition OFF the screen is working fine (very well, actually).

What's troubling me is that when I then try to bring LeftPanel "back", I'm getting an EXC_BAD_ACCESS. I've posted my code below, and I've looked at it, but I really can't see what I'm accessing that's been released (or whatever is causing the EXC_BAD_ACCESS).

DrawerTestingViewController.h
#import <UIKit/UIKit.h>

typedef enum {
    kHidden,
    kShown
} PanelState;

@interface DrawerTestingViewController : UIViewController {

    PanelState   currentState;

    UIButton    *showHideButton;

    UIView      *leftPanel;
    UIView      *rightPanel;
}

@property (assign, nonatomic)          PanelState   CurrentState;

@property (strong, nonatomic) IBOutlet UIButton     *ShowHideButton;

@property (strong, nonatomic) IBOutlet UIView       *LeftPanel;
@property (strong, nonatomic) IBOutlet UIView       *RightPanel;

- (IBAction)showHidePressed:(id)sender;

@end


DrawerTestingViewController.m
#import "DrawerTestingViewController.h"

@implementation DrawerTestingViewController

@synthesize CurrentState    = currentState;
@synthesize LeftPanel       = leftPanel;
@synthesize RightPanel      = rightPanel;
@synthesize ShowHideButton  = showHideButton;

#pragma mark - My Methods

- (IBAction)showHidePressed:(id)sender
{
    switch ([self CurrentState]) {
        case kShown:
            // Hide the panel and change the button's text
            // 1. Hide the panel
            [UIView animateWithDuration:0.5 
                animations:^{
                // b. Move left panel from (0, 0, w, h) to (-w, 0, w, h)
                CGRect currLeftPanelRect = [[self LeftPanel] frame];
                currLeftPanelRect.origin.x = -1 * currLeftPanelRect.size.width;
                [[self LeftPanel] setFrame:currLeftPanelRect];
                // c. Expand right panel from (x, 0, w, h) to (0, 0, w + x, h)
                CGRect currRightPanelRect = [[self RightPanel] frame];
                currRightPanelRect.origin.x = 0;
                currRightPanelRect.size.width += currLeftPanelRect.size.width;
                [[self RightPanel] setFrame:currRightPanelRect];}
                completion:NULL];
            // 2. Change the button's text
            [[self ShowHideButton] setTitle:@"Show Panel" forState:UIControlStateNormal];
            // 3. Flip [self CurrentState]
            [self setCurrentState:kHidden];
            break;
        case kHidden:
            // Show the panel and change the button's text
            // 1. Show the panel
            [UIView animateWithDuration:0.5 
                animations:^{
                // b. Move left panel from (-w, 0, w, h) to (0, 0, w, h)
                CGRect currLeftPanelRect = [[self LeftPanel] frame];
                currLeftPanelRect.origin.x = 0;
                [[self LeftPanel] setFrame:currLeftPanelRect];
                // c. Expand right panel from (0, 0, w, h) to (leftWidth, 0, w - leftWidth, h)
                CGRect currRightPanelRect = [[self RightPanel] frame];
                currRightPanelRect.origin.x = currLeftPanelRect.size.width;
                currRightPanelRect.size.width -= currLeftPanelRect.size.width;
                [[self RightPanel] setFrame:currRightPanelRect];}
                completion:NULL];
            // 2. Change the button's text
            [[self ShowHideButton] setTitle:@"Hide Panel" forState:UIControlStateNormal];
            // 3. Flip [self CurrentState]
            [self setCurrentState:kShown];
            break;
        default:
            break;
    }
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self setCurrentState:kShown];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    switch ([self CurrentState]) {
        case kShown:
            [[self ShowHideButton] setTitle:@"Hide Panel" forState:UIControlStateNormal];
            break;
        case kHidden:
            [[self ShowHideButton] setTitle:@"Show Panel" forState:UIControlStateNormal];
            break;
        default:
            break;
    }
}

@end

Am I missing something super-basic? Can anybody help?

Thanks!

Edit: I've tried 2 more things:
1. The problem seems to be related to bringing the off-screen view on-screen, as starting with LeftPanel off-screen gives me the same problem.
2. Stepping through the code reliably causes Xcode (4 Beta for Lion) to crash. Here are the details (same for every crash):

ASSERTION FAILURE in /SourceCache/DVTFoundation/DVTFoundation-867/Framework/Classes/FilePaths/DVTFilePath.m:373 Details: empty string is not a valid path Object: Method: +_filePathForParent:fileSystemRepresentation:length:allowCreation: Thread: {name = (null), num = 55} Hints: None Backtrace: 0 0x00000001068719a6 -[IDEAssertionHandler handleFailureInMethod:object:fileName:lineNumber:messageFormat:arguments:] (in IDEKit) 1 0x0000000105f3e324 _DVTAssertionFailureHandler (in DVTFoundation) 2 0x0000000105edd16f +[DVTFilePath _filePathForParent:fileSystemRepresentation:length:allowCreation:] (in DVTFoundation) 3 0x0000000105edcd4d +[DVTFilePath _filePathForParent:pathString:] (in DVTFoundation) 4 0x0000000105ede141 +[DVTFilePath filePathForPathString:] (in DVTFoundation) 5 0x00000001064a8dde -[IDEIndex queryProviderForFile:highPriority:] (in IDEFoundation) 6 0x000000010655193b -[IDEIndex(IDEIndexQueries) symbolsMatchingName:inContext:withCurrentFileContentDictionary:] (in IDEFoundation) 7 0x000000010aca6166 __68-[IDESourceCodeEditor symbolsForExpression:inQueue:completionBlock:]_block_invoke_01561 (in IDESourceEditor) 8 0x00007fff93fb490a _dispatch_call_block_and_release (in libdispatch.dylib) 9 0x00007fff93fb615a _dispatch_queue_drain (in libdispatch.dylib) 10 0x00007fff93fb5fb6 _dispatch_queue_invoke (in libdispatch.dylib) 11 0x00007fff93fb57b0 _dispatch_worker_thread2 (in libdispatch.dylib) 12 0x00007fff8bb5e3da _pthread_wqthread (in libsystem_c.dylib) 13 0x00007fff8bb5fb85 start_wqthread (in libsystem_c.dylib)

Update: Screen Shots
Panel Shown (startup state) Panel Shown
Panel Hidden (successful transition after button press) Panel Hidden
Error: Pressing button again causes failure Error


Answer

mbm29414 picture mbm29414 · Jul 29, 2011

After playing around with this a bunch and banging my head against the wall, I finally came to the conclusion that my code wasn't wrong unless I misunderstood something about blocks. Stepping through my code in debug yielded answers exactly like I expected, but between the end of the animation block and the beginning of the completion block, an EXC_BAD_ACCESS kept popping up.

Anyway, I'm not sure where I got the idea, but I figured I might want to try doing my mathematical calculations (for changing the frames) outside of my animation block.

Guess what? IT WORKED!

So, without further ado, here's the working code for what I wanted:

- (IBAction)showHidePressed:(id)sender
{
    switch ([self CurrentState]) {
        case kShown:
        {
            // Hide the panel and change the button's text
            CGRect currLeftPanelRect = [[self LeftPanel] frame];
            currLeftPanelRect.origin.x -= currLeftPanelRect.size.width / 2;
            CGRect currRightPanelRect = [[self RightPanel] frame];
            currRightPanelRect.origin.x = 0;
            currRightPanelRect.size.width += currLeftPanelRect.size.width;
            // 1. Hide the panel
            [UIView animateWithDuration:0.5 
              animations:^{
              // b. Move left panel from (0, 0, w, h) to (-w, 0, w, h)
              [[self LeftPanel] setFrame:currLeftPanelRect];
              // c. Expand right panel from (x, 0, w, h) to (0, 0, w + x, h)
              [[self RightPanel] setFrame:currRightPanelRect];
              }
              completion:^(BOOL finished){ if(finished) {
              [[self ShowHideButton] setTitle:@"Show Panel" forState:UIControlStateNormal];
              [self setCurrentState:kHidden];
              }
              }];
        }            
            break;
        case kHidden:
        {
            // Show the panel and change the button's text
            // 1. Show the panel
            [UIView animateWithDuration:0.5 
              animations:^{
              // b. Move left panel from (-w, 0, w, h) to (0, 0, w, h)
              CGRect currLeftPanelRect = [[self LeftPanel] frame];
              currLeftPanelRect.origin.x += currLeftPanelRect.size.width / 2;
              [[self LeftPanel] setFrame:currLeftPanelRect];
              // c. Expand right panel from (0, 0, w, h) to (leftWidth, 0, w - leftWidth, h)
              CGRect currRightPanelRect = [[self RightPanel] frame];
              currRightPanelRect.origin.x = currLeftPanelRect.size.width;
              currRightPanelRect.size.width -= currLeftPanelRect.size.width;
              [[self RightPanel] setFrame:currRightPanelRect];
              }
              completion:^(BOOL finished){ if(finished) {
              [[self ShowHideButton] setTitle:@"Hide Panel" forState:UIControlStateNormal];
              [self setCurrentState:kShown];
              }
              }];
        }
            break;
        default:
            break;
    }
}