Building a Spotify-like currently playing feature: Part 2 - Building the iOS player
You will need Xcode 10+ and CocoaPods installed on your machine. Some knowledge of the Xcode IDE will be helpful.
Introduction
In the previous part, we started by creating the API that will power the client applications. In this part, we will be adding realtime functionality to a prebuilt iOS client application. This will be a simple music player that will play songs and broadcast the current song, device, and the track position.
This will make it easy to switch listening between clients just like Spotify does with its music clients. Let’s get started.
Prerequisites
To follow along in this part, you must have the following requirements:
- Completed the first part of the series
- Have Xcode 10.x installed on your machine
- Have CocoaPods installed on your machine
- Knowledge of the Xcode IDE
If you have the requirements then let’s get started.
Setting up your project
Clone the project from GitHub. The iOS application is in the iOS-*
directory in the repository. One is the completed project and the other is the music player without realtime features (iOS_Base
). You can use the one without realtime to follow along and the other for reference.
Now that you have the project, open the .xcworkspace
file in Xcode. Here’s a quick explanation of the project.
There is a Podfile
in the root of the project. This file is used to define the dependencies of the application. To update or install the dependencies, close Xcode and run the following commands:
$ pod install
Note that sometimes you may need to clear your cache to get the latest version of your dependencies. If this happens, just run the following commands below:
$ pod cache clean --all
$ pod repo update
$ pod install
When your installation is complete, reopen the project using the Spot.xcworkspace
file in the root of your project.
In the project, there is a Main.storyboard
file that defines the scenes of the application, here is what it looks like:
The first is the home screen with a button to enter the application. The second scene is a navigation controller that has the third scene defined as the root controller. The root controller in the third scene just displays the tracks available. The final scene displays the currently playing track.
In the project, we have a Song
struct which is just a struct that we will use to store the song details when we fetch them from the API we built in the first part of the article. The struct looks like this:
import Foundation
struct Song: Codable {
let id: Int
let title: String
let cover: String
let duration: Int
let artist: String
var isPlaying: Bool? = false
}
Next, we have the PlaylistTableViewController
. This class is responsible for fetching the tracks list, playing the selected track, keeping a song seconds elapsed timer. In the same file, we also defined a small Duration
class that we will use for the elapsed timer.
The TrackViewController
is responsible for the view displaying the currently playing track. It displays the time elapsed and also has a few controls but only the Play/Pause button works to keep the tutorial focused.
In the assets, we have the stock mp3 file, which we got from bensounds.com. We also have a placeholder cover image. If you build and run the application in a simulator, you should have the music player but without any realtime features and it also plays the mp3 file.
Adding some extra features to the application
Now that we have the application running, let’s connect it to our backend API and add some extra features to the application.
To get started, make sure your Node.js application is still running in the background. To start the Node.js server, cd
to the root of the Node.js application and run the following command:
$ node index.js
When the server is running, we can begin.
The first thing we want to do is load the tracks list from the server. We will just use this so we can have multiple listed on the main page instead of the one. Note though that the sound we will play will always be the same.
Before we do anything, open your project’s info.plist
and make sure the setting to Allow Arbitrary Loads is set to YES.
Next, open the PlaylistTableViewController
class and at the top, import the Alamofire
library:
// File: PlaylistTableViewController.swift
// [...]
import Alamofire
Then, replace the contents of the populateTracks
method with the following code:
// File: PlaylistTableViewController.swift
// [...]
fileprivate func populateTracks() {
Alamofire.request("http://localhost:3000/tracks").validate().responseData { res in
guard res.result.isSuccess, let responseData = res.data else {
return print("Failed to fetch data from the server")
}
let decoder = JSONDecoder()
self.tracks = try! decoder.decode([Song].self, from: responseData)
self.tableView.reloadData()
}
}
Also, go to the tickTimer
method and replace the contents with the following code:
// File: PlaylistTableViewController.swift
// [...]
@objc fileprivate func tickTimer() {
if Duration.instance.freeze {
return
}
Duration.instance.count += 1
if Duration.instance.count > 1000 {
killTimer()
}
else if (playingDevice == deviceName) {
let params: Parameters = [
"device": deviceName,
"id": currentlyPlaying?.id ?? 0,
"position": Duration.instance.count,
]
Alamofire.request("http://localhost:3000/tick", method: .post, parameters: params)
.validate()
.responseData { _ in }
}
}
The code above makes sure that the tracks are loaded from the Node.js server instead of the hardcoded implementation we had previously.
Next, let’s add a simple indication to each track name to know which is currently playing from the list view. In the same PlaylistTableViewController
class, find and replace the following code as seen below:
// Replace:
cell.textLabel?.text = "🎶 \(track.title) - \(track.artist)"
// With:
let isPlaying = track.isPlaying ?? false
cell.textLabel?.text = "\(isPlaying ? "🎶" : "") \(track.title) - \(track.artist)"
Then in the tableView(``*_*
tableView:, didSelectRowAt indexPath:)
method, find and replace the method with the following:
// File: PlaylistTableViewController.swift
// [...]
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
startTimer()
if lastPlayed == nil {
lastPlayed = tracks[indexPath.row]
}
if sound.playing == false || currentlyPlaying == nil || currentlyPlaying?.id != lastPlayed?.id {
if let index = tracks.firstIndex(where: { $0.id == currentlyPlaying?.id }) {
tracks[index].isPlaying = false
}
tracks[indexPath.row].isPlaying = true
self.tableView.reloadData()
lastPlayed = currentlyPlaying
currentlyPlaying = tracks[indexPath.row]
if playingDevice == nil || playingDevice == deviceName {
playingDevice = deviceName
playSound()
resetTimer()
}
if timerStarted == false {
timer?.fire()
timerStarted = true
}
}
}
That should do the trick. Now let’s start adding realtime features.
Adding realtime features to the application
The first thing we want to do is connect the application to Pusher Channels. To do this, open the PlaylistTableViewController
file and in the viewDidLoad
method, add the following code:
// File: PlaylistTableViewController.swift
// [...]
func viewDidLoad() {
// [...]
pusherConnect()
}
Then add the following property and methods to the class as shown below:
// File: PlaylistTableViewController.swift
// [...]
var pusher: Pusher!
fileprivate func pusherConnect() {
pusher = Pusher(key: "PUSHER_KEY", options: PusherClientOptions(
host: .cluster("PUSHER_CLUSTER")
))
pusher.connect()
let channel = pusher.subscribe("spotmusic")
let _ = channel.bind(eventName: "tick") { [unowned self] data in
if let data = data as? [String: Any] {
self.handleTickEvent(data: data)
}
}
}
fileprivate func handleTickEvent(data: [String: Any]) {
guard let device = data["device"] as? String, device != deviceName else {
playingDevice = deviceName
return
}
guard let position = data["position"] as? Int else { return }
if playingDevice == deviceName {
killTimer()
}
playingDevice = device
setTimer(count: position)
startTimer()
pauseSound()
}
Replace the
PUSHER_*
placeholders with the values in your Pusher dashboard.
We are using a device name so we can know where the event is fired from. We don’t want an event fired from the same device to be handled by the same device.
At the top of the class, import the PusherSwift
library:
// File: PlaylistTableViewController.swift
// [...]
import PusherSwift
One last thing we need to make sure we do is ping the main server to update the tick
event. We will use this to broadcast the position of the currently playing track at all times the track is playing.
Track Controller
There is another controller called the TrackViewController
, which displays the song details when a song is tapped. We do not need to make any changes to this file but it could come in handy if you need to make some improvements to the application.
Great. Now let’s test our application.
Testing your application
Make sure the Node server is running in the background. You can run the server by running the command below in the root of the API project:
$ node index.js
Now, build your music application and run it . You should notice no major difference from how it was but if you log into your Pusher dashboard and look at the Debug Console for your application, you will notice there will be an event fired for every second the song plays.
Now that we have the ticker, let’s build another application that listens for these changes and displays it in realtime.
Conclusion
In this part of the tutorial, we have learned how we can use realtime features to enhance our music player. In the next part, we will consume this information from another player.
The source code is available on GitHub.
28 August 2019
by Neo Ighodaro