CarPlay: Linking to the Maps App
Tags: swift, ios
Published: 2020-10-18 14:28:52

I've recently been granted a CarPlay developer entitlement from Apple 🎉 and as a result been working on a little app for this platform.  On first impression, its development platform has shades of legacy WatchKit development due to the heavy reliance of adding views (or, in this instance, populated templates) to an "interface controller", except without any use of Interface Builder, and definitely no form of SwiftUI at the time of writing!  Indeed the best way of seeing CarPlay is like a second display for your app the provides a **very specific** set of functionality - that in itself sounds a lot like the early days of Apple Watch!

When being granted your CarPlay entitlement, you get pointed to two resources:

1.  [Apple's CarPlay Programming Guide](https://developer.apple.com/carplay/documentation/CarPlay-App-Programming-Guide.pdf)
2.  [WWDC 2020: Accelerate Your App with CarPlay](https://developer.apple.com/videos/play/wwdc2020/10635/)

From my experience, combined together these give an extensive guide to developing on CarPlay.  However, I've noticed a small omission that I'd like to write about.

One template for CarPlay I've been using is the Point of Interest template (`CPPointOfInterestTemplate`), for which there literally is a point of interest about this template in the WWDC video:

>When a location is selected from a list, you may choose to show a secondary card for that point, revealing additional information and up to two buttons.
>
>These buttons could be used for a variety of tasks. For instance, they _may signal you to switch to a navigation app for driving directions_ or push to another template to display additional information for that location.

Now interestingly neither the video nor the programming guide state _how_ to switch to a navigation app!

`CPPointOfInterest` has 2x optional properties relating to buttons that show up when you have a secondary card displayed - `primaryButton` and `secondaryButton`.  These are both of type `CPTextButton`.  In `CPTextButton`'s initialiser, there's a `handler` parameter that takes a closure.  But what should go there if you want to open the point up on your maps app so you can route to that location?

Now chances are you already have an `MKMapItem` since `CPPointOfInterest` requires one in its initialiser.  This class has a method of interest, specifically `openInMaps` - this sounds very promising.

So let's try it out:

```
let mapItem = MKMapItem(placemark: ...)
let poi = CPPointOfInterest(
    location: mapItem,
    title: "Sample Location",
    subtitle: "Subtitle",
    summary: "Summary",
    detailTitle: "Detail Title",
    detailSubtitle: "Detail subtitle",
    detailSummary: "Detail Summary",
    pinImage: UIImage()
)

poi.primaryButton = CPTextButton(
    title: "GO",
    textStyle: .normal,
    handler: { _ in item.openInMaps() }
)

let mapTemplate = CPPointOfInterestTemplate(
    title: "Locations", 
    pointsOfInterest: [poi], 
    selectedIndex: NSNotFound
)
```

The template will now show one location...

![Simulator Screen Shot - iPhone 11 Pro Max - 2020-10-18 at 15.06.16](https://dev-to-uploads.s3.amazonaws.com/i/hcd3fy65ukhpw08wh3nn.png)
 
...and you can drill down to the location in question...

![Simulator Screen Shot - iPhone 11 Pro Max - 2020-10-18 at 15.06.19](https://dev-to-uploads.s3.amazonaws.com/i/4m8icgke0btzpb0awag1.png)
 
...tapping "GO" will open up the maps app - unfortunately this happens on your phone - and crucially Apple will reject your app because Apple rightly states:

> All CarPlay user flows must be possible without interacting with iPhone.

Thankfully this can be fixed - `MKMapItem.openInMaps(...)` can optionally take a few parameters:

* `launchOptions` - there are a few optional launch parameters you can pass to the maps app to help with directions.   More on this a bit later.
* `scene` - the scene that you're launching the maps app from.
* `completion` - an optional completion handler that is called upon the maps app being loaded.

Now the `scene` parameter looks promising.  Chances are you'll be calling all of this from `CPTemplateApplicationSceneDelegate.templateApplicationScene(:didConnect:)`, which has a `CPTemplateApplicationScene` parameter.  Conveniently this is a `UIScene` subclass, so you  can either pass this directly in or retain it from your `didConnect` method.

So let's now populate that `scene` parameter - again assuming this is called from `didConnect`:

```
poi.primaryButton = CPTextButton(
    title: "GO",
    textStyle: .normal,
    handler: { _ in 
        item.openInMaps(
            from: templateApplicationScene
        ) 
    }
)
```

This indeed opens up the maps app with the suggested location prepopulated.  However, as of iOS 14.0.1, your own app will almost certainly crash shortly afterwards with a system stack trace that looks something like this:

![Screenshot 2020-10-18 at 2.59.53 pm](https://dev-to-uploads.s3.amazonaws.com/i/6069qcty7k0n5u13fnt1.png)
 
The best way to stop this happening is to provide an empty `completionHandler` parameter:

```
poi.primaryButton = CPTextButton(
    title: "GO",
    textStyle: .normal,
    handler: { _ in 
        item.openInMaps(
            from: templateApplicationScene,
            completionHandler: { _ in return }
        ) 
    }
)
```

And now the external maps app opens on your CarPlay display with your location prepopulated, but this time without any crashes on your app.

---

**Note about the CarPlay external display on the iOS simulator:** annoyingly there is no maps app on the CarPlay display on the simulator.  Opening the link opens a "Now Playing" display, as shown below.  This in turn explains why I have no further screenshots of the maps app experience from here.  

![Simulator Screen Shot - iPhone 11 Pro Max - 2020-10-18 at 15.04.28](https://dev-to-uploads.s3.amazonaws.com/i/xex4occ3zq5ngvjporle.png)

---

Now I did mention `MKMapItem.openInMaps(...)` had a `launchOptions` parameter.  Given that you already have a destination prepopulated and it's also a car routing, no parameters need passing here - therefore `launchOptions` can be left out.

I would however suggest populating 2x additional properties of your `MKMapItem` instance to give the maps app some extra information:

* `name` - this shows up as the name of the location.  Chances are you'll know this in some way.  If you leave this out, the maps app will name the location "Unknown Location", which isn't desirable.
* `pointOfInterestCategory` - this is more for future-proofing than a necessity if you have this information at hand - it might help the maps app hook into any additional functionality for this specific location.

Finally, you may be applying a button like this from a different kind of template, such as `CPInformationTemplate` - again, as long as you have the location that you wish to navigate to, creating a map item only requires an `MKPlacemark` instance, which only requires the location in question to initialise.