Build

Radio Station App using Apple Music & iTunes Search API’s

Michael Carroll on Jul 7, 2016
Radio Station App using Apple Music & iTunes Search API’s

With the steady rise of Apple Music, Apple has released an API allowing developers to check if a user is an Apple Music member, check which country the user account is associated with, queue up songs using song id's for playback and can look at playlists the user already has or create a new one. This opens up a vast amount of possibilities to developers to create exciting new music applications, especially with how prevalent digital music services are now in peoples lives.

What the Radio Station App Does

In this tutorial we'll create a real-time radio station. If a user is a Apple music member, they'll be able to search for songs with the iTunes search API to queue up for playback and DJ their own radio station. On top of that, they'll receive listener feedback through upvotes and downvotes in real time. A user will also be able to be a listener by connecting to a radio station and upvote or downvote that station.

Getting Started with the PubNub Swift SDK

We'll start off by including the SDK into our project. The easiest way to do this is with CocoaPods, which is a dependency manager for Swift projects. If you've never used CocoaPods before, check out how to install CocoaPods and use it!

After you have installed cocoapods, go into your project's root directory and type:

$ (your favorite editor) Podfile

Paste the following code into your Podfile:

source 'https://github.com/CocoaPods/Specs.git'

use_frameworks!

platform :ios, '9.0'

target 'YourProjectName' do

pod 'PubNub'

end

Finish it off by typing:

$ pod install

Now open the file YourProjectName.xcworkspace

App Setup in Account

You will need a PubNub account in order to get your own unique keys and to turn on some PubNub features we will need for this project. If you don't already have an account, go ahead and sign up for a free account to get your own publish and subscribe keys. Head over to the PubNub Developer Dashboard and create a new app, enable the Storage and Playback feature and the Presence feature.

Connect to PubNub from the AppDelegate

In order to use the PubNub features, we must create a PubNub instance property.

Open your AppDelegate.swift file and add this before the class declaration:

import PubNub

We will then create the PubNub instance, replace the “demo” keys with your own respective publish and subscribe keys in the parameters of the PNConfiguration.

class AppDelegate: UIResponder, UIApplicationDelegate, PNObjectEventListener {
    var window: UIWindow?
    lazy var client: PubNub = {
        let config = PNConfiguration(publishKey: "demo", subscribeKey: "demo")
        let pub = PubNub.clientWithConfiguration(config)
        return pub
    }()

Request App Access to Apple Music

Before anything, we need to request access to the user's Apple Music library and then check if their device is capable of Apple Music playback when they first open the app.

This is achieved using the SKCloudServiceController inside of the AppDelegate.swift file within the applicationDidBecomeActive function:

func applicationDidBecomeActive(application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
        //Ask user for for Apple Music access
        SKCloudServiceController.requestAuthorization { (status) in
            if status == .Authorized {
                let controller = SKCloudServiceController()
                //Check if user is a Apple Music member
                controller.requestCapabilitiesWithCompletionHandler({ (capabilities, error) in
                    if error != nil {
                        dispatch_async(dispatch_get_main_queue(), {
                            self.showAlert("Capabilites error", error: "You must be an Apple Music member to use this application")
                        })
                    }
                })
            } else {
                dispatch_async(dispatch_get_main_queue(), {
                    self.showAlert("Denied", error: "User has denied access to Apple Music library")
                })
            }
        }
    }

Search iTunes and display results

Once it's confirmed that the user is a Apple Music member, the searchItunes() function will use the iTunes Search API to make a GET request for whatever input the user provided from the searchBarSearchButtonClicked() function:

//Search iTunes and display results in table view
func searchItunes(searchTerm: String) {
    Alamofire.request(.GET, "https://itunes.apple.com/search?term=\(searchTerm)&entity=song")
    .validate()
    .responseJSON { response in
        switch response.result {
        case .Success:
            if let responseData = response.result.value as? NSDictionary {
                if let songResults = responseData.valueForKey("results") as? [NSDictionary] {
                    self.tableData = songResults
                    self.tableView!.reloadData()
                }
            }
         case .Failure(let error):
             self.showAlert("Error", error: error.description)
         }
      }
}
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
    //Search iTunes with user input
    if searchBar.text != nil {
        let search = searchBar.text!.stringByReplacingOccurrencesOfString(" ", withString: "+")
        searchItunes(search)
        searchBar.resignFirstResponder()
    }
}

Once we get this data, it will be displayed in a table view allowing the user to pick a track and add it to their playback queue:

//Display iTunes search results
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: nil)
    if let rowData: NSDictionary = self.tableData[indexPath.row] as? NSDictionary,
       urlString = rowData["artworkUrl60"] as? String,
       imgURL = NSURL(string: urlString),
       imgData = NSData(contentsOfURL: imgURL) {
       cell.imageView?.image = UIImage(data: imgData)
       cell.textLabel?.text = rowData["trackName"] as? String
       cell.detailTextLabel?.text = rowData["artistName"] as? String
    }
    return cell
}
//Add song to playback queue if user selects a cell
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let indexPath = tableView.indexPathForSelectedRow
    if let rowData: NSDictionary = self.tableData[indexPath!.row] as? NSDictionary, urlString = rowData["artworkUrl60"] as? String,
        imgURL = NSURL(string: urlString),
        imgData = NSData(contentsOfURL: imgURL) {
        queue.append(SongData(artWork: UIImage(data: imgData), trackName: rowData["trackName"] as? String, artistName: rowData["artistName"] as? String, trackId: String (rowData["trackId"]!)))
        //Show alert telling the user the song was added to the playback queue
        let addedTrackAlert = UIAlertController(title: nil, message: "Added track!", preferredStyle: .Alert)
        self.presentViewController(addedTrackAlert, animated: true, completion: nil)
        let delay = 0.5 * Double(NSEC_PER_SEC)
        let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
        dispatch_after(time, dispatch_get_main_queue(), {
            addedTrackAlert.dismissViewControllerAnimated(true, completion: nil)
        })
        tableView.deselectRowAtIndexPath(indexPath!, animated: true)
    }
}

Searching with the iTunes API and displaying the song data in a table view

Creating a Radio Station

After the user finishes adding tracks to their playback queue, they can create a radio station. A dialogue is presented for the user to name their radio station. This name is also used for their channel. A PubNub channel cannot contain a comma, colon, asterisk, slash or backslash, the createValidPNChannel() makes sure of this by deleting any of those characters in the name. It then gets the current timestamp to concatenate to the name so the channel will have a unique name.

//Create station name and segue to radio station if playback queue isn't empty
@IBAction func takeInputAndSegue(sender: AnyObject) {
    let alert = UIAlertController(title: "Name your radio station!", message: nil, preferredStyle: .Alert)
    alert.addTextFieldWithConfigurationHandler(nil)
    alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: { (action) -> Void in
    if !self.queue.isEmpty {
        let radioStationName = alert.textFields![0] as UITextField
    if !radioStationName.text!.isEmpty && radioStationName.text?.characters.count <= 60 {
        let stationName = radioStationName.text!
        //Adds a timestamp to the station name to make it a unique channel name
        let channelName = self.createValidPNChannel(stationName)
        //Publish station to a list of all stations created
        self.appDelegate.client.publish(["stationName" : stationName, "channelName" : channelName], toChannel: "All_Stations", withCompletion: { (status) in
            if status.error {
                self.showAlert("Error", error: "Network error")
            }
            self.appDelegate.client.subscribeToChannels([channelName], withPresence: true)
            dispatch_async(dispatch_get_main_queue(), {
                //Segue to the radio station
                let musicPlayerVC = self.storyboard?.instantiateViewControllerWithIdentifier("MusicPlayerViewController") as! MusicPlayerViewController
                musicPlayerVC.queue = self.queue
                musicPlayerVC.channelName = channelName
                self.navigationController?.pushViewController(musicPlayerVC, animated: true)
            })
          })
      } else {
          dispatch_async(dispatch_get_main_queue(), {
              self.showAlert("Try again", error: "Radio station name can't be empty or more than 60 characters")
          })
      }
   } else {
      dispatch_async(dispatch_get_main_queue(), {
          self.showAlert("Try again", error: "Playlist cannot be empty")
      })
   }
  }))
 self.presentViewController(alert, animated: true, completion: nil)
}
//Create unique PubNub channel by concatenating the current timestamp to the name of the radio station
func createValidPNChannel(channelName: String) -> String {
    let regex = try? NSRegularExpression(pattern: "[\\W]", options: .CaseInsensitive)
    var validChannelName = regex!.stringByReplacingMatchesInString(channelName, options: [], range: NSRange(0..<channelName.characters.count), withTemplate: "")
    validChannelName += "\(NSDate().timeIntervalSince1970)"
    validChannelName = validChannelName.stringByReplacingOccurrencesOfString(".", withString: "")
    return validChannelName
}

Selecting a song and adding it to a radio station

DJ the Radio Station

Now that the user has their own radio station, the tracks they added to their queue will begin to play. With PubNub's Presence feature, we can detect when a user has joined our channel. Once they do, we will send out the track data and current playback time so they can listen at the same playback position on their device.

//Listen if a user joins and and publish the trackId, currentPlaybackTime, trackName and artistName to the current channel
func client(client: PubNub, didReceivePresenceEvent event: PNPresenceEventResult) {
    var playbackTime: Double!
    if controller.currentPlaybackTime.isNaN || controller.currentPlaybackTime.isInfinite {
        playbackTime = 0.0
    } else {
        playbackTime = controller.currentPlaybackTime
    }
    if event.data.presenceEvent == "join" {
        appDelegate.client.publish(["trackId" : trackIds[controller.indexOfNowPlayingItem], "currentPlaybackTime" : playbackTime, "trackName" : queue[controller.indexOfNowPlayingItem].trackName!, "artistName" : queue[controller.indexOfNowPlayingItem].artistName!], toChannel:  channelName, withCompletion: { (status) in
            if status.error {
                self.showAlert("Error", error: "Network error")
            }
        })
    }
}

If the DJ skips forwards or backwards, we'll publish a message on the channel with the track data and current playback time again.

//Skip to the next track and publish the trackId, currentPlaybackTime, trackName and artistName to the current channel
    @IBAction func skipForwards(sender: AnyObject) {
        controller.skipToNextItem()
        if controller.indexOfNowPlayingItem < queue.count {
        trackName.text = queue[controller.indexOfNowPlayingItem].trackName
        artistName.text = queue[controller.indexOfNowPlayingItem].artistName
        appDelegate.client.publish(["trackId" : trackIds[controller.indexOfNowPlayingItem], "currentPlaybackTime" : controller.currentPlaybackTime, "trackName" : queue[controller.indexOfNowPlayingItem].trackName!, "artistName" : queue[controller.indexOfNowPlayingItem].artistName!], toChannel: channelName, withCompletion: { (status) in
            if !status.error {
                self.controller.play()
                dispatch_async(dispatch_get_main_queue(), {
                    self.thumbsUpCount.text = "0"
                    self.thumbsDownCount.text = "0"
                })
            } else {
                self.showAlert("Error", error: "Network error")
            }
        })
        }
    }
    
    //Skip to the previous track and publish the trackId, currentPlaybackTime, trackName and artistName to the current channel
    @IBAction func skipBackwards(sender: AnyObject) {
        controller.skipToPreviousItem()
        if controller.indexOfNowPlayingItem < queue.count {
        trackName.text = queue[controller.indexOfNowPlayingItem].trackName
        artistName.text = queue[controller.indexOfNowPlayingItem].artistName
        appDelegate.client.publish(["trackId" : trackIds[controller.indexOfNowPlayingItem], "currentPlaybackTime" : controller.currentPlaybackTime, "trackName" : queue[controller.indexOfNowPlayingItem].trackName!, "artistName" : queue[controller.indexOfNowPlayingItem].artistName!], toChannel: channelName, withCompletion: { (status) in
            if !status.error {
                self.controller.play()
                dispatch_async(dispatch_get_main_queue(), {
                    self.thumbsUpCount.text = "0"
                    self.thumbsDownCount.text = "0"
                })
            } else {
                self.showAlert("Error", error: "Network error")
            }
        })
        }
    }

The DJ is also listening for up and down vote messages on this channel to know if the listeners like what is playing.

//Update thumbs up and thumbs down counts
    func client(client: PubNub, didReceiveMessage message: PNMessageResult) {
        if "thumbsUp" == message.data.message!["action"] as? String {
            let count = Int(thumbsUpCount.text!)
            thumbsUpCount.text = String(count! + 1)
        } else if "thumbsDown" == message.data.message!["action"] as? String {
            let count = Int(thumbsDownCount.text!)
            thumbsDownCount.text = String(count! + 1)
        }
    }
Broadcasting the radio station to all users listening in real time

Listen to a Radio Station

To listen to a radio station, a user chooses from a table view of radio stations and they automatically subscribe to the channel from the cell they selected. In order to get these radio stations, we use PubNub's storage and playback feature to retrieve all of the radio stations that have been created.

override func viewDidAppear(animated: Bool) {
        stationNames.removeAll()
        channelNames.removeAll()
        //Go through the history of the channel holding all stations created
        //Update table view with history list
        appDelegate.client.historyForChannel("All_Stations") { (result, status) in
            for message in (result?.data.messages)! {
                if let stationName = message["stationName"] as? String, channelName = message["channelName"] as? String{
                    self.stationNames.append(stationName)
                    self.channelNames.append(channelName)
                }
            }
            dispatch_async(dispatch_get_main_queue(), {
                self.tableView.reloadData()
            })
        }
    }

Subscribing to a radio station in real time
Once a user selects a cell, the channel name and station name is passed to the next view.

//Segue to that radio station and pass the channel name and station name
    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let cell = tableView.cellForRowAtIndexPath(indexPath)
        let stationVC = self.storyboard?.instantiateViewControllerWithIdentifier("StationViewController") as! StationViewController
        stationVC.channelName = (cell?.detailTextLabel?.text)!
        stationVC.stationName = (cell?.textLabel?.text)!
        self.navigationController?.pushViewController(stationVC, animated: true)
    }

Now the user can subscribe to that channel with just one line of code.

override func viewDidAppear(animated: Bool) {
        appDelegate.client.addListener(self)
        appDelegate.client.subscribeToChannels([channelName], withPresence: true)
        self.title = "Radio station - \(stationName)"
    }

The user is also listening for messages on this channel with the addListener() function in viewDidAppear() above . As explained before, the DJ will send a message to whoever joins the channel. The user listening to the radio station will then listen for the incoming song data.

//Recieve song data to play
    func client(client: PubNub, didReceiveMessage message: PNMessageResult) {
        if let trackId = message.data.message!["trackId"] as? String, currentPlaybackTime = message.data.message!["currentPlaybackTime"] as? Double, trackName = message.data.message!["trackName"] as? String, artistName = message.data.message!["artistName"] as? String {
            controller.setQueueWithStoreIDs([trackId])
            controller.play()
            controller.currentPlaybackTime = currentPlaybackTime
            self.trackName.text = trackName
            self.artistName.text = artistName
            thumbsDownButton.backgroundColor = UIColor.clearColor()
            thumbsUpButton.backgroundColor = UIColor.clearColor()
        }
    }

They can also send their upvotes and downvotes by simply publishing to the channel.

//Publish a upvote to the subscribed channel
    @IBAction func thumbsUp(sender: AnyObject) {
        appDelegate.client.publish(["action" : "thumbsUp"], toChannel: channelName) { (status) in
            if !status.error {
                self.thumbsDownButton.backgroundColor = UIColor.clearColor()
                self.thumbsUpButton.backgroundColor = UIColor(red: 44/255, green: 62/255, blue: 80/255, alpha: 1.0)
            } else {
                self.showAlert("Error", error: "Network error")
            }
        }
    }
    
    //Publish a downvote to the subscribed channel
    @IBAction func thumbsDown(sender: AnyObject) {
        appDelegate.client.publish(["action" : "thumbsDown"], toChannel: channelName) { (status) in
            if !status.error {
                self.thumbsUpButton.backgroundColor = UIColor.clearColor()
                self.thumbsDownButton.backgroundColor = UIColor(red: 44/255, green: 62/255, blue: 80/255, alpha: 1.0)
            } else {
                self.showAlert("Error", error: "Network error")
            }
        }
    }

Listening to a radio station in real time

Making It Even Better

That's all there is to it! You are now able to become the DJ you've always wanted to be or see what other people are listening to and give your opinion on it. All in real time! You can easily expand on this by adding some extra features. You could restrict the range of your broadcasting with BLE (bluetooth low energy), provide stats on how well you're doing as a DJ in relevance to the amount of upvotes and downvotes you get or even add a chat to it so listeners could talk to whoever is the DJ.