The iPhone 4′s front camera is limited to 640×480 resolution. Although handy for video conferencing, for some apps that’s to small to yield a usable photo. Unfortunately the UIImagePickerController class does not have an option to restrict the user from using the front camera. Although you can check the size of the photo after the user is finished, it’s not great user experience to reject it after they go through the entire process of taking a photo.
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.
You might create the button in another class (such as a view controller) and assign it to our custom class, but you can also create a standard button in your initWithFrame: method. Note that we still have to call addSubview: here as we’re not using the property accessor method (which is discouraged in init methods).
With the button in place, let’s move on to intercepting touch events which would normally go to the “switch cameras” button. We can do this by overriding the UIView methods which are called to determine if an event occurred within a view’s frame or not. Just check to see if the event took place within the button’s frame rectangle.
It may seem like we’re almost done at this point. If you set the camera overlay view to an instance of this class you’ll see the “switch camera” button should no longer work, and if you give the button an action method you can pop up an alert message telling users not to use the front camera. However, if you rotate the phone to landscape mode, you’ll notice that while the “switch camera” button is re-positioned, our custom button is not! To see what I mean, try setting a custom background color on our button so you can see it on screen.
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.
Now let’s move the button whenever the orientation changes, and implement the required UIAccelerometerDelegate method.
We’re almost done! Just make sure to start and stop the accelerometer in your init and dealloc methods.
Finished! If you haven’t tested your overlay yet, you can add it to your UIImagePickerController as so.
Don’t forget to provide the button’s action method. You could show an alert, for instance.
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]; }