Creating and Exporting a CSV File In Swift

A lot of apps offer options for the user to export data. I've even offered it as an option in my MPG Tracking app as a paid feature. One common way to do this is with a .csv file, so let's see how that's done.

HOW A CSV FILE WORKS

CSV Stands for Comma Separated Values. A .csv file is a text-based file where each value is separated by a comma. When the file is opened by a program like Excel, the software reads the data and separates each value into its own cell.

One row of data in a csv file would look like this - "Date,Task,Time Started,Time Ended"
You might want the second row to look like this - "9/12,Designing,09:35,11:02"

In swift, this would be written as a simple string - "Date,Task,Time Started,Time Ended\n9/12,Designing,09:35,11:02"
As you can see, creating a new line requires "\n". No commas or spaces are required. 

Ok cool, but can you give us a practical example?
Sure, but let's go over some important (but quick) steps first.

You need to create a filename and create a path to the file in the temporary directory.

let fileName = "Tasks.csv"
let path = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent(fileName)

Then you want to create the initial text for the csv file

var csvText = "Date,Task,Time Started,Time Ended\n"

As you can see, I've included the line break "\n" at the end of the text. It's good to either always include it at the beginning or the end of each line as long as you are consistent.

In this scenario, we would then do a loop of tasks completed within the app in order to create a new line of data for for each task and add it to the csv file.

for task in tasks {
    let newLine = "\(task.date),\(task.name),\(task.startTime),\(task.endTime)\n"
    csvText.appendContentsOf(newLine)
}

For the final step of creating the csv file, we need to actually write the file to the path we created earlier.

do {
    try csvText.writeToURL(path, atomically: true, encoding: NSUTF8StringEncoding)
} catch {
    print("Failed to create file")
    print("\(error)")
}

Great! You've created a csv file! Well now you have to decide what to do with it. You can email it, save it, open it in another app, etc. I'm going to show you how to email it.

EXPORTING YOUR CSV FILE

Note: This may not work in simulator

Using the activity view controller is actually very simple and only requires 2 lines

let vc = UIActivityViewController(activityItems: [path], applicationActivities: [])
presentViewController(vc, animated: true, completion: nil)

You can also choose to exclude certain options from showing up as a share option like so:

vc.excludedActivityTypes = [
    UIActivityTypeAssignToContact,
    UIActivityTypeSaveToCameraRoll,
    UIActivityTypePostToFlickr,
    UIActivityTypePostToVimeo,
    UIActivityTypePostToTencentWeibo,
    UIActivityTypePostToTwitter,
    UIActivityTypePostToFacebook,
    UIActivityTypeOpenInIBooks
]

FULL EXAMPLE

@IBAction func export(sender: AnyObject) {
        
        let fileName = "\(currentCar.nickName).csv"
        let path = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent(fileName)
        
        var csvText = "Make,Model,Nickname\n\(currentCar.make),\(currentCar.model),\(currentCar.nickName)\n\nDate,Mileage,Gallons,Price,Price per gallon,Miles between fillups,MPG\n"
        
        currentCar.fillups.sortInPlace({ $0.date.compare($1.date) == .OrderedDescending })
        
        let count = currentCar.fillups.count
        
        if count > 0 {
            
            for fillup in currentCar.fillups {
                
                let dateFormatter = NSDateFormatter()
                dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
                let convertedDate = dateFormatter.stringFromDate(fillup.date)
                
                let newLine = "\(convertedDate),\(fillup.mileage),\(fillup.gallons),\(fillup.priceTotal),\(fillup.priceGallon),\(fillup.mileDelta),\(fillup.MPG)\n"
                
                csvText.appendContentsOf(newLine)
            }
            
            do {
                try csvText.writeToURL(path, atomically: true, encoding: NSUTF8StringEncoding)
                                
                let vc = UIActivityViewController(activityItems: [path], applicationActivities: [])
                vc.excludedActivityTypes = [
                    UIActivityTypeAssignToContact,
                    UIActivityTypeSaveToCameraRoll,
                    UIActivityTypePostToFlickr, 
                    UIActivityTypePostToVimeo,
                    UIActivityTypePostToTencentWeibo,
                    UIActivityTypePostToTwitter,
                    UIActivityTypePostToFacebook,
                    UIActivityTypeOpenInIBooks
                ]
                presentViewController(vc, animated: true, completion: nil)

            } catch {
                
                print("Failed to create file")
                print("\(error)")
            }
            
        } else {
            showErrorAlert("Error", msg: "There is no data to export")
        }
    }

And this is what it looks like

SEND YOUR CSV FILE IN A CUSTOM EMAIL

Note: This may not work in simulator

First, you'll need to get set up for sending emails. Import MessageUI and add MFMailCompseViewControllerDelegate to your class.

import MessageUI

class ViewController: UIViewController, MFMailComposeViewControllerDelegate {

Then you need to get the email to pop up with some pre-set information:

if MFMailComposeViewController.canSendMail() {
    let emailController = MFMailComposeViewController()
    emailController.mailComposeDelegate = self
    emailController.setToRecipients([]) //I usually leave this blank unless it's a "message the developer" type thing
    emailController.setSubject("Here is your fancy email")
    emailController.setMessageBody("Wow, look at this cool email", isHTML: false)
}

Let's quickly run through what's happening here. First, we're checking if it's even possible to check mail by creating the if statement. Within the if statement, we're initializing the mail view controller and setting the delegate to self. From there, we can pre-load recipients, subject, and body. I have isHTML set to false, it shouldn't really matter if the message is only text.

But wait! We still need to do 2 things before we can send this email. We need to attach the csv file and get the email to actually pop up onto the screen. Go back into the if statement from before and add the following two lines

emailController.addAttachmentData(NSData(contentsOfURL: path)!, mimeType: "text/csv", fileName: "Tasks.csv")
                    
presentViewController(emailController, animated: true, completion: nil)

And that's all we need in order to create and email a csv file except for one small piece. With the code as it is, when you send or cancel the email, it will not dismiss the screen and the email will be visible forever until you close the app, so now we have to add a function to tell the app what do do when the user is done with the email view controller.

func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) {
        controller.dismissViewControllerAnimated(true, completion: nil)
    }

 

FULL EXAMPLE

@IBAction func export(sender: AnyObject) {
        
        let fileName = "\(currentCar.nickName).csv"
        let path = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent(fileName)
        
        var csvText = "Make,Model,Nickname\n\(currentCar.make),\(currentCar.model),\(currentCar.nickName)\n\nDate,Mileage,Gallons,Price,Price per gallon,Miles between fillups,MPG\n"
        
        currentCar.fillups.sortInPlace({ $0.date.compare($1.date) == .OrderedDescending })
        
        let count = currentCar.fillups.count
        
        if count > 0 {
            
            for fillup in currentCar.fillups {
                
                let dateFormatter = NSDateFormatter()
                dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
                let convertedDate = dateFormatter.stringFromDate(fillup.date)
                
                let newLine = "\(convertedDate),\(fillup.mileage),\(fillup.gallons),\(fillup.priceTotal),\(fillup.priceGallon),\(fillup.mileDelta),\(fillup.MPG)\n"
                
                csvText.appendContentsOf(newLine)
            }
            
            do {
                try csvText.writeToURL(path, atomically: true, encoding: NSUTF8StringEncoding)
                
                if MFMailComposeViewController.canSendMail() {
                    let emailController = MFMailComposeViewController()
                    emailController.mailComposeDelegate = self
                    emailController.setToRecipients([])
                    emailController.setSubject("\(currentCar.nickName) data export")
                    emailController.setMessageBody("Hi,\n\nThe .csv data export is attached\n\n\nSent from the MPG app: http://www.justindoan.com/mpg-fuel-tracker", isHTML: false)
                    
                    emailController.addAttachmentData(NSData(contentsOfURL: path)!, mimeType: "text/csv", fileName: "\(currentCar.nickName).csv")
                    
                    presentViewController(emailController, animated: true, completion: nil)
                }

            } catch {
                
                print("Failed to create file")
                print("\(error)")
            }
            
        } else {
            showErrorAlert("Error", msg: "There is no data to export")
        }
    }
    
    func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) {
        controller.dismissViewControllerAnimated(true, completion: nil)
    }

And this is what it looks like

CNContact Tutorial: Retrieve information from a user's address book in iOS 9 and Swift 2.2

The app I'm currently working on asks the user to enter an emergency contact, including name, phone number, email address, and relationship. I thought it would be easier if I could let the user choose a contact from their address book and have that pre-fill the UITextFields since this would give them the option to either manually enter the information or choose a contact and have the ability to edit the information as needed.
 

Getting Started

The first thing you need to do is set up a single page project. You can call it whatever you want, mine is called "CNContacts Example" I set up the view controller like this with 1 button, 8 labels, and 8 textfields:

I then set up the following outlet references:

As you can see, we added references for every textfield: Name, Phone 1-3, Email 1-3, and Relationship. We also added references to the labels for Phone 1-3 and Email 1-3, this will come into play later. You can also name these whatever you want, but it will be easier to transfer the code if you use the same names,

There are two frameworks that we will be using in this project, Contacts and ContactsUI. In your ViewController, above the class, enter the following code:

  import Contacts  
  import ContactsUI  

You will also wanted to add CNContactPickerDelegate to your class, by adding the following:

  class ViewController: UIViewController, CNContactPickerDelegate { 

 

The Code

The first thing you want to happen when someone opens your app for the first time is for the user to grant permission to access their contacts. You can do this by adding the following function to your code:

    func askForContactAccess() {  
      let authorizationStatus = CNContactStore.authorizationStatusForEntityType(CNEntityType.Contacts)  
        
      switch authorizationStatus {  
      case .Denied, .NotDetermined:  
        self.contactStore.requestAccessForEntityType(CNEntityType.Contacts, completionHandler: { (access, accessError) -> Void in  
          if !access {  
            if authorizationStatus == CNAuthorizationStatus.Denied {  
              dispatch_async(dispatch_get_main_queue(), { () -> Void in  
                let message = "\(accessError!.localizedDescription)\n\nPlease allow the app to access your contacts through the Settings."  
                let alertController = UIAlertController(title: "Contacts", message: message, preferredStyle: UIAlertControllerStyle.Alert)  
                  
                let dismissAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default) { (action) -> Void in  
                }  
                  
                alertController.addAction(dismissAction)  
                  
                self.presentViewController(alertController, animated: true, completion: nil)  
              })  
            }  
          }  
        })  
        break  
      default:  
        break  
      }  
    }  

You're going to get an error from "contactStore", go ahead and add the following within the ViewController class (I entered it below the label references):

    var contactStore = CNContactStore()  

Now in order to get the askForContactAccess to actually happen, you need to add "self.askForContactAccess()" to the viewDidLoad function so it looks like this:

    override func viewDidLoad() {  
      super.viewDidLoad()  
        
      self.askForContactAccess()  
        
    } 

The next thing we need to is add an action for our button. The button will pull up the user's list of contacts. Within the IBAction, add the following code:

     let contactPicker = CNContactPickerViewController()  
     contactPicker.delegate = self  
     self.presentViewController(contactPicker, animated: true, completion: nil)

Within this code, you are declaring the ViewController as a delegate to present the "contactPicker" view controller, which will display the user's list of contacts.

As of right now, we can bring up the contact list, but nothing will happen when one is selected. Now we need to create the actual function "contactPicker" to tell the app what to do when a contact is selected. Add the following code the ViewController:

 func contactPicker(picker: CNContactPickerViewController, didSelectContact contact: CNContact) {  
     NSNotificationCenter.defaultCenter().postNotificationName("addNewContact", object: nil, userInfo: ["contactToAdd": contact])  
       
 }  

Great, we he have the function! Now what?
In order to fill these fields, we need to know how to get 4 pieces of information

  1. First name
  2. Last name
  3. Phone numbers
  4. Email addresses

Let's look it what happens when you pull a contact's information. This function calls an item called "contact". If the only code you entered into the above function was "print(contact)", the log would print something like this:

<CNContact: 0x7fbe721302a0: identifier=EF087737-DE17-4C30-84DB-B997D59E13E2, givenName=Kate, familyName=Bell, organizationName=Creative Consulting, phoneNumbers=(    "<CNLabeledValue: 0x7fbe70513a50: identifier=44F2A168-0C77-4C5C-9905-921B83CEDCBB, label=_$!<Mobile>!$_, value=<CNPhoneNumber: 0x7fbe70523650: countryCode=us, digits=5555648583>>",    "<CNLabeledValue: 0x7fbe705012c0: identifier=77ABFC95-8916-4D3B-AF0C-ADE7088CEBD6, label=_$!<Main>!$_, value=<CNPhoneNumber: 0x7fbe70513780: countryCode=us, digits=4155553695>>"), emailAddresses=(    "<CNLabeledValue: 0x7fbe72166930: identifier=A86750FD-7496-4169-8DF8-09E8507B7BF9, label=_$!<Work>!$_, value=kate-bell@mac.com>",    "<CNLabeledValue: 0x7fbe7214b0d0: identifier=11188900-112A-422B-AA07-2365AABBB52C, label=_$!<Work>!$_, value=www.icloud.com>"), postalAddresses=(    "<CNLabeledValue: 0x7fbe70517380: identifier=5B898E66-F3FD-49F7-949C-35CB01294351, label=_$!<Work>!$_, value=<CNPostalAddress: 0x7fbe7051a8a0: street=165 Davis Street, city=Hillsborough, state=CA, postalCode=94010, country=, countryCode=us, formattedAddress=(null)>>")>

As you can see, this shows information for name, emails, phone number, and mailing address (which I'm not currently interested in) But how do we turn this into usable information?
 

Pulling information from the contact data

To get the name information

To get the first name: contact.givenName - This returns "Kate"
To get the last name: contact.familyName - This returns "Bell"
To turn this into a full name: contact.givenName + " " + contact.familyName - This returns "Kate Bell"

self.txtName.text = contact.givenName + " " + contact.familyName  

That was fairly easy, wasn't it? Well getting the phone numbers and email addresses is a little trickier, so let's go ahead and break those down.

To get the phone number(s)

You can get the contact's phone information by calling contact.phoneNumbers, which looks like this:

[<CNLabeledValue: 0x7f91dab8fef0: identifier=44F2A168-0C77-4C5C-9905-921B83CEDCBB, label=_$!<Mobile>!$_, value=<CNPhoneNumber: 0x7f91dab0be10: countryCode=us, digits=5555648583>>, <CNLabeledValue: 0x7f91dab09c30: identifier=77ABFC95-8916-4D3B-AF0C-ADE7088CEBD6, label=_$!<Main>!$_, value=<CNPhoneNumber: 0x7f91daba30c0: countryCode=us, digits=4155553695>>]

This is an array array of 2 phone numbers along with any relevant information, so the first step is to pull one of numbers out of the array with contact.phoneNumbers[0], which will look like:

<CNLabeledValue: 0x7fcb3ad73cd0: identifier=44F2A168-0C77-4C5C-9905-921B83CEDCBB, label=_$!<Mobile>!$_, value=<CNPhoneNumber: 0x7fcb3ad55350: countryCode=us, digits=5555648583>>

What we're looking for here is the 'digits' field. We can get this with "(contact.phoneNumbers[0].value as!CNPhoneNumber).valueForKey("digits")"

self.txtP1.text = (contact.phoneNumbers[0].value as! CNPhoneNumber).valueForKey("digits") as? String

you can then repeat this as txtP2.text = contact.phoneNumbers[1], etc.

However, if there are no phone numbers, reading contact.phoneNumbers[0] will cause a crash. If there is only one phone number, reading contact.phoneNumbers[1] will cause a crash, and so on and so forth, so we need to make sure the numbers exist before we actually send them to the text field.

     if contact.phoneNumbers.count > 0 {  
       self.txtP1.text = (contact.phoneNumbers[0].value as! CNPhoneNumber).valueForKey("digits") as? String  
     } else {  
       self.txtP1.text = ""  
     }  

"if contact.phoneNumbers.count > 0" checks that there is at least 1 item in the array before performing the code. Because my app uploads textfield data to a server, I personally needed to enter the self.P1.text = "" into the 'else' section so it wouldn't be empty, but this is unnecessary if you are simply displaying the information.

Changing the labels

I also wanted to change the labels from "Phone 1", "Phone 2"... to "Home", "Cell", or whatever type of phone number it was.

We can achieve this with "contact.phoneNumbers[0].label", which, in this case, returns:

_$!<Mobile>!$_

So now we just have to get rid of those extra characters. I was able to separate "Mobile" with the following code:

       let pLabel = contact.phoneNumbers[0].label //_$!<Mobile>!$_  
       let pLabel2 = pLabel.characters.split("<").map(String.init) //[_$!<, Mobile>!$_]  
       let pLabel3 = pLabel2[1].characters.split(">").map(String.init) //[Mobile, >!$_]  
       self.lblP1.text = pLabel3[0] //Mobile  

Additionally, in the else section, I added "self.lblP1.text = "Phone 1". If the user chooses a contact that has a phone number, and then chooses another contact that does not have a phone number, this returns the label to the original text.

So now, the entire text for the phone number looks like this:

     if contact.phoneNumbers.count > 0 {  
       self.txtP1.text = (contact.phoneNumbers[0].value as! CNPhoneNumber).valueForKey("digits") as? String  
       let pLabel = contact.phoneNumbers[0].label //_$!<Mobile>!$_  
       let pLabel2 = pLabel.characters.split("<").map(String.init) //[_$!<, Mobile>!$_]  
       let pLabel3 = pLabel2[1].characters.split(">").map(String.init) //[Mobile, >!$_]  
       self.lblP1.text = pLabel3[0] //Mobile  
     } else {  
       self.txtP1.text = ""  
       self.lblP1.text = "Phone 1"  
     }  

This can then be repeated with slight modifications for phone 2 and 3.

To get the email(s)

This will be similar to the phone numbers but I will still break it down a bit.

contact.emailAddresses returns:

[<CNLabeledValue: 0x7fccd3e2ad30: identifier=A86750FD-7496-4169-8DF8-09E8507B7BF9, label=_$!<Work>!$_, value=kate-bell@mac.com>, <CNLabeledValue: 0x7fccd3d58a30: identifier=11188900-112A-422B-AA07-2365AABBB52C, label=_$!<Work>!$_, value=www.icloud.com>]

Now I should mention that the contact I am using in this tutorial is one of the pre-loaded contacts in the simulator. If you read the above results, you will see that there are 2 work emails listed, one of which is "www.icloud.com"". I don't know why, it just is. I chose this contact for the tutorial because it is the only pre-loaded contact with multiple phone entries and multiple email entries.

contact.emailAddresses[0] returns:

<CNLabeledValue: 0x7fc1024a6b50: identifier=A86750FD-7496-4169-8DF8-09E8507B7BF9, label=_$!<Work>!$_, value=kate-bell@mac.com>

To get the email address into the textfield:

       self.txtE1.text = "\(contact.emailAddresses[0].value)" 

the rest is exactly the same as the phone numbers, so your code for Email 1 should look like this:

     if contact.emailAddresses.count > 0 {  
       self.txtE1.text = "\(contact.emailAddresses[0].value)"  
       let eLabel = contact.emailAddresses[0].label //_$!<Work>!$_  
       let eLabel2 = eLabel.characters.split("<").map(String.init) //[_$!<, Work>!$_]  
       let eLabel3 = eLabel2[1].characters.split(">").map(String.init) //[Work, >!$_]  
       self.lblE1.text = eLabel3[0] //Work  
     } else {  
       self.txtE1.text = ""  
       self.lblE1.text = "Email 1"  
     }  

And that's it! Just repeat the code and make the necessary adjustments for Email 2 and Email 3.
 

What your final result should look like:

The full project can be found on GitHub here.

MPMedia Tutorial: Control Music Playback In iOS 9 with Swift


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.

Storyboard Setup

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.

 

The Code

First, you'll need to import the media framework:

import MediaPlayer

You'll also need to create variables for the media player and for the timer function so you can call them later

let mp = MPMusicPlayerController.systemMusicPlayer()
var timer = NSTimer()

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:

mp.prepareToPlay()
Next you'll need to set up the timer, this will come in handy when a song is playing
self.timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(ViewController.timerFired(_:)), userInfo: nil, repeats: true)
self.timer.tolerance = 0.1

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.
 

mp.beginGeneratingPlaybackNotifications()
 
NSNotificationCenter.defaultCenter().addObserver(self, selector:#selector(ViewController.updateNowPlayingInfo), name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: nil)
This will call the updateNowPlayingInfo function, which, again, we'll see later.

 

Updating the labels

Let's create the timerFired function from before.

 func timerFired(_:AnyObject) {
 
 }
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:
if let currentTrack = MPMusicPlayerController.systemMusicPlayer().nowPlayingItem {
 
}


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.

 let trackName = currentTrack.title!
 
 let trackArtist = currentTrack.artist!
 
 let albumImage = currentTrack.artwork?.imageWithSize(imageAlbum.bounds.size)
 
 let trackDuration = currentTrack.playbackDuration
 
 let trackElapsed = mp.currentPlaybackTime
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.
imageAlbum.image = albumImage

We can also set the label above the slider to display the song's artist and title.

labelTitle.text = "\(trackArtist) - \(trackName)"
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.
let trackDurationMinutes = Int(trackDuration / 60)
 
let trackDurationSeconds = Int(trackDuration % 60)
 
labelDuration.text = "Length: \(trackDurationMinutes):\(trackDurationSeconds)"


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:

if trackDurationSeconds < 10 {
    labelDuration.text = "Length: \(trackDurationMinutes):0\(trackDurationSeconds)"
} else {
    labelDuration.text = "Length: \(trackDurationMinutes):\(trackDurationSeconds)"
}
Great! Now we have all our bases covered. The code for labelElapsed will look exactly the same:
let trackElapsed = mp.currentPlaybackTime
 
let trackElapsedMinutes = Int(trackElapsed / 60)
 
let trackElapsedSeconds = Int(trackElapsed % 60)
 
if trackElapsedSeconds < 10 {
    labelElapsed.text = "Elapsed: \(trackElapsedMinutes):0\(trackElapsedSeconds)"
} else {
    labelElapsed.text = "Elapsed: \(trackElapsedMinutes):\(trackElapsedSeconds)"
}


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:

let trackRemaining = Int(trackDuration) - Int(trackElapsed)
 
let trackRemainingMinutes = trackRemaining / 60
 
let trackRemainingSeconds = trackRemaining % 60
 
if trackRemainingSeconds < 10 {
    labelRemaining.text = "Remaining: \(trackRemainingMinutes):0\(trackRemainingSeconds)"
} else {
    labelRemaining.text = "Remaining: \(trackRemainingMinutes):\(trackRemainingSeconds)"
}
Now that we've taken care of the labels, we need to handle the slider. We want the slider to have two functions:

  1. Move from left to right as the song progresses from beginning to end
  2. 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.
 

sliderTime.maximumValue = Float(trackDuration)
 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. 
 sliderTime.value = Float(trackElapsed)


That's it for the timerFired function.

Remember that updateNowPlayingInfo function we called in viewDidLoad? Add this function to the view controller:

func updateNowPlayingInfo(){
 
}
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
func updateNowPlayingInfo(){
 
    self.timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(ViewController.timerFired(_:)), userInfo: nil, repeats: true)
    self.timer.tolerance = 0.1
 
}


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.
 

@IBAction func sliderTimeChanged(sender: AnyObject) {
    mp.currentPlaybackTime = NSTimeInterval(sliderTime.value)
}
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:
@IBAction func buttonPlay(sender: AnyObject) {
    mp.play()
}
 
@IBAction func buttonPause(sender: AnyObject) {
    mp.pause()
}
 
@IBAction func buttonPrevious(sender: AnyObject) {
    mp.skipToPreviousItem()
}
 
@IBAction func buttonBeginning(sender: AnyObject) {
    mp.skipToBeginning()
}
 
@IBAction func buttonNext(sender: AnyObject) {
    mp.skipToNextItem()
}


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:

 @IBAction func buttonPrevious(sender: AnyObject) {
     if trackElapsed < 3 {
         mp.skipToPreviousItem()
     } else {
         mp.skipToBeginning()
     }
 }
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:

  1. The user clicks the "pick" buton
  2. An additional view comes up that shows the user all of their music
  3. They choose the song(s), playlist(s), artist(s), album(s) they want to play
  4. They click the "Done" button that is displayed in the music picker view
  5. 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:

class ViewController: UIViewController, MPMediaPickerControllerDelegate {

We then need to create a variable for MPMediaPickerController so we can easily refer to it later:

var mediapicker1: MPMediaPickerController!
Next, we go into the viewDidLoad and set up the media picker controller:
let mediaPicker: MPMediaPickerController = MPMediaPickerController.self(mediaTypes:MPMediaType.Music)
mediaPicker.allowsPickingMultipleItems = true
mediapicker1 = mediaPicker
mediaPicker.delegate = self

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:

self.presentViewController(mediapicker1, animated: true, completion: nil)
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:
 func mediaPicker(mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
     self.dismissViewControllerAnimated(true, completion: nil)
     let selectedSongs = mediaItemCollection
     mp.setQueueWithItemCollection(selectedSongs)
     mp.play()
 }

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.

let volumeSlider = MPVolumeView(frame: self.view.layer.bounds)
self.view.addSubview(volumeSlider)

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