After starting to learn swift, my first project was a fitness app. I knew I wanted people to stay in the app as long as possible in order to make more ad revenue, so I set out to let users control their music from within the app. I found it surprisingly difficult to found any resources out there to help me accomplish this, so I decided to create an example project full of everything I learned. Now, almost a year later, I've decided to write up a tutorial.
UPDATE: I have since decided to spruce up that project and release it to the app store.
Create a single page project, mine is called "Audio Test". I set up the view controller with 6 buttons, 4 labels, 1 slider, one image view, and one container view. like this:
I have the constraints for the labels, slider, and buttons all set relative to the top of the view controller. The constraints for the container view are set relative to the bottom of the view controller. The bottom constraint of the image view is set relative to the container view and the top constraint is set to the label placed below slider. With this setup, the image view will resize itself based on the size of the display being used and it should look good on most devices.
Also make sure to set the image view's mode to aspect fit, this makes the image fit within the constraints without stretching it to fill she shape of the image view.
I have the outlets labeled like this:
You can call them whatever you want but it will be helpful when going through the tutorial to have the same outlet names.
First, you'll need to import the media framework:
You'll also need to create variables for the media player and for the timer function so you can call them later
Next we'll need to add some code to the viewDidLoad function to get things going at startup, starting with getting the media player ready to go:
Next you'll need to set up the timer, this will come in handy when a song is playing
This will call a function called timerFired, which we'll create later.
While still inside viewDidLoad, we need to create a notification for when the playback item (song) changed.
This will call the updateNowPlayingInfo function, which, again, we'll see later.
Updating the labels
Let's create the timerFired function from before.
Inside that function is where we are going to update all of the labels as well as get the slider to progress along with the song. Enter the following in the function:
This creates a constant for MPMusicPlayerController.systemMusicPlayer().nowPlayingItem so it's easier to call while also ensuring that it exists before trying to pull the information. We're going to need to pull a few key things before we can do anything, and the code should be pretty self explanatory.
Now remember, because we set the timer's interval to 0.1, this information will be updated every 0.1 seconds, so the currentPlaybackTime will constantly update as the song plays. So what do we do with that information? First, let's go ahead and set the mage to the current track's album artwork, which we've already gotten above.
We can also set the label above the slider to display the song's artist and title.
That one was simple enough, so now let's set the label below the slider to show the length of the song. While you'd think it might be as simple as labelDuration.text = trackDuration, but it's slightly trickier than that. Let's say, for example, a song is 4 minutes and 5 seconds long, the value for trackElapsed is actually 245, because the song is 245 seconds long. What we need to do is split this into two values to show both the minutes by dividing trackDuration by 60 and the seconds by getting the remainder of trackDuration / 60. So we can enter the following code.
But wait! There's another step! Think about what would happen when a song is 4 minutes and 5 seconds long. They way we just set that up, the label would show "Length: 4:5". That looks a little wrong, doesn't it? We need to account for what should happen when trackDurationSeconds is only 1 digit. In that case, we should show labelDuration.text = "Length: \(trackDurationMinutes):0\(trackDurationSeconds)". This will display "Length: 4:05". Looks better, right? So lets change that last bit of code to the following:
Great! Now we have all our bases covered. The code for labelElapsed will look exactly the same:
As mentioned before, this value will update every 0.1 seconds, leading for a constantly changing label that shows the current elapsed time of the song. So that takes care of three of the four labels, leaving us with labelRemaining. This will also look exactly the same as the code for labelDuration and labelElapsed, but we cant do anything without first getting the value for how much time is left in the song. Logically, we can get this by subtracting trackDuration by trackElapsed:
Now that we've taken care of the labels, we need to handle the slider. We want the slider to have two functions:
- Move from left to right as the song progresses from beginning to end
- Move through the song as the user slides it from left to right
Step 1 is taken care of within the timerFired function, while step two will be handled within its own function. A UISlider will, by default, have a minimum value of 0 and a maximum value of 100. In order to get it to work with the length of the song, we need to change the maximum value of the slider to the number of seconds in the current track.
And now, finally, for the last line of code within the timerFired function, we can get the slider to move with the elapsed time of the song.
That's it for the timerFired function.
Remember that updateNowPlayingInfo function we called in viewDidLoad? Add this function to the view controller:
This method will be called whenever the song changes, whether that's done by pressing the next or previous buttons or by letting the song end and move on to the next. Within this function, we are just going to re-call the timer function. This may not always be necessary, but it is good to make sure the labels and album image will change to the new song
At this point you can start playing music on your device, then load this app and see the labels and slider at work. I should note, this app will NOT work on simulator. It requires access to music previously loaded onto the device, which cannot be done with the simulator.
IBActions: Buttons and slider
The next step will be to work on all the IBActions. This will include all the buttons as well as the action for when the user moves the slider.
I have my actions titled as sliderTimeChanged, buttonPlay, buttonPause, buttonPrevious, buttonBeginning, buttonNext,and buttonPick. Hopefully, you should be able to figure out which IBAction title relates to which button. I'm going to quickly run through all of these except for buttonPick, which will require a little extra work to set up.
When a user moves the slider, we need two things to happen. We need to know where the slider is moved to and bring the the songs playback time to match the new value of the slider.
As a reminder, we have already changed the minimum and maximum values of the slider from 0-100 to 0-245, so when the slider is moved, it will change the value to, for example, 137, which will then start playing the song at the 137th second, or 2 minutes an 17 seconds into the song For the media control buttons, the code is fairly straightforward so I'll just lay them out here:
While I have skipToPreviousItem() and skipToBeginning() set up as two different buttons, you may notice that most music apps will have one button that skips to beginning or skips to previous item based on where you are in the song. If you want to skip to the previous item in one of these apps, you often have to press the back button twice. I wanted to keep the functions separate for this example, but if you wanted to make yours work in that fashion, the code would probably look something like this:
Warning: If you wanted to try this in the app as it is currently set up, it would not work because trackElapsed was created within the timerFired function and cannot be called outside of that function. In order to get this to work you would have to create var trackElapsed: NSTimeInerval! outside of the timerFired function and, within the timerFired function, change the code from let trackElapsed = mp.currentPlaybackTime to just trackElapsed = mp.currentPlaybackTime, without the "let".
Using MPMediaPickerController to pick the next song(s) to play
Let's walk through the steps we want to happen here:
- The user clicks the "pick" buton
- An additional view comes up that shows the user all of their music
- They choose the song(s), playlist(s), artist(s), album(s) they want to play
- They click the "Done" button that is displayed in the music picker view
- The view is dismissed and the music they selected loads and begins to play.
First thing we need to to is add MPMediaPickerControllerDelegate to the class:
We then need to create a variable for MPMediaPickerController so we can easily refer to it later:
Next, we go into the viewDidLoad and set up the media picker controller:
Let's take a second to talk about the allowsPickingMultipleItems property. When set to false, the picker view will display, the user will have to find the exact song they want to play, the view will automatically dismiss itself when one song is selected, and that one song will play on repeat until a new song is picked. When set to true, the picker view will display, all songs, artists, playlists, albums, etc. will have a + symbol next to them, the user can select as many as they want, and there will be a "Done" button they can press to dismiss the view and load the items they have chosen.
Now let's get back to that IBAction, buttonPick. Within that action, all we are doing is bringing up the media picker controller view:
We then need to create a function for when the user is done selecting the music they want to play. Again, when allowsPickingMultipleItems is set to false the following function will be called after one song is selected. If it's set to true, the function will be called after the "Done" button is pressed. What this function needs to do is dismiss the view, tell the app what songs were selected, create a queue of those songs, and start playing them:
And that's it for all the IBActions! At this point, you should be able to load the app to your device, select some songs to play, and control the playback of those songs.
Controlling the device's volume
When I first set out on this project, controlling the volume of the device was one of the hardest things to find a solution for. It is possible to add another slider to the storyboard and control the volume with it, BUT, this only works when controlling the sound of something coming from within the app in relation to the device's volume. Adding a slider via storyboard cannot, as far as I have found, control the volume of the device itself, which is what we need to do here. The solution I found requires a "wrapper view" which needs to be added programmatically. You also have to manually enter the frame of this view, which, for me, required a lot of trial and error and I could only make it look good for one device at a time and only for one orientation,
However, while reworking the app for this tutorial, I found a slightly more graceful solution that allows for the use of the storyboard and can have constraints applied to it. This is where the container view comes in.
As you can see, adding the container view to the first view controller creates an additional view controller with the same size and shape as the container view. Whatever we do in this new view controller will display within the container view of the original view controller.
In order to control what happens in the new view controller, we need to create a new cocoa touch class. Mine is called "VolumeViewController".
We also need to connect the new view controller to the class.
Within the new .swift file, we only need to add 2 lines of code into viewDidLoad. We need to create view for the volume slider, then add the subview to the main view controller.
As I mentioned before, I originally had to enter the x position, y position, width, and height of the MPVolumeView frame, but now we can just set it to the frame of the view controller, which will match the frame of the container view that we've set up in the storyboard.
Important: Make sure the "Clip Subviews" option is selected for the VolumeViewController. Without this, at least on my device, the volume slider extended beyond the view of the screen.
And that's it! You can now load the app to your device, pick songs, control playback, progress through the song with the slider, and control the volume of the device!
The full github project can be found here