Introduction
Selecting and editing images like the features found in the contacts app appeared to be the correct use case for the class UIImagePickerController
(image picker). It even seemed to nicely support customization for an editing overlay. Unfortunately when I began using it, it proved to be inflexible. Replicating the features presented more challenges than one may have expected.
This article describes a class called MMSProfileImagePicker
(profile picker) to support image selection and editing features identical to the contacts app.
There are others who solved this problem and made their classes available to the developer community. And so you might ask, what’s new? My aim with this article is to reveal some techniques that could be reused to solve other vexing problems.
This solution supports these features with the help of the image picker. This article focuses on the techniques to integrate with it and how to use profile picker in your own app. The downloadable example implements an identical image selection solution to the contacts app.
Figure 1 - Example Application
Background
While exploring how to use the overlay feature of the image picker, I encountered a number of stumbling blocks. Some of the problems encountered were how to position and size the overlay correctly, how to make the circle only show on the edit screen when selecting from the camera, and how to position it correctly in the z-order? These were the few that I recollect.
Some had no solutions, others were complicated, and others were too tightly coupled with the internal implementation of the class. For example, this solution on stackoverflow manipulated the private views created by image picker to display the circle overlay.
It was undoubtedly clever, but it was too susceptible to changes in future iOS
releases, and it only worked for selecting a photo and not when taking a picture or editing one: it’s not possible to intercept the navigation delegate calls when displaying the camera to insert the overlay just before the Move and Scale screen (edit screen) presents.
There were many interesting problems to solve: too many to include in one article. For the challenges and solutions for cropping a bitmap, please have a read of my article A View Class for Cropping Images. It describes the UIImage+Cropping
category used by this solution.
Note, I did not review any of the open source solutions. Any similarities with them is coincidental.
Approach
The strategy was to exploit image picker to its fullest since it had much of the required functionality. Unfortunately, some of the class’ features necessitated reimplementation to work as in contacts app. One challenging aspect was to show the circle overlay on only the edit screen. When configured to show the camera, the overlay displays on both the acquire image screen and the edit one. Consequently, solving this problem required reinventing solutions to problems that image picker had solved.
To address that need, I concluded that the best way was make image picker responsible for presenting the camera and image selection from the photo library and have the new class responsible for creating and showing the edit screen.
This approach revealed other complexities to solve:
- How to prevent image picker from presenting its edit screen?
- How to transition to the edit screen from the image picker?
- How to grab the camera image from image picker to hand-off to the edit screen?
<licreen< li=""> -
-
-
Disabling Image Picker's Edit Screen
This solution entirely recreates the edit screen functionality and shows it instead of image picker’s. Image picker displays the cameraOverlay
property on both the acquire image and edit screens when configured to select from the camera. This approach provides a solution for preventing the circle overlay from showing on the camera’s image acquire screen.
To present the custom edit screen from the select from photo library configuration was quite simple. Image picker has a property to disable presentation of the edit screen. Setting allowsEditing
property to NO
returns the user’s selection without showing it. This provided an easy hook for showing the custom edit screen.
Unfortunately, when configured for camera selection, image picker does not obey this property and shows the edit screen regardless of the value. Here’s where things got tricky. I did not wish to rewrite the entire camera functionality, but there was no documented way to disable it, yet I did not wish to abandon this approach.
If I could find a way to have image picker call my action method instead of its, profile picker could prevent its edit screen from presenting. Being that image picker is a navigation controller, profile picker could observe the views it transitions to by supporting the UINavigationControllerDelegate
interface. However, if image picker is presented from the application’s view controller, UIKit
calls the methods on the application’s view controller.
To remove any responsibility from the application for handling the navigation delegate, profile picker creates a proxy view controller whose job is to handle the navigation delegate and forward the calls to profile picker.
In the navigation delegate method navigationController:didShowViewController:animated:
profile picker searches the view hierarchy for the button that when tapped takes the photo. Here are the steps:
It checks if the camera is about to display its view.
if (imagePicker.sourceType == UIImagePickerControllerSourceTypeCamera && !isSnapPhotoTargetAdded && isPresentingCamera)
It finds the take photo button view at index 8 of the bottom view bar’s sub view list.
UIView* bottomBarView = [viewController.view.subviews objectAtIndex:2];
UIButton* buttonView = [bottomBarView.subviews objectAtIndex:8];
and removes its action method for the UIControlEventTouchUpInside
event.
[buttonView removeTarget:viewController.view action:NULL forControlEvents:UIControlEventTouchUpInside];
It adds its own handler to the button:
[buttonView addTarget:self action:@selector(takePhoto:) forControlEvents:UIControlEventTouchUpInside];
Now when the user taps the button to take a photo, profile picker’s action method takePhoto:
gets called and image picker is unable to show its edit screen.
Transition to Edit Screen
Profile picker is the view controller responsible for presenting the custom edit screen. It creates the view and sub views, displays the circular overlay and image, and handles the screen’s button events. While it’s not a terribly difficult screen to layout, there are a number of knotty layout calculations needed to present and center the image, to center the overlay, and to create the scroll view’s contentInset
.
Since the objective of this article is to describe the integration between this object and the image picker, I leave it to the reader to download the code and review the implementation for the details.
There are three paths to transition to the edit screen.
- The application presents an existing image for editing.
- The image is to be selected from the photo album.
- The camera is to capture an image.
Presenting Existing Image
For the use case where the application requests profile picker to edit an existing image, it presents the edit screen with the modal presentation style in the method presentEditScreen:withImage:
.
-(void)presentEditScreen:(UIViewController* _Nonnull)vc withImage:(UIImage* _Nonnull)image{
isDisplayFromPicker = isPresentingCamera = NO;
imageToEdit = image;
presentingVC = vc;
self.modalPresentationStyle = UIModalPresentationFullScreen;
[presentingVC presentViewController:self animated:YES completion:nil];
}
Selecting an Image from the Photo Album
When the application requests profile picker to display the photo album selection, it sets the sourceType property of image picker to UIImagePickerControllerSourceTypePhotoLibrary
. Profile picker does not rely on the image picker to display its edit screen, so it sets the image picker allowsEditing
property to NO
.
When the user selects an image, image picker calls the delegate method imagePickerController:didFinishPickingMediaWithInfo
on the profile picker controller. Profile picker references the image and calls its method editImage
to present the edit screen with the image.
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
UIImage* tempImage = [info objectForKey:UIImagePickerControllerOriginalImage];
[self editImage:tempImage];
}
Since image picker is a navigation controller, it pushes the edit screen on the stack to support cancel navigation.
[imagePicker pushViewController:self animated:NO]
Unless the navigation bar is hidden, it displays on the edit screen. Before pushing it, it sets the property to hide the bar.
[imagePicker setNavigationBarHidden:YES]
The full implementation of editImage
follows:
-(void)editImage:(UIImage*)image {
imageToEdit = image;
self.modalPresentationStyle = UIModalPresentationFullScreen;
[imagePicker setNavigationBarHidden:YES];
[imagePicker pushViewController:self animated:NO];
}
Selecting an Image from the Camera
Similar to selecting from the photo album, to show the camera the sourceType
property is set to UIImagePickerControllerSourceTypeCamera
. When the photo is captured, profile picker calls editImage
to display the edit screen.
Grabbing the Camera Image
Since profile picker replaces image picker’s action method to prevent it from showing the edit screen, it additionally prevents it from acquiring the image. Profile picker has responsibility for transferring the image in the camera’s view from the device.
For readers interested in the algorithms for that, I leave it to you to review the code, but if you have any questions, please submit a comment. See the following methods in the file MMSProfileImagePicker.m
:
prepareToCaptureStillImage:
- It has the responsibility for creating the AVCaptureSession and initializing it. captureStillImage:
- It has the responsibility for initiating transfer of the image from the camera’s view into the data buffer. cameraConnection:
- Finds the camera’s AVCaptureConnection and returns it. getCameraDevice:
- Returns the AVCaptureDevice for the camera on the back of the phone. captureInpute:
- Adds the AVCaptureDeviceInput for the device to the session.
The method captureStillImage
gets the camera’s image and calls editImage
with the photo received.
There was one particular challenge with acquiring the image that I’d like to note. In the first draft of the implementation, the image quality was low: it was darker than if taken with the camera app.
I scoured the internet for other’s experiences, and one explanation recommended to delay the call to captureStillImageAsynchronouslyFromConnection
after starting AVCaptureSession
. Inserting a 0.75 second delay corrected the problem. I sense that there is some other missing configuration to obsolete this requirement. It's too unreliable to insert an undocumented delay to correct this. For now, it works, but if any readers have other solutions, I’d like to hear about them.
Using MMSProfileImagePicker
To give your application the analog of the features found in the contacts app, the class supports three public interfaces:
Call presentEditScreen:
method to edit an image that the application already has.
-(void)presentEditScreen:(UIViewController* _Nonnull)vc withImage:(UIImage* _Nonnull)image;
Call selectFromPhotoLibrary:
method to select the image from the photo library.
-(void)selectFromPhotoLibrary:(UIViewController* _Nonnull)vc;
Call selectFromCamera:
method to select an image by taking a photo.
-(void)selectFromCamera:(UIViewController* _Nonnull)vc;
To receive the selected and edited image, the application implements the delegate MMSProfileImagePickerDelegate
(profile picker delegate) on the view controller presenting profile picker. Before calling the desired method to select an image, it sets the profile picker’s delegate property.
- (IBAction)moveAndScale{
profilePicker = [[MMSProfileImagePicker alloc] init];
profilePicker.delegate = self;
[profilePicker presentEditScreen:self withImage:originalImage];
}
The profile picker delegate is an analogous interface to UIImagePickerControllerDelegate
(image picker delegate), and its purpose is similar: to receive the selected and edited image. Before exiting the method, the application should dismiss the profile picker.
-(void)mmsImagePickerController:(MMSProfileImagePicker *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
cropImage = [info objectForKey:UIImagePickerControllerEditedImage];
originalImage = [info objectForKey:UIImagePickerControllerOriginalImage];
self.circleImageView.image = cropImage;
self.squareImageView.image = cropImage;
self.btnAdd.hidden = YES;
self.circleImageView.hidden = NO;
self.btnEdit.hidden = NO;
[picker dismissViewControllerAnimated:YES completion:nil];
}
The dictionary parameter supports all the editing information keys defined in image picker delegate’s equivalent implementation. See editing information keys in iOS developer documentation.
On the other hand, the user may have begun the process of selecting and image and decides to abort the operation. In this instance, profile picker calls delegate method mmsImagePickerControllerDidCancel:
. In response, the application should dismiss the profile picker with a call to dismissModalViewControllerAnimated:
-(void)mmsImagePickerControllerDidCancel:(MMSProfileImagePicker *)picker {
[self dismissViewControllerAnimated:YES completion:nil];
}
Summary
There is rarely the best way to solve a problem in software. Oh sure, we all have our strong opinions. Nevertheless, there are good ways, better ways, different ways, and awful ways. If you pull the covers from over any application, you will find examples of all of them. The forces of competing constraints, team culture, experience, and individual style shape our solutions.
I wrote this article to reveal a different way of solving this one. My priorities when developing an application are to use the framework to it’s fullest, write the least amount of code, find ways to focus on the business logic over creating enabling widgets and tools such as the one described here, and use third party libraries when available.
In this instance, I expected the Cocoa Touch framework to provide the answer. As I began using it, I found it did not support the feature out of the box. However, it appeared to support extending the functionality with overlay support. So in the effort to write less code, I explored that approach.
I found it was too kludgy and rigid. Since I had already exhausted a bit of time down this path, I believed I could write my own version of the edit screen and just plug it in. But inexperience slammed into the wall of reality, and I found it wasn’t going to be that simple. As I solved one problem, the onion peeled, and a new one revealed itself until finally I arrived at the solution described here.
Hopefully with this article I’ve given the reader an appreciation for one way of solving this problem and some techniques that you may use in your own development. If you like it enough to use the pod in your application, that would be great. It would be even better if you liked it enough to contribute your own enhancements to the widget.
This class is available as a cocoapod at cocoapods.org. Search for MMSProfileImagePicker
to begin using it. And if you wish to report defects and contribute your own improvements to the widget, you can find the code on github at https://github.com/miller-ms/MMSProfileImagePicker.
Happy coding!
History
- March 7, 2016 - Updated the summary.
- March 3, 2016 - Reworded some sentences that did not read well.
- March 2, 2016 - Initial release.