3

As the title states, I am trying to build an app for my car. I only see a brief mention of CarPlay in Xamarin docs.

So is it possible to develop a CarPlay app with Xamarin and Visual Studio tools?

P.S. I did some more research and while you could develop apps for CarPlay, Apple only allows navigation and streaming apps as time of writing. So it's a complete non-starter for what I wanted to do.

AngryHacker
  • 59,598
  • 102
  • 325
  • 594

3 Answers3

5

Yes its possible. I made a blog post and an example on GitHub.


Here the answer in short:

In our iOS project we init the delegates in the AppDelegate.cs with CarIntegrationBridge.Init();.

The delegates are registered as follow:

public class CarIntegrationBridge : ICarIntegrationBridge
{
  public static void Init()
  {
    PlayableContentDelegate playableContentDelegate = new PlayableContentDelegate();
    MPPlayableContentManager.Shared.Delegate = playableContentDelegate;
    PlayableContentDataSource playableContentDataSource = new PlayableContentDataSource();
    MPPlayableContentManager.Shared.DataSource = playableContentDataSource;
  }
}

Now we define our datasource. In my example I added radio stations with name and url. We have to define the menu items count and how a menu item is displayed (name, icon, …):

internal class PlayableContentDataSource : MPPlayableContentDataSource
{
  public static List<Station> Stations = new List<Station>
  {
    new Station{Name = "Rainbow radio", Url = "https://stream.rockantenne.de/rockantenne/stream/mp3"},
    new Station{Name = "Unicorn radio", Url = "http://play.rockantenne.de/heavy-metal.m3u"}
  };
  
  public override MPContentItem ContentItem(NSIndexPath indexPath)
  {
    var station = Stations[indexPath.Section];
    var item = new MPContentItem(station.Url);
    item.Title = station.Name;
    item.Playable = true;
    item.StreamingContent = true;
    var artWork = GetImageFromUrl("station.png");
    if (artWork != null)
    {
      item.Artwork = artWork;
    }
    return item;
  }
  public override nint NumberOfChildItems(NSIndexPath indexPath)
  {
    if (indexPath.GetIndexes().Length == 0)
    {
      return Stations.Count;
    }
    throw new NotImplementedException();
  }
  private MPMediaItemArtwork GetImageFromUrl(string imagePath)
  {
    MPMediaItemArtwork result = null;
    try
    {
      using (var nsUrl = new NSUrl(imagePath))
      {
        using (var data = NSData.FromUrl(nsUrl))
        {
          var image = UIImage.LoadFromData(data);
          result = new MPMediaItemArtwork(image);
        }
      }
    }
    catch
    {
      UIImage image = UIImage.FromBundle(imagePath);
      if (image != null)
      {
        result = new MPMediaItemArtwork(image);
      }
    }
    return result;
  }
}

Now we have to decide what is todo, if an item is taped. The simulator have an other behavior than a real device. So I hacked a solution for calling the NowPlayingScene.

internal class PlayableContentDelegate : MPPlayableContentDelegate
{
  public override void InitiatePlaybackOfContentItem(
    MPPlayableContentManager contentManager, NSIndexPath indexPath, Action<NSError> completionHandler)
  {
    Execute(contentManager, indexPath);
    completionHandler?.Invoke(null);
  }
  private void Execute(MPPlayableContentManager contentManager, NSIndexPath indexPath)
  {
    DispatchQueue.MainQueue.DispatchAsync(async () => await ItemSelectedAsync(contentManager, indexPath));
  }
  private async Task ItemSelectedAsync(MPPlayableContentManager contentManager, NSIndexPath indexPath)
  {
    // Play
    var station = PlayableContentDataSource.Stations[indexPath.Section];
    await CrossMediaManager.Current.Play(station.Url);
    // Set playing identifier
    MPContentItem item = contentManager.DataSource.ContentItem(indexPath);
    contentManager.NowPlayingIdentifiers = new[] { item.Identifier };
    // Update on simulator
    if (DeviceInfo.DeviceType == DeviceType.Virtual)
    {
      InvokeOnMainThread(() =>
      {
        UIApplication.SharedApplication.EndReceivingRemoteControlEvents();
        UIApplication.SharedApplication.BeginReceivingRemoteControlEvents();
      });
    }
  }
}

To reload the data (e.g. if you change the stations), you have to call this:

public void ReloadStations()
{
  MPPlayableContentManager.Shared?.ReloadData();
}
Suplanus
  • 1,523
  • 16
  • 30
0

Update:

Seeing that .NET MAUI is Xamarin's replacement, I'm adding these resources to this answer in case someone stumbles upon this question in the future:

If you're targeting .NET MAUI now, I've made a full tutorial on how to create an Apple CarPlay app for .NET MAUI.

You can find the source code for the .NET MAUI Android Auto and Apple CarPlay apps at this GitHub repository.

Hope it helps!

Original answer:

I managed to build an Apple CarPlay app with Xamarin, although I used quite a different approach to what Suplanus did because I didn't create a music app, my approach followed the Apple docs approach more closely.

First make sure your provisioning profile has the correct CarPlay entitlement.

Add the entitlement to Entitlements.plist file.

In your info.plist like suggested in Apple's docs:

<key>UIApplicationSceneManifest</key>
<dict>
    <key>UISceneConfigurations</key>
    <dict>
        <!-- Device Scene -->
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneConfigurationName</key>
                <string>Default Configuration</string>
                <key>UISceneDelegateClassName</key>
                <string>DeviceSceneDelegate</string>
            </dict>
        </array>
        <!-- Carplay Scene -->
        <key>CPTemplateApplicationSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneClassName</key>
                <string>CPTemplateApplicationScene</string>
                <key>UISceneConfigurationName</key>
                <string>YourAppName-Car</string>
                <key>UISceneDelegateClassName</key>
                <string>YourAppName.CarPlaySceneDelegate</string>
            </dict>
        </array>
    </dict>
</dict>

I then used the solution suggested in this thread to send CarPlay to the correct delegate, with the following changes:

  1. On my CarPlaySceneDelegate class inherit directly from CPTemplateApplicationSceneDelegate.
  2. Implement override methods for DidConnect and DidDisconnect instead.
  3. UIWindowSceneSessionRole.CarTemplateApplication has now been added to the enum, so use that instead of checking in the catch if it’s a CarPlay app in AppDelegate class's GetConfiguration override.
  4. Return default configuration in GetConfiguration override if it's not a CarPlay app.

You'll notice all the classes mentioned in Apple's developer docs are available in the CarPlay library that lives in Xamarin.iOS, so it's not too difficult to translate the swift code to C# to create and display the required templates.

Finally, use this solution to launch your Xamarin.Forms app from the device scene delegate instead of seeing a blank screen on your phone when you open the app.

Hope this helps a bit that you don't have to struggle as much as I did to get it working!

CVStrydom
  • 11
  • 5
-1

Maybe this https://forums.xamarin.com/discussion/144790/android-auto-ios-car-play answers the question ?

Car Play seems to be a private framework on Apple's side. And there is no relative documentation for XCode.

For Android Auto, go to github, search project Xamarin_Android_Auto_Test

Community
  • 1
  • 1
Softlion
  • 12,281
  • 11
  • 58
  • 88
  • That's an old answer, the framework is now open, but only to streaming and navigation apps, so it's non-starter for what I want. – AngryHacker May 13 '19 at 17:11