Monday, November 16, 2009

MPMediaItemCollection Category

I'm currently working on the chapter for More iPhone 3 Development on accessing the iPod Library. I thought I'd throw out another bit of teaser code in the form of a category. MPMediaItemCollection is the class that is used to specify queues of songs to be played. Unfortunately, it's an immutable collection object, and there is no mutable counterpart to it. That makes it kind of a pain when you have to add, delete, or insert items into a collection, or when you want to join multiple collections together.

This category adds a bunch of methods to MPMediaItemCollection that make it easier to manipulate play queues. It doesn't make MPMediaItemCollection mutable, but it does the next best thing. It makes it easier to create new collections based on existing collections. There are methods that create new collections by inserting or deleting songs from an existing collection.

I plan to add a few additional methods for sorting and re-ordering collections for the final version of this category, so look for an update to this in the next few days.

Note: I fixed a couple of leaks after the initial posting

MPMediaItemCollection-Utils.h
#import <Foundation/Foundation.h>
#import <MediaPlayer/MediaPlayer.h>

@interface MPMediaItemCollection(Utils)
/** Returns the first media item in the collection
**/

- (MPMediaItem *)firstMediaItem;

/** Returns the last media item in the collection
**/

- (MPMediaItem *)lastMediaItem;

/** This method will return the item in this media collection at a specific index
**/

- (MPMediaItem *)mediaItemAtIndex:(NSUInteger)index;

/** Given a particular media item, this method will return the next media item in the collection.
If there are multiple copies of the same media item in the list, it will return the one
after the first occurrence.
**/

- (MPMediaItem *)mediaItemAfterItem:(MPMediaItem *)compare;

/** Returns the title of the media item at a given index.
**/

- (NSString *)titleForMediaItemAtIndex:(NSUInteger)index;

/** Returns YES if the given media item occurs at least once in this collection
**/

- (BOOL)containsItem:(MPMediaItem *)compare;

/** Creates a new collection by appending otherCollection to the end of this collection
**/

- (MPMediaItemCollection *)collectionByAppendingCollection:(MPMediaItemCollection *)otherCollection;

/** Creates a new collection by appending an array of media items to the end of this collection
**/

- (MPMediaItemCollection *)collectionByAppendingMediaItems:(NSArray *)items;

/** Creates a new collection by appending a single media item to the end of this collection
**/

- (MPMediaItemCollection *)collectionByAppendingMediaItem:(MPMediaItem *)item;

/** Creates a new collection based on this collection, but excluding the specified items.
**/

- (MPMediaItemCollection *)collectionByDeletingMediaItems:(NSArray *)itemsToRemove;

/** Creates a new collection based on this collection, but which doesn't include the specified media item.
**/

- (MPMediaItemCollection *)collectionByDeletingMediaItem:(MPMediaItem *)itemToRemove;

/** Creates a new collection based on this collection, but excluding the media item at the specified index
**/

- (MPMediaItemCollection *)collectionByDeletingMediaItemAtIndex:(NSUInteger)index;

/** Creates a new collection, based on this collection, but excluding the media items starting with
(and including) the objects at index from and ending with (and including) to.
**/

- (MPMediaItemCollection *)collectionByDeletingMediaItemsFromIndex:(NSUInteger)from toIndex:(NSUInteger)to;
@end



MPMediaItemCollection-Utils.m
#import "Simple_PlayerViewController.h"
#import "MPMediaItemCollection-Utils.h"

@implementation Simple_PlayerViewController
@synthesize titleSearch;
@synthesize playPauseButton;
@synthesize tableView;
@synthesize player;
@synthesize collection;
@synthesize nowPlaying;

#pragma mark -
- (IBAction)doTitleSearch {
if ([titleSearch.text length] == 0)
return;
MPMediaPropertyPredicate *titlePredicate =
[MPMediaPropertyPredicate predicateWithValue: titleSearch.text
forProperty: MPMediaItemPropertyTitle
comparisonType:MPMediaPredicateComparisonContains
]
;
MPMediaQuery *query = [[MPMediaQuery alloc] initWithFilterPredicates:[NSSet setWithObject:titlePredicate]];

if ([[query items] count] > 0) {
if (collection)
self.collection = [collection collectionByAppendingMediaItems:[query items]];
else
self.collection = [MPMediaItemCollection collectionWithItems:[query items]];

collectionModified = YES;
[self.tableView reloadData];
}

[query release];
titleSearch.text = @"";
[titleSearch resignFirstResponder];

}


- (IBAction)showMediaPicker {
MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeMusic];
picker.delegate = self;
[picker setAllowsPickingMultipleItems:YES];
picker.prompt = NSLocalizedString(@"Select items to play", @"Select items to play");
[self presentModalViewController:picker animated:YES];
[picker release];
}


- (IBAction)backgroundClick {
[titleSearch resignFirstResponder];
}


- (IBAction)previousTrack {
[player skipToPreviousItem];
}


- (IBAction)nextTrack {
[player skipToNextItem];
}


- (IBAction)playOrPause {
if (player.playbackState == MPMusicPlaybackStatePlaying) {
[player pause];
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"play.png"] forState:UIControlStateNormal];
}

else {
[player play];
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"pause.png"] forState:UIControlStateNormal];
}

[self.tableView reloadData];
}


- (IBAction)removeTrack:(id)sender {
NSUInteger index = [sender tag];
MPMediaItem *itemToDelete = [collection mediaItemAtIndex:index];
if ([itemToDelete isEqual:nowPlaying])
[player skipToNextItem];
MPMediaItemCollection *newCollection = [collection collectionByDeletingMediaItemAtIndex:index];
self.collection = newCollection;
collectionModified = YES;

NSUInteger indices[] = {0, index};
NSIndexPath *deletePath = [NSIndexPath indexPathWithIndexes:indices length:2];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:deletePath] withRowAnimation:UITableViewRowAnimationFade];
}


#pragma mark -
- (void)viewDidLoad {
MPMusicPlayerController *thePlayer = [MPMusicPlayerController iPodMusicPlayer];
self.player = thePlayer;
[thePlayer release];

if (player.playbackState == MPMusicPlaybackStatePlaying) {
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"pause.png"] forState:UIControlStateNormal];
MPMediaItemCollection *newCollection = [MPMediaItemCollection collectionWithItems:[NSArray arrayWithObject:[player nowPlayingItem]]];
self.collection = newCollection;
self.nowPlaying = [player nowPlayingItem];
}

else {
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"play.png"] forState:UIControlStateNormal];
}





NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver: self
selector: @selector (nowPlayingItemChanged:)
name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification
object: player
]
;

[player beginGeneratingPlaybackNotifications];
}


- (void)viewDidUnload {
self.titleSearch = nil;
self.playPauseButton = nil;
self.tableView = nil;
[super viewDidUnload];
}


- (void)dealloc {
[titleSearch release];
[playPauseButton release];
[tableView release];
[player release];
[collection release];
[super dealloc];
}


#pragma mark -
#pragma mark Media Picker Delegate Methods
- (void) mediaPicker: (MPMediaPickerController *) mediaPicker
didPickMediaItems: (MPMediaItemCollection *) theCollection
{
[self dismissModalViewControllerAnimated: YES];

if (collection == nil){
self.collection = theCollection;
[player setQueueWithItemCollection:collection];
[player setNowPlayingItem:[collection firstMediaItem]];
self.nowPlaying = [collection firstMediaItem];
[player play];
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"pause.png"] forState:UIControlStateNormal];
}

else {
self.collection = [collection collectionByAppendingCollection:theCollection];
}


collectionModified = YES;
[self.tableView reloadData];
}


- (void) mediaPickerDidCancel: (MPMediaPickerController *) mediaPicker {
[self dismissModalViewControllerAnimated: YES];
}


#pragma mark -
#pragma mark Player Notification Methods
- (void)nowPlayingItemChanged:(NSNotification *)notification {
if (![collection containsItem:[player nowPlayingItem]]) {
if (collectionModified) {
[player play];
[player setQueueWithItemCollection:collection];
[player setNowPlayingItem:[collection mediaItemAfterItem:nowPlaying]];
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"pause.png"] forState:UIControlStateNormal];
}

else if ([player nowPlayingItem] != nil) {
MPMediaItem *nowPlayingItem = [player nowPlayingItem];
self.collection = [collection collectionByAppendingMediaItem:nowPlayingItem];
}

}

self.nowPlaying = [player nowPlayingItem];

if (nowPlaying == nil)
[playPauseButton setBackgroundImage:[UIImage imageNamed:@"play.png"] forState:UIControlStateNormal];

collectionModified = NO;
[self.tableView reloadData];
}


#pragma mark -
#pragma mark Table View Methods
- (NSInteger)tableView:(UITableView *)theTableView numberOfRowsInSection:(NSInteger)section {
return [collection count];
}


- (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *identifier = @"Music Queue Cell";
UITableViewCell *cell = [theTableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier] autorelease];

UIButton *removeButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *removeImage = [UIImage imageNamed:@"remove.png"];
[removeButton setBackgroundImage:removeImage forState:UIControlStateNormal];
[removeButton setFrame:CGRectMake(0.0, 0.0, removeImage.size.width, removeImage.size.height)];
[removeButton addTarget:self action:@selector(removeTrack:) forControlEvents:UIControlEventTouchUpInside];
cell.accessoryView = removeButton;
}

cell.textLabel.text = [collection titleForMediaItemAtIndex:[indexPath row]];
if ([nowPlaying isEqual:[collection mediaItemAtIndex:[indexPath row]]]) {
cell.textLabel.font = [UIFont boldSystemFontOfSize:21.0];
if (player.playbackState == MPMusicPlaybackStatePlaying)
cell.imageView.image = [UIImage imageNamed:@"play_small.png"];
else
cell.imageView.image = [UIImage imageNamed:@"pause_small.png"];

}

else {
cell.textLabel.font = [UIFont systemFontOfSize:21.0];
cell.imageView.image = [UIImage imageNamed:@"empty.png"];
}


cell.accessoryView.tag = [indexPath row];


return cell;
}


- (void)tableView:(UITableView *)theTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
MPMediaItem *selected = [collection mediaItemAtIndex:[indexPath row]];

if (collectionModified) {
[player setQueueWithItemCollection:collection];
}


[player setNowPlayingItem:selected];
[player play];

[playPauseButton setBackgroundImage:[UIImage imageNamed:@"pause.png"] forState:UIControlStateNormal];
[self.tableView reloadData];
}


- (CGFloat)tableView:(UITableView *)theTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 34;
}


@end