Advertisement

Build an iOS Music Player: Player Controls

by

This three-part tutorial series will teach you how to build a custom music player with the iOS SDK. Read on!


Series Format:


Welcome to the second part of this tutorial series about building a custom music player with the iOS SDK. In this part, we will continue with the albums section we started in the first installment and we will learn how to play the individual tracks.


Step 1: Updating the Storyboard

Open the storyboard and drag a table view controller from the Object Library to the canvas. We will use this Table View Controller to show a single album. CTRL-drag from the cell in the Albums Table View Controller to the new Table View Controller we just added and select "push" under the "Selection Segue" section from the pop-up menu.

    

Now we are going to create our cells.The first cell in the table view will contain some information about the album, such as the artwork, the album artist, the duration of the album, and the number of songs on the album. The other cell will contain the songs from the album. Select the cell and open the Attributes Inspector. Set the Identifier to "Cell" and change the Style from "Custom" to "Subtitle".

    

Now that we have created the cell for the songs, drag a new table view cell from the Object Library on top of the cell we just edited. Change it’s Selection Style from "Blue" to "None" and set the Identifier to "InfoCell". After that, open the Size Inspector and change the Row Height to 120.

In this cell, we will need an image view for the artwork and two labels for the information. So, first, drag an Image View into the cell and change its size to 100x100. Also change the X and Y properties to 10. After that, drag the two labels in the cell and align them like the image below:

    

Select the first label, open the Attributes Inspector, and then change the Font to "System Bold 17.0". Next, delete the text from both the labels and change the tag from the first label to 101 and the tag from the second label to 102. At last, select the image view and change its tag to 100. We will use these tags to adjust the labels and image view in the tableView:cellForRowAtIndexPath: method.


Step 2: Display a Selected Album

Go to "File" > "New" > "File..." to create a new file. Select "Objective-C class" and then click "Next". Enter "AlbumViewController" for the class and make sure that it’s a subclass of UITableViewController and that both the checkboxes are not selected. Click "Next" again and than click "Create".

Open AlbumViewController.h and modify the code to read as follows:

#import <UIKit/UIKit.h>
#import <MediaPlayer/MediaPlayer.h>

@interface AlbumViewController : UITableViewController
{
    NSString *albumTitle;
}

@property NSString *albumTitle;

@end

Here, we basically just add the MediaPlayer framework to our table view controller and we create an NSString which will contain the album title. We will update this string when the user selects an album.

Now open AlbumsViewController.m and add the following line under #import "AlbumsViewController.h":

#import "AlbumViewController.h"

After that, add the following method:

- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    AlbumViewController *detailViewController = [segue destinationViewController];
    
    MPMediaQuery *albumsQuery = [MPMediaQuery albumsQuery];
    NSArray *albums = [albumsQuery collections];
    
    int selectedIndex = [[self.tableView indexPathForSelectedRow] row];
    MPMediaItem *selectedItem = [[albums objectAtIndex:selectedIndex] representativeItem];
    NSString *albumTitle = [selectedItem valueForProperty:MPMediaItemPropertyAlbumTitle];
                    
    [detailViewController setAlbumTitle:albumTitle];
}

Here we pass the title of the selected album to the detailViewController, which is the AlbumViewController. The storyboard will call this method at runtime when you trigger a segue in the current scene (i.e. when the user selects an album).

Now open AlbumViewController.m and add the following line under the @implementation:

@synthesize albumTitle;

After that, go to the viewDidLoad method and change it to read as follows:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.title = albumTitle;
}

Here, we simply set the the title of the navigation bar to the selected album title.

Now that we have created a new screen for the selected album, I think it’s a good idea to test the app. Click Build and Run to test the app. If you go to the albums tab and select an album, you will go to a new screen with an empty table view, but the title of the navigation bar should be the same as the selected album.


Step 3: Show the Album Songs & Info

Go to the numberOfSectionsInTableView: and the tableView:numberOfRowsInSection: methods in the AlbumViewController.m and modify them to read as follows:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{    
    MPMediaQuery *albumQuery = [MPMediaQuery albumsQuery];
    
    MPMediaPropertyPredicate *albumPredicate = [MPMediaPropertyPredicate predicateWithValue: albumTitle forProperty: MPMediaItemPropertyAlbumTitle];
    [albumQuery addFilterPredicate:albumPredicate];
    
    NSArray *albumTracks = [albumQuery items];

    return [albumTracks count]+1;
}

These methods should be familiar by now, but as you can see we did something new in the second method. We used an MPMediaPropertyPredicte. With an MPMediaPropertyPredicte object, you can filter a query. We used the album title of the selected album for the filter value and we used MPMediaPropertyAlbumTitle as the property, so our query will only contain the album with the title of the album the user selected.

You can use a lot of different properties for a MPMediaPropertyPredicate. You can view them all in the iOS Developer Library docs.

We return the number of songs, plus 1. The extra cell will be for the cell with the album information.

Because we have two different types of cells, with two different heights, we need to tell our table view which height to use for which cell. To do that, add the following method under the numberOfRowsInSection: method:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ([indexPath row] == 0) {
        return 120;
    } else {
        return 44;
    }
}

Here we tell our tableview that our first cell with the album information has a hight of 120 pixels and all the other cells with the songs of the album have a height of 44 pixels.

Now we will create four different methods to get some information about the album. In the first method, we will get the album artwork of the selected album. In the second method, we will get the album artist. In the third method, we will get the duration of the album and the number of songs in the album. And in the last method, we will check if the artists of the songs in the album are the same. Sometimes the songs in an album have the same artists, but not always. If the songs have different artists, we show them in the tableview, but if all the artists are the same, we won’t show them because you can already see it in the first cell.

We will start with creating the first method, so add the following code under the viewDidLoad method:

- (UIImage *) getAlbumArtworkWithSize: (CGSize) albumSize
{
    MPMediaQuery *albumQuery = [MPMediaQuery albumsQuery];
    MPMediaPropertyPredicate *albumPredicate = [MPMediaPropertyPredicate predicateWithValue: albumTitle forProperty: MPMediaItemPropertyAlbumTitle];
    [albumQuery addFilterPredicate:albumPredicate];
    NSArray *albumTracks = [albumQuery items];
    
    for (int i = 0; i < [albumTracks count]; i++) {
        
        MPMediaItem *mediaItem = [albumTracks objectAtIndex:i];
        UIImage *artworkImage;
        
        MPMediaItemArtwork *artwork = [mediaItem valueForProperty: MPMediaItemPropertyArtwork];
        artworkImage = [artwork imageWithSize: CGSizeMake (1, 1)];
        
        if (artworkImage) {
            artworkImage = [artwork imageWithSize:albumSize];
            return artworkImage;
        }
        
    }
    
    return [UIImage imageNamed:@"No-artwork-album.png"];
}

This method will return the artwork image of the album with a specific size. First, we check each song to see if it has an artwork image. We do this with a small size, so our app will be fast. As soon as we find an artwork image, we get that image again with the correct size and return the image. After that, the methods stops, so if the first song contains an artwork image, it returns that image and stops with the method. We added this method because sometimes not all songs in an album contain an artwork image. If the method didn’t find an artwork image, we return a default artwork image.

We haven’t added that default artwork image yet, so let’s do this first. Download the source code attached to this project and drag the No-artwork-album@2x.png and No-artwork-album.png images into the project. Make sure "Copy items into destination group’s folder (if needed)" is checked and click "Finish".

Now add the following code for the second method:

- (NSString *) getAlbumArtist
{
    MPMediaQuery *albumQuery = [MPMediaQuery albumsQuery];
    MPMediaPropertyPredicate *albumPredicate = [MPMediaPropertyPredicate predicateWithValue: albumTitle forProperty: MPMediaItemPropertyAlbumTitle];
    [albumQuery addFilterPredicate:albumPredicate];
    NSArray *albumTracks = [albumQuery items];
    
    for (int i = 0 ; i < [albumTracks count]; i++) {
        
        NSString *albumArtist = [[[albumTracks objectAtIndex:0] representativeItem] valueForProperty:MPMediaItemPropertyAlbumArtist];
        
        if (albumArtist) {
            return albumArtist;
        } 
    }

    return @"Unknown artist";
}

Here we do the same as in the previous method, but this time for the album artist. We check to see if each song already has an album artist. If it does have one, we return it and the method stops. If the method doesn’t find an album artist, it returns the string "Unknown artist".

Now add the following code for our third method:

- (NSString *) getAlbumInfo
{
    MPMediaQuery *albumQuery = [MPMediaQuery albumsQuery];
    MPMediaPropertyPredicate *albumPredicate = [MPMediaPropertyPredicate predicateWithValue: albumTitle forProperty: MPMediaItemPropertyAlbumTitle];
    [albumQuery addFilterPredicate:albumPredicate];
    NSArray *albumTracks = [albumQuery items];
    
    
    NSString *trackCount;
    
    if ([albumTracks count] > 1) {
        trackCount = [NSString stringWithFormat:@"%i Songs", [albumTracks count]];
    } else {
        trackCount = [NSString stringWithFormat:@"1 Song"];
    }
    
    long playbackDuration = 0;
    
    for (MPMediaItem *track in albumTracks)
    {
        playbackDuration += [[track  valueForProperty:MPMediaItemPropertyPlaybackDuration] longValue];
    }
    
    int albumMimutes = (playbackDuration /60);
    NSString *albumDuration;
    
    if (albumMimutes > 1) {
        albumDuration = [NSString stringWithFormat:@"%i Mins.", albumMimutes];
    } else {
        albumDuration = [NSString stringWithFormat:@"1 Min."];
    }
    
    return [NSString stringWithFormat:@"%@, %@", trackCount, albumDuration];

}

In this method, we create a string with information about the number of songs in the selected album and about the duration of the album. First, we create our query and add the filter for the album title. Then we create an array with the items from the query. Next, we check if the album has one or more songs. If it has one song we set the trackCount string to "1 Song" or else we set that string to the number of songs. After that, we create a variable for the duration of the album. We get the duration in seconds, but because we want to show them in minutes we divide the playbackDuration variable by 60. Once that's done, we check if the album is longer than 1 minute. If true, we set the album duration string to the number of minutes or else we say it is equal to 1 minute. At last, we return a string with the track count and the album duration.

Now that we have created our first three methods, add the following code for the last method:

- (BOOL) sameArtists
{
    MPMediaQuery *albumQuery = [MPMediaQuery albumsQuery];
    MPMediaPropertyPredicate *albumPredicate = [MPMediaPropertyPredicate predicateWithValue: albumTitle forProperty: MPMediaItemPropertyAlbumTitle];
    [albumQuery addFilterPredicate:albumPredicate];
    NSArray *albumTracks = [albumQuery items];
    
    for (int i = 0 ; i < [albumTracks count]; i++) {
        
        if ([[[[albumTracks objectAtIndex:0] representativeItem] valueForProperty:MPMediaItemPropertyArtist] isEqualToString:[[[albumTracks objectAtIndex:i] representativeItem] valueForProperty:MPMediaItemPropertyArtist]]) {
        } else {
            return NO;
        }
    }
    
    return YES;
}

In this method, we check if the artist of the songs are the same or not. It uses a for loop to check if the artist of the first song is the same as the artist from the current loop number. If the artist values are not equal, the method returns the boolean NO, but if it is finished with the loop and none of the artists are the same, the methods returns the boolean YES.

That was a lot of code, but it will give our app a better experience, so it’s worthwhile. To actually show the album information and songs, we need to modify the tableView: cellForRowAtIndexPath: method. Go to that method and modify the code to read as follows:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ([indexPath row] == 0) {
        static NSString *CellIdentifier = @"InfoCell";
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
        
        UIImageView *albumArtworkImageView = (UIImageView *)[cell viewWithTag:100];
        albumArtworkImageView.image = [self getAlbumArtworkWithSize:albumArtworkImageView.frame.size];
        
        UILabel *albumArtistLabel = (UILabel *)[cell viewWithTag:101];
        albumArtistLabel.text = [self getAlbumArtist];
        
        UILabel *albumInfoLabel = (UILabel *)[cell viewWithTag:102];
        albumInfoLabel.text = [self getAlbumInfo];
        
        return cell;
    } else {
        
        static NSString *CellIdentifier = @"Cell";
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
        
        
        MPMediaQuery *albumQuery = [MPMediaQuery albumsQuery];
        MPMediaPropertyPredicate *albumPredicate = [MPMediaPropertyPredicate predicateWithValue: albumTitle forProperty: MPMediaItemPropertyAlbumTitle];
        [albumQuery addFilterPredicate:albumPredicate];
        NSArray *albumTracks = [albumQuery items];
        
        NSUInteger trackNumber = [[[albumTracks objectAtIndex:(indexPath.row-1)]  valueForProperty:MPMediaItemPropertyAlbumTrackNumber] unsignedIntegerValue];
        
        if (trackNumber) {
            cell.textLabel.text = [NSString stringWithFormat:@"%i. %@", trackNumber, [[[albumTracks objectAtIndex:(indexPath.row-1)] representativeItem] valueForProperty:MPMediaItemPropertyTitle]];
        } else {
            cell.textLabel.text = [[[albumTracks objectAtIndex:(indexPath.row-1)] representativeItem] valueForProperty:MPMediaItemPropertyTitle];
        }

        
        if ([self sameArtists]) {
    
            cell.detailTextLabel.text = @"";
            
        } else {
            
            if ([[[albumTracks objectAtIndex:(indexPath.row-1)] representativeItem] valueForProperty:MPMediaItemPropertyArtist]) {
                cell.detailTextLabel.text = [[[albumTracks objectAtIndex:(indexPath.row-1)] representativeItem] valueForProperty:MPMediaItemPropertyArtist];
            } else {
                cell.detailTextLabel.text = @"";
            }
            
        }

        return cell;
    }
}

This is a very large method, but the code isn’t really difficult. Let's start from the beginning.

Begin by checking which row we are using. If we are using the first row, we want to show the first cell we created in our storyboard. As you can can see, we use the same CellIdentifier as we used for the cell in our storyboard. We created UIImageView and assigned it to the imageview in the cell with the tag 100, which is of course the same as we used earlier. After that, we call the getAlbumArtworkWithSize: method we created earlier to get the album artwork and update the image view for that artwork. We used the size of the image view for the size of our artwork. After that, we do the same for the two labels. For the text of the first label, we call the getAlbumArtist method and for the second label we call the getAlbumInfo method.

When we are not using the first row, but another one, we want to show the songs of the album. Here we first create a query and add a filter to that query, and then we store the items of that query in an array. After that, we get the track number in the album and check if there is one. If there is a track number available, we add that in front of the song title or else we just show the title of the song. As you can see, we used (indexpath.row-1) for the index of the tracks. We do this because the first cell of the table view is used for the album info.

After that, we check if the songs have the same artists by calling the sameArtists method we created earlier. If the artists are the same, we delete the text of the cell’s detailTextLabel, but if the artists are different and the track contains an artist, we set the text of the detailTextLabel to the artist of that track.

At last, we need to update the class of our table view controller, so open the storyboard, select the table view controller we created in this tutorial, open the Identity Inspector and change the class to "AlbumViewController".

Now that we have filled our cells with the information and songs, I think this is a good time to test our app. Click Build and Run to test the app. If you go to the albums tab and select an album, you should see a table view with an album image, some information about the album and of course the songs that are in that album.


Step 4: Play Some Music

Now that we have created a cool app that shows our songs and albums, it would be nice if our music player could actually play these songs and albums. Open the storyboard and drag a View Controller from the Object Library to the canvas. We are going to use this View Controller in the next tutorial to show the now playing song, but we already need the segues to play the tracks. CTRL-drag from the second cell in the Table View Controller we created in this tutorial and select "push" under the "Selection Segue" section from the pop-up menu. Now do the same for the cell in the Songs Table View Controller.

Open SongsViewController.m and add the following method:

- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{    
    MPMediaQuery *songsQuery = [MPMediaQuery songsQuery];
    NSArray *songs = [songsQuery items];
    
    int selectedIndex = [[self.tableView indexPathForSelectedRow] row];
    
    MPMediaItem *selectedItem = [[songs objectAtIndex:selectedIndex] representativeItem];
    
    MPMusicPlayerController *musicPlayer = [MPMusicPlayerController iPodMusicPlayer];
    
    [musicPlayer setQueueWithItemCollection:[MPMediaItemCollection collectionWithItems:[songsQuery items]]];
    [musicPlayer setNowPlayingItem:selectedItem];
    
    [musicPlayer play];
}

We used this method before in this tutorial, but this time we use it to play a selected track. First we create a query for our songs and put the items in an array. Then we get an MPMediaItem from the array we created. We used the same index as the row we selected, so we can use this MPMediaItem later in this method to update the now playing item. After that, we create an MPMusicPlayerController object and set it to an iPodMusicPlayer. This means that our app shares the iPod state and if we quit our app, the music will continue playing. Next we update the queue of the music player to the items of the query and set the now playing item to the MPMediaItem we created earlier in this method. At last, we start playing the music.

Now open up AlbumViewController.m and add the following method:

- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    MPMediaQuery *albumQuery = [MPMediaQuery albumsQuery];
    MPMediaPropertyPredicate *albumPredicate = [MPMediaPropertyPredicate predicateWithValue: albumTitle forProperty: MPMediaItemPropertyAlbumTitle];
    [albumQuery addFilterPredicate:albumPredicate];
    NSArray *albumTracks = [albumQuery items];
    
    int selectedIndex = [[self.tableView indexPathForSelectedRow] row];
    
    MPMediaItem *selectedItem = [[albumTracks objectAtIndex:selectedIndex-1] representativeItem];
    
    MPMusicPlayerController *musicPlayer = [MPMusicPlayerController iPodMusicPlayer];
    
    [musicPlayer setQueueWithItemCollection:[MPMediaItemCollection collectionWithItems:[albumQuery items]]];
    [musicPlayer setNowPlayingItem:selectedItem];
    
    [musicPlayer play];
}

Here we actually do the same as in the method we created in the SongsViewController.m, but we set the queue of the musicplayer to the selected album.

Now our app can play the songs, click Build & Run to test this app!


Conclusion

In this second part of the series on building a music player, we covered how to create a custom cell for a tableview with a storyboard, how to use a MPMediaPropertyPredicate and how to play the songs and albums we show in our app. In the next and final part of the series, we will create the now playing screen and create a custom interface design for our app.

Advertisement