Music has always been an oasis for me while coding and writing. I love chatting for hours on end with my friends, exploring our peculiar music taste. What if we had an app where we could listen to music and discuss with a like-minded community? This tutorial will create a music chat app where you can listen to your dearest music while sharing and chatting about it!
Note: This article's content is outdated.
In the meantime, check out our SwiftUI SDK, or get started with our new SwiftUI chat app tutorial.
We’ll use the Apple Music API to create our own music player. It helps us to access the user’s library and play songs from Apple Music using MusicKit.
For straightforward implementation of a chat feature, we’ll use Stream Chat SDK. It helps us seamlessly integrate messaging service in your application that you can use for education, e-commerce and more.
The SDK has a free trial, and it's free to use for small companies and hobby projects with a Maker Account.
We’ll learn how to-
- Setup Apple Music API
- Configure Apple Music API
- Stream Account Setup
- Setup Chat SDK in Xcode
- Design Home and Search Screen
- Design Login and Chat Screen
Note - You need a paid developer account, and Apple Music installed on your device to follow along this article.
Introduction
The app has a list of popular songs in US. You can play and search for songs. It also includes a chat screen to share your favourite music with your friends.
Please download the starter project and explore the contents under the Initial folder. It consists of helper views, a few extensions and the model for the API data.
So let’s get started!
Setup Apple Music API
The API gives access to the media in the Apple Music Catalog and the user's personal iCloud Music Library.
We use this service to retrieve information about albums, songs, artists, playlists, music videos, Apple Music stations, ratings, charts, recommendations, and the user's most recently played content.
MusicKit Identifier and Private Key
We need an identifier and private key to access the API. We create a JSON Web Token (JWT) to communicate to Apple Music.
Go to the Account section on https://developer.apple.com. Next, select Certificates, Identifiers & Profiles.
In the sidebar, select Identifiers. Next, click the Add button (+), choose MusicKit IDs, and then click Continue. Finally, enter TonesChat in the description and the identifier in a reverse-domain style.
music.com.rudrankriyam.TonesChat
Click Continue, and then Register.
Now, select Keys from the sidebar. We click the Add button (+), enter the Key Name as TonesChat, and enable the MusicKit service.
Click on Configure button. From the dropdown menu, select the Music ID that we configured in the previous step. Click Save. We’ll be taken back to the previous page. Click Continue and then Register to proceed further.
Following all the steps, we can download the key. We also need the Key ID mentioned on this page later.
Name:TonesChat
Key ID: 12AB3C4D56 // <-- Take a note of this Key ID
Services:MusicKit
Click on the Download button to finally retrieve the key. Save it somewhere where we can access it for generating the developer token.
Developer Token
We use the Key ID and private key to create a developer token for authenticating with Apple Music. The starter project contains SwiftJWTSample to generate the JSON Web Token.
After opening it, run the following command in terminal -
swift run generateToken path_to_keys/AuthKey.p8
For example,
rudrankriyam@MacBook-Pro ~ % cd /Users/rudrankriyam/Desktop/TonesChat/SwiftJWTSample
rudrankriyam@MacBook-Pro SwiftJWTSample % swift run generateToken ZYXWDS9V6JD 12AB3C4D56 /Users/rudrankriyam/Desktop/AuthKey_12AB3C4D56.p8
If everything is done correctly, we’ll receive the output containing the JSON Web Token. Copy the token, and save it for the next step.
Configure Apple Music API
With the more challenging part of creating the token completed, working with the Apple Music API is relatively straightforward.
- Create a new Swift file, and name it as AppleMusicManager. Next, we will create a singleton class
AppleMusicManager
to handle all the API requests.
class AppleMusicManager {
static let shared = AppleMusicManager()
// 1
private let developerToken = "JWT GOES HERE"
// 2
func getSearchSongs(for term: String) -> AnyPublisher<SearchSongModel, Error> {
execute(request: createURLRequest(with: .search(term: term)))
}
func getTopSongs() -> AnyPublisher<SongModel, Error> {
execute(request: createURLRequest(with: .chart))
}
private func execute<T: Codable>(request: URLRequest) -> AnyPublisher<T, Error> {
URLSession.shared
.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: T.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
private func createURLRequest(with endpoint: Endpoint) -> URLRequest {
var request = URLRequest(url: endpoint.url)
request.httpMethod = "GET"
request.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")
return request
}
}
Here’s what this class is doing:
- Create a constant to save the JWT token. Ideally, you should securely save this in Keychain.
- Create a request to search songs for a particular term. Similarly, request for the top songs.
We’ll use these two methods later for the home and search screen. Before moving on to the code, let’s first set up the Stream SDK.
Stream Account Setup
The iOS SDK of Stream Chat helps us to build our own chat experience for the app. We want to implement chat functionality in TonesChat for sharing our exceptional music taste with the world.
First, go to https://getstream.io and sign up for an account.
We’ll be redirected to the dashboard screen.
Click on the Create App button. Fill the list as follows -
- App Name: TonesChat.
- Feeds Server Location: Based on where you’re located.
- Chat Server Location: Based on where you’re located.
- Clone Existing App: ---
- Environment: Development.
Click on the Create App button.
On our dashboard screen, we can find the app created for us. Take note of the key, as it’ll be helpful later.
Now, click on TonesChat.
Next, from the navigation bar on the top, select Chat, and then select Overview.
Scroll down, and disable auth checks and permission checks for the purpose of this article.
Now, we’re ready to add the SDK to our app!
Stream Chat SDK in Xcode
In Xcode, select File, then **Swift Packages, and finally, choose Add Package Dependency. In the search field, add the following package location -
https://github.com/GetStream/stream-chat-swift.git
Click Next. The current version at the time of writing is 3.1.10. Click Next again and then checkmark both the products to add to the TonesChat target. Finally, click Finish to add the dependencies to the app.
The next step is to configure the SDK for our project. To initialise Stream, open the project and select TonesChatApp.swift file. Import StreamChat at the top of the file.
import StreamChat
Create an extension on ChatClient
to have a singleton object for our app. It is the root object representing a Stream Chat.
extension ChatClient {
static var shared: ChatClient!
}
Create an extension on TonesChatApp
and add setupStream()
method. Here, we configure the ChatClient
with the key that we created on the dashboard.
extension TonesChatApp {
private func setupStream() {
let config = ChatClientConfig(apiKeyString: "KEY")
ChatClient.shared = ChatClient(config: config, tokenProvider: .anonymous)
}
}
Inside TonesChatApp
, we create an initializer to set the chat client. The whole struct
looks like this -
@main@main
struct TonesChatApp: App {
init() {
setupStream()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
And that’s it to get started with the next step of designing the user interface!
Home Screen
The home screen will contain a grid of songs that we can play/pause, and copy the name of the song. For illustration, we’ll fetch the top 20 popular songs in the US.
Create a new file named SongsViewModel.swift and copy the class -
class SongsViewModel: ObservableObject {
var cancellable: AnyCancellable?
@Published var songs: [SongData] = []
func requestAuthorization(completion: @escaping (Bool) -> ()) {
SKCloudServiceController.requestAuthorization { (status) in
completion(status == .authorized ? true : false)
}
}
}
We’ve a variable to store the array of songs to be displayed. The requestAuthorization(completion:)
method asks the user for permission to access the music library on the device.
class TopSongsViewModel: SongsViewModel {
func updateTopSongs() {
requestAuthorization { status in
if status {
self.cancellable = AppleMusicManager.shared.getTopSongs()
.sink(receiveCompletion: { _ in
}, receiveValue: { model in
if let songs = model.results.songs.first {
self.songs = songs.data
}
})
}
}
}
}
Inheriting from SongsViewModel
, we create another class, TopSongsViewModel
, that updates the top songs. We get the user token and then fetch the required data using AppleMusicManager
.
With the core logic done, we move on to creating the grid layout to display the album art, song and artist name.
Create MusicCardView.swift and add the following content to it -
struct MusicGridView: View {
// 1
@ObservedObject var viewModel: SongsViewModel
// 2
var player = MPMusicPlayerController.applicationQueuePlayer
var items: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: items, spacing: 4) {
ForEach(viewModel.songs, id: \.id) { song in
MusicCardView(song: song)
// 3
.onTapGesture(count: 2) {
setSong(with: song.id)
}
// 4
.onLongPressGesture {
copyToClipboard(string: song.attributes.name)
}
}
}
.padding()
}
}
// 5
private func setSong(with id: String) {
switch player.playbackState {
case .stopped, .paused:
player.setQueue(with: [id])
player.play()
default:
player.stop()
}
successNotification()
}
private func copyToClipboard(string: String) {
UIPasteboard.general.string = string
successNotification()
}
private func successNotification() {
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
}
Figuring out the code, we -
- Create an
@ObservedObject
variable of the typeSongsViewModel
. This helps us to reuse this view in the home screen as well as the search screen. - Create an instance of the application queue player that helps us to play music locally within our app.
- Add a double-tap gesture to play/pause the song.
- Add a long gesture on the card. This method copies the name of the song to the phone’s clipboard. This helps us to directly share our music with the world.
- Use the music player to set the song based on its
id
and play/pause it accordingly.
Next, we piece all of them together to create the HomeView
-
struct HomeView: View {
// 1
@StateObject private var viewModel = TopSongsViewModel()
var body: some View {
NavigationView {
// 2
MusicGridView(viewModel: viewModel)
.navigationTitle("TonesChat")
}
.navigationViewStyle(StackNavigationViewStyle())
// 3
.onAppear {
viewModel.updateTopSongs()
}
}
}
Here's what's happening -
- Create a
@StateObject
forTopSongsViewModel()
to be alive for the whole lifecycle, becoming the ultimate source of truth. - We pass the instance of
TopSongsViewModel
as the parameter toMusicGridView
. - As soon as the view appears, we fetch the top songs firing the method
updateTopSongs()
.
Run the app to see a beautiful home screen.
You can play/pause some of the popular songs out there. With the home screen being completed, and working, it’s time for some searching!
Search Screen
Whenever anyone shares a song with us, we want to search that song in the whole catalogue of Apple Music, and if found, play it.
First, create SearchSongsViewModel.swift with the following class -
class SearchSongsViewModel: SongsViewModel {
func updateSearchSongs(for term: String) {
requestAuthorization { status in
if status {
self.cancellable = AppleMusicManager.shared.getSearchSongs(for: term)
.sink(receiveCompletion: { _ in
}, receiveValue: { model in
self.songs = model.results.songs.data
})
}
}
}
}
We use the method updateSearchSongs(for:)
to get the search term from the user. Then, after successful authorisation, we receive the list of the songs that matched.
To create the UI for the search, add SearchView
to SearchView.swift -
struct SearchView: View {
// 1
@State private var searchText = ""
@State private var showCancelButton: Bool = false
// 2
@StateObject private var viewModel = SearchSongsViewModel()
var body: some View {
NavigationView {
VStack {
// 3
SearchBar(searchText: $searchText, showCancelButton: $showCancelButton) {
UIApplication.shared.resignFirstResponder()
if self.searchText.isEmpty {
viewModel.songs = []
} else {
viewModel.updateSearchSongs(for: searchText)
}
}
// 4
MusicGridView(viewModel: viewModel)
}
.navigationTitle("Search")
.navigationBarHidden(showCancelButton)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Here, the code:
- Creates private
@State
variables for mutating the search text and to show/hide the cancel button. - Create a
@StateObject
forSearchSongsViewModel()
to be alive for the whole lifecycle even when the view recreates. - Adds the
SearchBar
accepts the search text, and on commit, calls theupdateSearchSongs(for:)
method from the view model. When the songs are fetched, it updates the@Published
variablesongs
, resulting in recreating the view. - Add the
MusicGridView()
that shows the search results in a grid.
Run the app and search for your treasure tune!
Now we can search for whatever songs our good friends recommend!
Login Screen
To enter the music paradise, we need to log in with our username first. So, for that, we create a login screen.
Create a new SwiftUI file named LoginHeaderView.swift and add the following struct
to it:
struct LoginHeaderView: View {
var body: some View {
VStack {
// 1
Image("login_header_image")
.resizable()
.aspectRatio(contentMode: .fit)
Text("Welcome to Music Paradise!")
.fontWeight(.black)
.foregroundColor(Color(.systemIndigo))
.font(.largeTitle)
.multilineTextAlignment(.center)
Text("Share your exceptional music taste with the world.")
.fontWeight(.light)
.multilineTextAlignment(.center)
.padding()
}
}
}
You can customise the LoginHeaderView
however you want. Create another file with the name LoginView.swift and add the following code to it -
import SwiftUI
import StreamChat
struct LoginView: View {
// 1
@State private var username: String = ""
@State private var success: Bool?
var body: some View {
NavigationView {
VStack {
Spacer()
// 2
LoginHeaderView()
VStack(alignment: .leading) {
Text("Username")
.font(.headline)
TextField("Enter username", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding()
Spacer()
// 3
NavigationLink(destination: ChatView(), tag: true, selection: $success) {
EmptyView()
}
Button("Log in".uppercased(), action: login)
.buttonStyle(AuthenticationButtonStyle())
}
}
}
private func login() {
// 4
ChatClient.shared.tokenProvider = .development(userId: username)
// 5
ChatClient.shared.currentUserController().reloadUserIfNeeded { error in
switch error {
case .none:
success = true
case .some:
success = false
}
}
}
}
// 5
struct AuthenticationButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color(.systemIndigo))
.cornerRadius(12)
.padding()
}
}
Here's the breakdown:
- We use the
@State
variables for mutating the username as well as successful authentication. - It is the code for the UI of the login screen. You can customise it however you want.
NavigationLink
to go to theChatView()
after successful authentication.- A Login button that calls the
login()
method to login the user. For the purpose of this article, we use the development provider since it doesn't require a token. - We fetch the token from
tokenProvider
and prepare the sharedChatClient
variable for the new user. If there’s no error, we update the value ofsuccess
totrue
. This results in the navigation to theChatView()
. - Custom
ButtonStyle
for the login button.
To get the project running, create a new SwiftUI file, and name it as ChatView.swift.
With the login screen done and the pathway to the music paradise cleared, let’s create the Chat screen!
Chat Screen
Stream’s Chat SDK makes it easier to implement chat functionality in our app in few tens of lines of code.
We start off by building our own custom MessageView
. It contains the username of other users and the message in a design similar to iMessage.
import SwiftUI
import StreamChat
struct MessageView: View {
// 1
var message: ChatMessage
var body: some View {
VStack(alignment: .leading) {
// 2
if !message.isSentByCurrentUser {
Text(message.author.id)
.font(.footnote)
.bold()
}
Text(message.text)
.foregroundColor(isSentByCurrentUser ? .white : .primary)
.padding()
.background(background)
.clipShape(ChatBubbleShape(isSentByCurrentUser: isSentByCurrentUser))
// 3
.onLongPressGesture(perform: copyToClipboard)
}
.padding(isSentByCurrentUser ? .trailing : .leading)
.frame(maxWidth: .infinity, alignment: isSentByCurrentUser ? .trailing: .leading)
}
private var background: Color {
isSentByCurrentUser ? .brand : Color(.systemGray5)
}
private var isSentByCurrentUser: Bool {
message.isSentByCurrentUser
}
private func copyToClipboard() {
UIPasteboard.general.string = message.text
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
}
Breaking down the code of MessageView
into essential parts -
ChatMessage
is a type representing a chat message. It is an immutable snapshot of a chat message entity at the given time.- If the message is not sent by the user, we display the other user’s name above the text message.
- On long press on a message, we copy to the phone’s clipboard. This helps us to directly search for the song shared with us.
Now open ChatView.swift, and add the following to it:
import SwiftUI
import StreamChat
struct ChatView: View {
// 1
@StateObject private var channel = ChatClient.shared.channelController(
for: ChannelId(type: .messaging, id: "general"))
.observableObject
@State private var text: String = ""
var body: some View {
VStack {
// 2
List(channel.messages, id: \.self) { message in
MessageView(message: message)
.scaleEffect(x: 1, y: -1, anchor: .center)
}
.scaleEffect(x: 1, y: -1, anchor: .center)
.offset(x: 0, y: 2)
HStack {
TextField("Type a message", text: $text)
Button(action: sendMessage) {
Image(systemName: "paperplane.circle.fill")
.accessibility(label: Text("Send"))
.font(.system(size: 24))
.foregroundColor(Color(.systemIndigo))
}
}
.padding()
}
.navigationBarTitle("Music Paradise", displayMode: .inline)
.onAppear {
// 3
channel.controller.synchronize()
}
}
// 4
private func sendMessage() {
if !text.isEmpty {
channel.controller.createNewMessage(text: text) { result in
switch result {
case .success(let response):
print(response)
case .failure(let error):
print(error)
}
}
text = ""
}
}
}
The chat screen forms the core of the app, so here's the detailed explanation:
- We create
@StateObject
for managing the channel. For this post, we'll create a single channel for messaging and name it paradise. To keep the article within limits, we'll have a single channel that the user joins as soon as they log in. We assign thechannel
variable to a channel controller. The controller is used for continuous data change observations (like getting new messages in the channel) and for quick channel mutations (like adding a member to a channel). - We create a
List
to list all the messages from the channel. When we receive new messages, the body of the view automatically recreates aschannel
is an observable object. synchronize()
asynchronously fetches the latest version of the data from the servers.- The
sendMessage()
method creates a new message locally and schedules it for sending.
This completes the chat screen. Create few different usernames and test the app out with your friends and family!
Main Screen
Wrapping up the design process, we add the three main views to ContentView
. It contains a TabView
with the three contains with their respective Label
.
struct ContentView: View {
var body: some View {
TabView {
HomeView().tabItem { TabViewItem(type: .home) }
SearchView().tabItem { TabViewItem(type: .search) }
LoginView().tabItem { TabViewItem(type: .chat) }
}
.accentColor(.brand)
}
}
Completing our app, run the app to enter your new world!
Conclusion
You can download the final project from the GitHub repository of TonesChat.
We finished building a glimpse of the opportunities Apple Music SDK and Stream Chat SDK provide. For example, adding a whole chat system into your app is now much more accessible, thanks to the open-source framework of Stream.
We only worked with one channel for this starter tutorial. You can experiment with adding more channels, each for different genres of music. You can also add reactions, as well as send attachments using the SDK.
I hope you enjoyed this tutorial! Also, if you found this article helpful, let us know!