One option is to replace the standard camera controls with a custom interface, but that’s a whole lot of work if you just want to prevent the user from taking a photo with the front camera. Fortunately there’s another option: put a transparent button over the “switch camera” button, which will intercept touch events and show an alert dialog. It sounds simple, but as you’ll see there are a few tricks to actually getting this to work.
We’re going to use UIImagePickerController’s cameraOverlayView property to add our button to the view hierarchy. We can’t simply provide a UIButton object though. In the latest iOS SDK, cameraOverlayView is automatically resized to fill the entire screen, while we only want to cover a small corner of it. Instead, we’re going to put the button inside a UIView subclass that will be used for layout and a few other tasks. Go ahead and create this UIView subclass in your project, call it ELCCameraOverlayView, and delete any methods the Xcode template includes by default.
We’ll need a button, so let’s start by giving our new subclass a UIButton instance variable named _button with a corresponding button property. Declare this as you’d declare any synthesized property, but let’s override the default setter method to also add it to the view hierarchy.
- (void)setButton:(UIButton *)button; { if ( _button != button ) { [_button removeFromSuperview]; [_button release]; _button = [button retain]; if ( button != nil ) [self addSubview:button]; } }
- (id)initWithFrame:(CGRect)frame; { if ( ( self = [super initWithFrame:frame] ) ) { _button = [[UIButton alloc] initWithFrame:CGRectMake( 240.0f, 0.0f, 80.0f, 80.0f )]; [self addSubview:_button]; } return self; }
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; { if ( [super hitTest:point withEvent:event] == self.button ) return self.button; return nil; } - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; { if ( CGRectContainsPoint( self.button.frame, point ) ) return YES; return NO; }
This is a tricky problem. Unlike UIViewController, our UIView subclass does not have any way of telling when the interface orientation is changed. This doesn’t even matter though, since UIImagePickerController doesn’t actually change its interface orientation, it simply re-arranges the camera buttons while remaining in UIInterfaceOrientationPortrait!
The way I’ve solved this problem is to use the accelerometer to determine the device orientation. It might sound complicated, but it’s actually not a lot of work. Start by adding a new instance variable _interfaceOrientation and property interfaceOrientation to your class, of type UIInterfaceOrientation. We’ll also override its setter method to call setNeedsLayout whenever its changed.
- (void)setInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation; { if ( _interfaceOrientation != interfaceOrientation ) { _interfaceOrientation = interfaceOrientation; [self setNeedsLayout]; } }
- (void)layoutSubviews; { CGFloat width = CGRectGetWidth( self.button.frame ); CGFloat height = CGRectGetHeight( self.button.frame ); switch ( self.interfaceOrientation ) { case UIInterfaceOrientationPortrait: case UIInterfaceOrientationPortraitUpsideDown: self.button.frame = CGRectMake( CGRectGetMaxX( self.bounds ) - width, 0.0f, width, height ); break; case UIInterfaceOrientationLandscapeRight: self.button.frame = CGRectMake( CGRectGetMaxX( self.bounds ) - width, CGRectGetMaxY( self.bounds ) - height - 50.0f, width, height ); break; case UIInterfaceOrientationLandscapeLeft: self.button.frame = CGRectMake( 0.0f, 0.0f, width, height ); break; } } - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration; { CGFloat x = -[acceleration x]; CGFloat y = [acceleration y]; CGFloat angle = atan2(y, x); if ( angle >= -2.25f && angle <= -0.25f ) { self.interfaceOrientation = UIInterfaceOrientationPortrait; } else if ( angle >= -1.75f && angle <= 0.75f ) { self.interfaceOrientation = UIInterfaceOrientationLandscapeRight; } else if( angle >= 0.75f && angle <= 2.25f ) { self.interfaceOrientation = UIInterfaceOrientationPortraitUpsideDown; } else if ( angle <= -2.25f || angle >= 2.25f ) { self.interfaceOrientation = UIInterfaceOrientationLandscapeLeft; } }
- (id)initWithFrame:(CGRect)frame; { if ( ( self = [super initWithFrame:frame] ) ) { _interfaceOrientation = UIInterfaceOrientationPortrait; _button = [[UIButton alloc] initWithFrame:CGRectMake( 240.0f, 0.0f, 80.0f, 80.0f )]; [[UIAccelerometer sharedAccelerometer] setDelegate:self]; [self addSubview:_button]; } return self; } - (void)dealloc; { [[UIAccelerometer sharedAccelerometer] setDelegate:nil]; [_button release]; [super dealloc]; }
UIImagePickerController *controller = [[UIImagePickerController alloc] init]; ELCCameraOverlayView *view = [[ELCCameraOverlayView alloc] initWithFrame:controller.view.frame]; [view.button addTarget:self action:@selector(selectFrontCamera:) forControlEvents:UIControlEventTouchUpInside]; controller.sourceType = UIImagePickerControllerSourceTypeCamera; controller.cameraCaptureMode = UIImagePickerControllerCameraCaptureModePhoto; controller.cameraDevice = UIImagePickerControllerCameraDeviceRear; controller.delegate = self; controller.cameraOverlayView = view; [self.navigationController presentModalViewController:controller animated:YES]; [controller release]; [view release];
- (void)selectFrontCamera:(id)sender; { NSString *title = NSLocalizedString( @"Front Camera Disabled", @"" ); NSString *message = NSLocalizedString( @"The front camera does not have sufficient resolution.", @"" ); NSString *button = NSLocalizedString( @"Okay", @"" ); UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:button otherButtonTitles:nil]; [alert show]; [alert release]; }