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) } }
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 }
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) } }
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() }) } }
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") } } }
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.