How to make advanced CarPlay audio apps?
In this tutorial, you will be taught how to make more complex audio apps for CarPlay with more navigation tabs, more levels of hierarchy, and how to organize everything properly…
Even though I will cover in small portion basic’s of CarPlay implementation, this tutorial is more advanced. If you are not familiar with the CarPlay concept it’s maybe a good idea first to read official Apple documentation and then go back to my tutorial.
https://developer.apple.com/carplay/documentation/CarPlay-Audio-App-Programming-Guide.pdf
Let’s start with Setting up the project.
In your entitlement file, you have to set this key of type Boolean to YES. For those who don’t know how to create that file, just check any of Capabilities from the Xcode menu, delete all previously generated keys and add this one: com.apple.developer.playable-content
Adding more navigation tabs
First, you have to add UIBrowsableContentSupportsSectionedBrowsing key inside your Info.plist file, type of Boolean, and set it to YES.
Now, we are going to fight with IndexPaths. Everything in CarPlay hierarchy is built using IndexPaths.
I strongly recommend getting familiar with IndexPaths first. You can do it from here:
Conforming your ViewController with two CarPlay protocols MPPlayableContentDataSource and MPPlayableContentDelegate you will be able to present and play whatever you want.
As you can see from the image above, I set up CarPlay when the app runs, because I didn’t find the trigger which will tell me the user started using CarPlay… If you find that trigger please contact me.
Creating a separate class for storing data
If you want to create a basic CarPlay app with just one tab and want to present a list with playable items, do not read this part of the article.
If you want to make a more complex app I strongly recommend structuring your data properly. I will propose one solution, but it’s not set in stone.
Due to IndexPaths, from my perspective, the best way to store data for CarPlay is the dictionary.
I’ve made CarPlayData class and on top of it, I declared API properties, RxDisposables, and main dictionary.
That main dictionary is declared as [Int: RootData]
RootData is a separate class that represents the data structure of the entire app. It’s really abstract when I said like that but I will give you an example and we will get back to the RootData class.
Let’s assume that we have a radio app. It consists of two navigation tabs Home and Search.
This will be a standard user flow. Taping on Search to explore radio stations by country. After selecting e.g. the Netherlands as a wanted country, the user will see all Netherland’s radio stations.
As you can see we have container items e.g. “Explore by country” since that item contains more items inside itself. It is Serbia, Germany, Netherlands… On the other hand, we have playable items e.g. Radio A, Radio B, Radio C… With these two kinds of items, we will make the entire app.
Now let’s get back to our RootData class. Since it’s in the dictionary [Int: RootData], each key/value pair will represent one navigation tab.
Let’s recapitulate we have 3 classes: RootData, ContainerItem, and PlayableItem model class. RootData class will contain (has reference) to the ContainerItem and PlayableItem class.
MPPlayableContentDataSource
This class is in charge of presenting. It’s very similar to Collection or Table View DataSource’s protocol.
In the first method, you will return how many items you want to display for specific IndexPath, in the second method, you will create MPContentItem for that specific IndexPath and populate it with data. That’s all folks!
Also, there is one specific method if you want to start prefetching something.
Let’s deep dive into numberOfChildItems method
If you receive empty IndexPath, that is for a number of navigation tabs.
With that, we set 2 navigation tabs. Home and MyProfile, for example.
You should just continue in this way.
Keep in mind that you first have to check how many indices current IndexPath has, otherwise your app will crash!
So you can’t avoid these indexPath.indices.count == 1… I know it’s boring, but that’s how Apple said we have to do.
Keep in mind that Apple allows you up to 5 hierarchy levels. But in some cases, you will be able to go only to the 3rd level. For example, some car manufacturers have a rule when a car is in the move, CarPlay is able to show max 3 levels of depth, no matter how many levels your app has. Keep that in mind during development.
Creating ContentItem
Try to open Apple documentation and read it in parallel with my article.
The main thing here is Boolean isContainer. This flag will determine whether we have playableItem or containerItem. Just by setting true or false.
I suggest you creating helper methods since they will reduce your boiler-plate code a lot.
BeginLoadingChildItems is a little bit tricky
This is provided so that applications can begin asynchronous batched loading of content before MediaPlayer begins asking for content items to display.
Nice sentence, but there is a gimmick. I have been struggling with implementing this method for the whole week. I kept getting this same crash:
At first glance I said, oh okay, I should just return completionHandler(nil) at the end of the method and everything will be ok. It actually was, there weren’t crashes anymore.
But there were two pitfalls of that approach. First, I didn’t have a loading indicator anymore. Second, I was getting blank screens without any data. For example, the user opens a page and there is no data there. After 10 seconds by going back and return again, data will be there. I didn’t want that. So I start digging… Anddd I’ve found something really funny. Please follow these three simple pieces of advice.
- Start fetching data for a particular indexPath. Make sure it is synchronous call since we want to “freeze” application until data or error message is shown. In the meantime when we are waiting for data the loading indicator will be shown.
- After receiving the data call completionHandler(nil). The loading indicator will be removed and data will be shown.
- After receiving the error call completionHandler(error). An error message will be shown. From my perspective, it’s valuable for the user to see an error message. Maybe it’s just up to the internet connection.
MPPlayableContentDelegate
We get to the part when we want to PLAY something.
The important thing to know is that you don’t need to create a separate player for CarPlay. Instead, car play uses the same player and commands from the lock screen of your iPhone. So if you have implemented MPRemoteCommandCenter, previously, you will just have to provide proper data to it.
For those who didn’t, read this article: https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter
When it comes to playing content I’ve decided to go in this way.
The user will see next and previous commands on CarPlay. You could disable them, per se, but it’s more user friendly to empower them. Keep in mind that the user is probably driving his/her car and it’s not very convenient for him/her to keep switching between screens in your CarPlay app.
By playing an item from one specific screen, I will put all items from that screen into one array. Afterward, when the user taps next or previous command I will just pass the different index to that array and switch radio stations.
To display player on CarPlay simulator from your XCode, you have to add the following code inside the method mentioned above: initiatePlaybackOfContentItemAt.
I hope you enjoyed reading my article, if you have any questions don’t hesitate to ask me. I look forward to helping you. Reach out for me on LinkedIn or via email.
bratislav.baljak@gmail.com
Have a nice day! :)