Hello iCoders. This is a follow up post to my initial post on the Assets Library Framework and Blocks. We came across an interesting problem when working on the application for Animoto.com. They have had an app in the store since the very early days of the app store, and one of our biggest struggles has been creating an interface to allow users to select multiple photos from their photo album for a slideshow. While the iPhone photos application allows for this, the UIImagePicker does not. With the release of the Assets Library framework we can now recreate the experience of the UIImagePicker, with some additional custom functionality to fit our needs. As a result we created a new cloned version of the UIImagePicker that allows for multiple photo selection just like the photos app. We have decided to open source the controller for other developers to use. This post will explain the process of creating the new image picker as well as the method of incorporating it into your code.
The ELCImagePickerController is only possible because of the newly introduced Assets Library framework. This framework gives you raw (more or less) access to the photo and video elements of a user. We looked at UIImagePickerController and saw a lot of weaknesses with it. You can only select 1 photo at a time, and even in Apple’s photo app, where you can choose several pictures at a time, you can’t use multi touch to do your selection. To solve these problems we rolled our own solution that works very closely to UIImagePickerController.
First I am going to explain using the picker since to many people the process of creating it won’t be very interesting. The image picker is created and displayed in a very similar manner to the UIImagePickerController. The sample application that is part of the GitHub project, where I distribute the controller, shows its use, but I will go into detail here. To display the controller you instantiate it and display it modally like so.
ELCImagePickerController *controller = [[ELCImagePickerController alloc] initImagePicker]; [controller setDelegate:self]; [self presentModalViewController:controller animated:YES]; [controller release];
The ELCImagePickerController will return the select images back to the ELCImagePickerControllerDelegate. The delegate contains to methods very similar to the UIImagePickerControllerDelegate. Instead of returning one dictionary representing a single image the controller sends back an array of similarly structured dictionaries. The two delegate methods are:]
- (void)elcImagePickerController:(ELCImagePickerController *)picker didFinishPickingMediaWithInfo:(NSArray *)info; - (void)elcImagePickerControllerDidCancel:(ELCImagePickerController *)picker; |
You can find this project now on GitHub. Please let me know any issues you may have and look for future releases with feature enhancements
The ELCImagePicker is a collection of UITableViewControllers that are placed inside a UINavigationController. While the ELCImagePickerController is actually 5 separate custom classes I have written, I have put them all within a single header and main. I chose this to make the classes that were required to be imported into a project when using this as few as possible. While usually when presenting a UINavigationController you would create one yourself in code and use the initWithRootViewController method, in this case we have created a UINavigationController subclass called ELCImagePickerController which does all this behind the scenes. In the end all a developer has to do is use the initImagePicker method and present the controller modally. This lets the class match the functionality of UIImagePickerController closer. You can see the Header and Main for the ELCImagePickerController class on the next page.
ELCImagePickerControllerDemo from Collin Ruffenach on Vimeo.
#import #import @interface ELCImagePickerController : UINavigationController { id delegate; } @property (nonatomic, assign) id delegate; -(void)selectedAssets:(NSArray*)_assets; -(void)cancelImagePicker; -(id)initImagePicker; @end |
#import "ELCImagePickerController.h" @implementation ELCImagePickerController @synthesize delegate; -(id)initImagePicker { if(self = [super init]) { AlbumPickerController *albumController = [[AlbumPickerController alloc] initWithNibName:@"AlbumPickerController" bundle:[NSBundle mainBundle]]; [albumController setParent:self]; [super initWithRootViewController:albumController]; [albumController release]; } return self; } |
-(void)cancelImagePicker { if([delegate respondsToSelector:@selector(elcImagePickerControllerDidCancel:)]) { [delegate performSelector:@selector(elcImagePickerControllerDidCancel:) withObject:self]; } } -(void)selectedAssets:(NSArray*)_assets { NSMutableArray *returnArray = [[NSMutableArray alloc] init]; for(ALAsset *asset in _assets) { NSMutableDictionary *workingDictionary = [[NSMutableDictionary alloc] init]; [workingDictionary setObject:[asset valueForProperty:ALAssetPropertyType] forKey:@"UIImagePickerControllerMediaType"]; [workingDictionary setObject:[UIImage imageWithCGImage:[[asset defaultRepresentation] fullScreenImage]] forKey:@"UIImagePickerControllerOriginalImage"]; [workingDictionary setObject:[[asset valueForProperty:ALAssetPropertyURLs] valueForKey:[[[asset valueForProperty:ALAssetPropertyURLs] allKeys] objectAtIndex:0]] forKey:@"UIImagePickerControllerReferenceURL"]; [returnArray addObject:workingDictionary]; [workingDictionary release]; } if([delegate respondsToSelector:@selector(elcImagePickerController:didFinishPickingMediaWithInfo:)]) { [delegate performSelector:@selector(elcImagePickerController:didFinishPickingMediaWithInfo:) withObject:self withObject:[NSArray arrayWithArray:returnArray]]; } } #pragma mark - #pragma mark Memory management - (void)didReceiveMemoryWarning { // Releases the view if it doesn't have a superview. [super didReceiveMemoryWarning]; // Release any cached data, images, etc that aren't in use. } - (void)viewDidUnload { [super viewDidUnload]; } - (void)dealloc { [super dealloc]; } @end |
The other callback methods (cancelImagePicker and selectedAssets:) will be discussed later in the post.
The first table view we see is the AlbumPickerController. Here is the header to define that class
@interface AlbumPickerController : UITableViewController { NSMutableArray *assetGroups; NSOperationQueue *queue; id parent; } @property (nonatomic, assign) id parent; -(void)preparePhotos; -(void)selectedAssets:(NSArray*)_assets; @end |
In the main first we fill in viewDidLoad. We first set the navigation controller to “Loading”. Next we create an NSOperation queue which calls an operation on preparePhotos. The preparePhotos method is where we will ask the AssetsLibrary Framework for the list of Albums I can get access from. Finally I add the dismiss button to the navigation bar. Use the code below.
- (void)viewDidLoad { [super viewDidLoad]; [self.navigationItem setTitle:@"Loading..."]; queue = [NSOperationQueue mainQueue]; NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(preparePhotos) object:nil]; [queue addOperation:operation]; UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(dismiss:)]; [self.navigationItem setRightBarButtonItem:cancelButton]; [cancelButton release]; } |
Next we are going to define the dismiss method. This will call the cancelImagePicker method on out parent (ELCImagePickerController). We now get to the preparePhotos method. The first thing we do in this method is declare a block that we call assetGroupEnumerator. It takes an ALAssetGroup and a BOOL as parameters. The blocks checks the ALAssetGroup against nil and adds it to the NSArray class parameter assetGroups. At the end of the block I reload the table view. This is the block that I will use to enumerate through all the ALAssetGroups that the Assets Library Framework gives access to. After defining the block we instantiate the assetGroups array. Next we create an ALAssetsLibrary object. We call the method enumerateGroupsWithTypes:usingBlocks:filureBlocks: passing in assetGroupEnumberator the block we declared at the beginning of the method. We create a small block withing the method as the failureBlocks which will be called upon error. See the code below.
-(void)dismiss:(id)sender { [parent cancelImagePicker]; } -(void)preparePhotos { void (^assetGroupEnumerator)(struct ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop) { if(group != nil) { [assetGroups addObject:group]; NSLog(@"Number of assets in group %d", [group numberOfAssets]); } [self performSelectorOnMainThread:@selector(reloadTableView) withObject:nil waitUntilDone:NO]; }; assetGroups = [[NSMutableArray alloc] init]; ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; [library enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:assetGroupEnumerator failureBlock:^(NSError *error
The second table view we see is the AssetTablePicker. Here is the header to define that class
@interface AssetTablePicker : UITableViewController { ALAssetsGroup *assetGroup; NSMutableArray *assets; NSMutableArray *assetURLDictionaries; int selectedAssets; id parent; NSOperationQueue *queue; } @property (nonatomic, retain) IBOutlet UILabel *selectedAssetsLabel; @property (nonatomic, assign) id parent; -(void)setAssetsGroup:(ALAssetsGroup*)_group; -(IBAction)dismiss:(id)sender; @end |
This class is a bit more complicated than the album picker, but despite its complexity, its basic principals are similar. It has the same class properties as the AlbumPickerConrtroller with the addition of an array to keep track of the URL’s of all the Assets it encounters. Lets get into the main and see exactly what happen to fill this table. First in the view didLoad we do some house keeping with the navigation controller and call the method preparePhotos in the background. Since we are calling preparePhotos to run in the background the first thing we must do is declare an NSAutoReleasePool, at the end we will release it. This helps manage background operations within iOS. Next in prepare photos, we do the same operation as we did in AlbumPickerController except this time we add Assets to the array assets. The Assets object is instantiated with an ALAsset object and is a UIView subclass. It will be covered in greater detail later in this post. Every time we see an Asset we add its URL to the assetURLDictionaries array. This is due to accidental duplicates handed back from the Assets Library Framework. It is an odd workaround now, but seems to work fine. With that done we reload the table view.
@implementation AssetTablePicker @synthesize parent; @synthesize selectedAssetsLabel; -(void)viewDidLoad { [self.tableView setSeparatorColor:[UIColor clearColor]]; [self.tableView setAllowsSelection:NO]; UIBarButtonItem *doneButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dismiss:)]; [self.navigationItem setRightBarButtonItem:doneButtonItem]; [self.navigationItem setTitle:@"Loading..."]; [self performSelectorInBackground:@selector(preparePhotos) withObject:nil]; } - (void)setAssetsGroup:(ALAssetsGroup*)_group { assetGroup = _group; } -(void)preparePhotos { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; void (^assetEnumerator)(struct ALAsset *, NSUInteger, BOOL *) = ^(ALAsset *result, NSUInteger index, BOOL *stop) { if(result != nil) { if(![assetURLDictionaries containsObject:[result valueForProperty:ALAssetPropertyURLs]]) { if(![[result valueForProperty:ALAssetPropertyType] isEqualToString:ALAssetTypeVideo]) { [assetURLDictionaries addObject:[result valueForProperty:ALAssetPropertyURLs]]; Asset *asset = [[Asset alloc] initWithAsset:result]; [assets addObject:asset]; } } } }; assets = [[NSMutableArray alloc] init]; assetURLDictionaries = [[NSMutableArray alloc] init]; [assetGroup enumerateAssetsUsingBlock:assetEnumerator]; [self.tableView reloadData]; [self.navigationItem setTitle:@"Pick Photos"]; [pool release]; } |
So now we are at a point where we have an array (assets) of all the ALAssets in the selected ALAssetGroup represented as Assets objects. We now have to figure out a way to display thumbnails of each of these images (4 per row) in a table view. We will accomplish this be creating a custom UITableViewCell that we will call AssetCell. An asset cell will accept an array of Asset objects and utilize it as the UIView subclass that it is to lay them out. We will get to that in the next section, but it is important to know now as we need to create the method that will prepare the subarray of Assets that we will pass to our AssetCell cells. This method is called assetsForIndexPath:. The method will look at the index path and then convert it to the indices of the actual ALAsset objects the index path will be displaying. After it has done this it checks that it is not going to look for more assets then there are in the array. This method will always return an array of size 4 except for the last row of the table view which will be either 1, 2, 3 or 4 assets in length. See the code below.
-(NSArray*)assetsForIndexPath:(NSIndexPath*)_indexPath { int index = (_indexPath.row*4); int maxIndex = (_indexPath.row*4+3); if(maxIndex < [assets count]) { return [NSArray arrayWithObjects:[assets objectAtIndex:index], [assets objectAtIndex:index+1], [assets objectAtIndex:index+2], [assets objectAtIndex:index+3], nil]; } else if(maxIndex-1 < [assets count]) { return [NSArray arrayWithObjects:[assets objectAtIndex:index], [assets objectAtIndex:index+1], [assets objectAtIndex:index+2], nil]; } else if(maxIndex-2 < [assets count]) { return [NSArray arrayWithObjects:[assets objectAtIndex:index], [assets objectAtIndex:index+1], nil]; } else if(maxIndex-3 < [assets count]) { return [NSArray arrayWithObject:[assets objectAtIndex:index]]; } return nil; } |
With this convenience method in place we can move into the UITableViewDataSource methods for this tableview. The way in which we get it to look so unlike a normal tableview will become clear when we look into the implementation of the AssetCell UITableViewCell and the Asset UIView. For now we will just inject the AssetCells with the arrays provided by our assetsForIndexPath method.
#pragma mark UITableViewDataSource Delegate Methods - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return ceil([assets count]/4.0); } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; AssetCell *cell = (AssetCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[AssetCell alloc] initWithAssets:[self assetsForIndexPath:indexPath] reuseIdentifier:CellIdentifier] autorelease]; } else { [cell setAssets:[self assetsForIndexPath:indexPath]]; } return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 79; } - (void)dealloc { [super dealloc]; } |
The final method contained within our AssetTablePicker is a method used when the user has selected all the assets that they want. This method looks through the Assets we have within our assets array. An Asset object has a class method called selected which returns a BOOL. If the Asset has been selected this will return true. We make a subarray of all the selected Asset object, bundle them up and pass them to our parent (AlbumPickerController). This will become clearer near the end of the post. See the code for the dismiss method below.
-(IBAction)dismiss:(id)sender { NSMutableArray *selectedAssetsImages = [[NSMutableArray alloc] init]; for(Asset *asset in assets) { if([asset selected]) { [selectedAssetsImages addObject:[asset asset]]; } } [(AlbumPickerController*)parent selectedAssets:[NSArray arrayWithArray:selectedAssetsImages]]; } |
The next class we encounter is AssetCell, a UITableViewCell subclass. The header and main can be see below. The only interesting code within this class is the code to layout each of the assets located in layout subviews. This method goes through every Asset object which it was handed and applies a frame to it. The UIImagePickerController has a 4 pixel border between each of the assets it lists. We do the same and smartly calculate the frame for each asset as the for loop executes. Also within this loop we create a UITapGestureRecognizer for each Asset object. We essentially tell each asset view to call the method toggleSelection: on itself every time it is tapped. You will see the implementation of the toggleSelection method in a moment. For now see the code below for specifics on AssetCell.
@interface AssetCell : UITableViewCell { NSArray *assets; } -(id)initWithAssets:(NSArray*)_assets reuseIdentifier:(NSString*)_identifier; -(void)setAssets:(NSArray*)_assets; @end |
@implementation AssetCell -(id)initWithAssets:(NSArray*)_assets reuseIdentifier:(NSString*)_identifier { if(self = [super initWithStyle:UITableViewStylePlain reuseIdentifier:_identifier]) { assets = _assets; [assets retain]; } return self; } -(void)setAssets:(NSArray*)_assets { for(UIView *view in [self subviews]) { [view removeFromSuperview]; } assets = nil; assets = _assets; [assets retain]; } -(void)layoutSubviews { CGRect frame = CGRectMake(4, 2, 75, 75); for(Asset *asset in assets) { [asset setFrame:frame]; [asset addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:asset action:@selector(toggleSelection)]]; [self addSubview:asset]; frame.origin.x = frame.origin.x + frame.size.width + 4; [asset release]; } } -(void)dealloc { [assets release]; [super dealloc]; } |
@end |
Asset is the final class which comprises the ELCImagePickerController. Asset is a UIView subclass which takes in an ALAsset and creates a view that represents it. The final view will be a square thumbnail of the ALAsset that is passed in along with a checkmark overlay that is very similar to the checkmark used in Apple’s Photos application. In AssetCell we set up the UITapGestureRecognizer that will call this method whenever an asset is tapped. ALAssets, which we inject into an Asset object when creating, have a convenience method called thumbnail that we can utilize to get the actual image that will represent the asset. After adding that to the view we add the image view for the overlay and set it as hidden. Our toggleOverlay: method will check whether that view is hidden. Finally we have a checker to see if the asset is selected with the method selected. If the overview is hidden the asset is not selected and if it is visible it is selected. All this comes together to form the final smarts we need to make our picker functional. See the header and main below.
@interface Asset : UIView { ALAsset *asset; UIImageView *overlayView; BOOL selected; id parent; } @property (nonatomic, retain) ALAsset *asset; @property (nonatomic, assign) id parent; -(id)initWithAsset:(ALAsset*)_asset; -(BOOL)selected; @end |
@implementation Asset @synthesize asset; @synthesize parent; - (id)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { // Initialization code } return self; } -(id)initWithAsset:(ALAsset*)_asset { if (self = [super initWithFrame:CGRectMake(0, 0, 0, 0)]) { asset = _asset; [asset retain]; CGRect viewFrames = CGRectMake(0, 0, 75, 75); UIImageView *assetImageView = [[UIImageView alloc] initWithFrame:viewFrames]; [assetImageView setContentMode:UIViewContentModeScaleToFill]; [assetImageView setImage:[UIImage imageWithCGImage:[asset thumbnail]]]; [self addSubview:assetImageView]; [assetImageView release]; overlayView = [[UIImageView alloc] initWithFrame:viewFrames]; [overlayView setImage:[UIImage imageNamed:@"Overlay.png"]]; [overlayView setHidden:YES]; [self addSubview:overlayView]; } return self; } -(void)toggleSelection { overlayView.hidden = !overlayView.hidden; } -(BOOL)selected { return !overlayView.hidden; } -(void)setSelected:(BOOL)_selected { [overlayView setHidden:!_selected]; } - (void)dealloc { [asset release]; [overlayView release]; [super dealloc]; } @end |
So now that all the assets can be displayed and selected lets take a look at how they are passed up the chain. First thing to know is that the ELCImagePickerController passes up Asset objects, since they contain references to the ALAsset which they represent. With that said the place we start is in the AssetTablePicker. When dismiss is clicked there the class calls the dismiss method which looks through the assets array and checks for which one is selected. It adds the selected Asset objects to an array and then calls AlbumPickerControllers selectedAssets: method. The selectedAssets method just passes this array along to the ELCImagePickerController method selectedAssets:. Here a dictionary is constructed to represent every asset that was selected. The dictionary contains the same information that is delivered by UIImagePickerController when it delivers assets. This way the ELCImagePickerController can be included into projects as simply as possible. Below you will see each of the passback methods described above as well as the definition of the ELCImagePickerControllerDelegate Protocol which defines the methods that should be implemented on the calling class.
-(IBAction)dismiss:(id)sender { NSMutableArray *selectedAssetsImages = [[NSMutableArray alloc] init]; for(Asset *asset in assets) { if([asset selected]) { [selectedAssetsImages addObject:[asset asset]]; } } [(AlbumPickerController*)parent selectedAssets:[NSArray arrayWithArray:selectedAssetsImages]]; } |
-(void)selectedAssets:(NSArray*)_assets { [(ELCImagePickerController*)parent selectedAssets:_assets]; } |
-(void)selectedAssets:(NSArray*)_assets { NSMutableArray *returnArray = [[NSMutableArray alloc] init]; for(ALAsset *asset in _assets) { NSMutableDictionary *workingDictionary = [[NSMutableDictionary alloc] init]; [workingDictionary setObject:[asset valueForProperty:ALAssetPropertyType] forKey:@"UIImagePickerControllerMediaType"]; [workingDictionary setObject:[UIImage imageWithCGImage:[[asset defaultRepresentation] fullScreenImage]] forKey:@"UIImagePickerControllerOriginalImage"]; [workingDictionary setObject:[[asset valueForProperty:ALAssetPropertyURLs] valueForKey:[[[asset valueForProperty:ALAssetPropertyURLs] allKeys] objectAtIndex:0]] forKey:@"UIImagePickerControllerReferenceURL"]; [returnArray addObject:workingDictionary]; [workingDictionary release]; } if([delegate respondsToSelector:@selector(elcImagePickerController:didFinishPickingMediaWithInfo:)]) { [delegate performSelector:@selector(elcImagePickerController:didFinishPickingMediaWithInfo:) withObject:self withObject:[NSArray arrayWithArray:returnArray]]; } } |
@protocol ELCImagePickerControllerDelegate - (void)elcImagePickerController:(ELCImagePickerController *)picker didFinishPickingMediaWithInfo:(NSArray *)info; - (void)elcImagePickerControllerDidCancel:(ELCImagePickerController *)picker; @end |
You can find this project now on GitHub. Please let me know any issues you may have and look for future releases with feature enhancements. Happy coding!
Follow me on Twitter @cruffenach