According to Apple's Documentation one should show an Activity Indicator to indicate something is loading or taking time to show on screen. This is reasonable, and I'd love to do just that...
I feel like I'm missing something obvious, but I cannot figure out how to show an activity indicator in a template of a CarPlay app as I don't have access to the view. I can only interact with the templates available.
If I were trying to match the image from Apple's docs, I'd assume I would need to use the listTemplate
and some how add the activity indicator to that.
Has anyone else come across this? Any help would be appreciated!
I asked this question during WWDC21 and got the following response.
If you are using CPListTemplate
, the system will invoke your -[CPListItem handler]
when the user taps the list item. The system provides a completion block to your handler, which you should be sure to invoke after your app finishes processing the user's selection. If your app does not immediately call the completion block, that's okay! Maybe your app needs to perform a network request that takes some time. The system will automatically add a spinner to your list item after a short delay if the completion block has not yet been called.
Another option could be to present a list template with a single list item that indicates the rest of the items are still loading/in-progress.
For the tab bar template in particular, you can update the tabs displayed after it's already been presented. For example, you could dynamically add one or more tabs if they absolutely cannot be presented immediately upon app launch.
Or, for a full-screen presentation, you could present a CPAlertTemplate
to indicate an error or loading state in your app.
I had the same issue as you - I couldn't find how to show a loading spinner when asynchronously loading content from an API. The Apple Docs weren't very helpful in trying to figure this out. After lots of testing, particularly on a real CarPlay device in a car, I have an answer.
For me, when requesting data in MPPlayableContentDataSource's beginLoadingChildItems(), if I don't call the beginLoadingChildItems()'s completion handler for a few seconds then a loading spinner shows on the CarPlay screen. So beginLoadingChildItems() is called by CarPlay, I make an API request and only when the request returns data (or fails) do I call beginLoadingChildItems()'s completion handler. Most of the time the API request returns pretty quickly and the loading spinner doesn't show. But when the API is slow (a few seconds due to spotty connection), the loading spinner will show.
My MPPlayableContentDataSource beginLoadingChildItems() looks like:
class CarPlayContentManager: NSObject, MPPlayableContentDataSource {
private let contentProvider = ContentProvider()
func beginLoadingChildItems(at indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) {
// Make a request to fetch data from the API, passing in completionHandler
contentProvider.fetchContentItems(completionHandler: completionHandler)
}
}
ContentProvider is just a helper class to fetch data:
class ContentProvider {
func fetchContentItems(completionHandler: @escaping (Error?) -> Void) {
// Make the async API request
// When the API request returns (either as success or error) call completionHandler(nil)
}
}
Note: You can just test the loading spinner by setting up a delay in beginLoadingChildItems(). When beginLoadingChildItems() is called, just set up a delay of 5 seconds and then call completionHandler(nil) once the 5 seconds are up. That should show the loading spinner on the CarPlay screen.
Thanks very much @Xeaza for updating the question with the answer from the Apple Engineer.
Like mentioned there, if you are using CPListTemplate you can show an activity indicator in the list item. Or rather, CarPlay does that for you.
One important thing is missing that took me a while to find out:
this only works if the accessoryType
of the selected listItem is set to .none
If you have accessoryType
of all items set to .none
beforehand you're fine. If you have it set to .disclosureIndicator
or .cloud
, you need to set it to .none
before you start the work (first line in handler).
let listItem = CPListItem(text: "Item Name", detailText: nil, image: UIImage(named: "yourImage")!, accessoryImage: nil, accessoryType: .disclosureIndicator)
listItem.handler = { [weak self] selectableListItem, completion in
listItem.accessoryType = .none
yourFunctionThatTakesSomeTime() { [weak self] (_, _) -> Void in
// do your thing
completion()
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With