
你好 我叫Vanya,我正在为iOS编写2GIS移动应用程序。 今天将有一个关于我们的导航仪如何出现在CarPlay中的故事。 我将告诉您如何使用此类文档和未完成的工具创建有效的产品并将其放置在AppStore中。
关于CarPlay的几句话

首先,需要一些材料来了解CarPlay的某些方面以及我们做出某些决定的原因。
CarPlay不是另一个操作系统中的一个操作系统,因为有许多文章对此进行了介绍。 如果粗略地讲,那么CarPlay是用于与主机屏幕的外部显示器一起工作的协议。 汽车扬声器发出的声音; 触摸屏,触摸面板,洗衣机和其他输入设备。
也就是说,整个可执行代码直接位于主应用程序中(甚至没有单独的扩展程序!),这非常酷:为了获得新功能,您不需要更新无线电甚至是机器,只需要更新iOS。
在WWDC 2018主题演讲中,我们获得了为CarPlay创建导航应用程序的机会,这让我们感到非常高兴。 演讲结束后,我们立即发送了许可, 要求开发CarPlay。 在请求中,有必要证明我们的应用程序具有导航能力。
在等待苹果公司答复的同时,有一个讲座 ,其中使用示例应用程序CountryRoads讨论了如何使用CarPlay.framework。 讲座没有讨论使用CarPlay时的陷阱和微妙之处,但提到在连接到CarPlay收音机后,该应用程序将在后台模式下工作。
车轮上的第一根棍子
后台的应用程序使我们感到失望。 这有两个原因:
- 我们不在后台工作。 一旦出于技术原因和节约能源而放弃了这一限制。
- 我们的地图是用OpenGL编写的(是的,已弃用,是的,不是Metal,我们都知道),而处于背景状态的OpenGL不起作用。 充其量您可以看到黑屏,而最坏的情况是崩溃。
仍然可以在后台处理工作,但是该卡肯定需要解决。 然后,想法就通过标准的MKMapView得以实现。 在您开始以使用标准Apple卡的想法向我们扔石头之前,我将向您解释:我们将使用MKMapView,而不是Apple卡。
事实是MKMapView可以加载第三方切片。 瓷砖是用于纹理的特殊矩形容器。 我们原来是一个知道如何给瓷砖贴砖的Servochka。 GitHub上有实现代码 。
苹果答案
我们收到了Apple的答复,其中除了开发许可外,我们还收到了“精英”文档,CountryRoads示例应用程序的代码(在WWDC演讲中显示),最重要的是,私有功能密钥com.apple.developer.carplay-maps
。 该密钥被写入授权文件中,其值为YES,以便系统理解您可以在应用程序启动时处理CarPlay中的事件。
不用等待带有选定故事进行开发的冲刺,我就下载了Xcode Beta。 收集2GIS的第一次尝试失败。 但是CoutryRoads示例应用程序项目能够为模拟器组装。
在每次打开CarPlay仿真器窗口之前,必须通过以下窗口自定义后者:

为此,您必须在终端中写一行: defaults write com.apple.iphonesimulator CarPlayExtraOptions -bool YES
由于某种原因,这是行不通的-我不得不在几乎最小的模拟器上运行它,分辨率为800×480点,比例为2×。 目前,此设置有效并且很有帮助。
创建了示例项目并配备了文档之后,我开始了解发生了什么。
我意识到的第一件事:CarPlay的导航应用程序由基本视图和模板层组成。

基本视图就是您的地图。 在此层上应该只有地图,没有其他视图和控件。
模板是几乎不可定制的强制性UI元素集,用于显示路线,操作,各种列表等。
Beta开发
让我们继续编写代码。 首先要做的是在ApplicationDelegate文件中实现几个必需的CPApplicationDelegate方法。
func application( _ application: UIApplication, didConnectCarInterfaceController controller: CPInterfaceController, to window: CPWindow ) {} func application( _ application: UIApplication, didDisconnectCarInterfaceController controller: CPInterfaceController, from window: CPWindow ) {}
让我们看一下签名:
使用UIApplication,一切都变得清晰了。
CPWindow是UIWindow的后继产品,UIWindow是收音机的主机外部显示的窗口。
CPInterfaceController-类似于UINavigationController的类似物,仅来自CarPlay.framework。
现在我们直接进行该方法的实现。
func application( _ application: UIApplication, didConnectCarInterfaceController controller: CPInterfaceController, to window: CPWindow ) { let carMapViewController = CarMapViewController( interfaceController: controller ) let navigationController = UINavigationController( rootViewController: carMapViewController ) window.rootViewController = navigationController }
在didConnect中,您需要编写类似于我们在didFinishLaunching中看到的代码。 根据文档,CarMapViewController是基本视图(该控制器实际上是,但是还可以)。
这是我终于得到的照片:

这时的某个地方突然让我想到,默认情况下会启用新的Xcode新构建系统,因此,最有可能的是2GIS不会启用。
我打开了Xcode,安装了旧版(或者说很稳定,我们称其为spade)构建系统,我的理论得到了证实:2GIS已组装完毕。
设置了相同的功能密钥后,我在CarPlay下启动了2GIS,但没有看到有关应用程序切换到后台模式的任何日志。 变得更加难以理解,因为现场的Apple工程师谈到了背景模式,但另一方面,他们向我们保证了UIAlertView的contentView,结果UIAlertView被弃用了。
在决定应该这样做之后,我不再理会MKMapView。 这会使我们脱机,并使我们重新编写路线的渲染。
单卡问题
我没有时间为CarPlay将拥有我们的地图而高兴,因为以下问题面对我:由于技术特性,只能有一张地图。
尽管不是很优雅,但快速解决此问题的方法是。
通常,在CarPlay上使用2GIS时,电话被锁定并放在架子上的某个位置。 因此,此时确实不需要手机上的地图(当然,搜索不会受到伤害)。 因此,当我们将手机连接到CarPlay时,我们决定从主应用程序中提取卡并将其显示在收音机的CarPlay屏幕上。 并且分别断开连接后,返回手机上的应用程序。
是的,它是一种解决方案,但是很快,它仍然有效,并且不需要踢其他几个命令来铆钉MVP。
地图上的控件
因此,我们在无线电屏幕上获得了地图。 现在,有必要为任何地图做第一件事和显而易见的事情:缩放,当前位置和地图移动的控件。

让我们从缩放和当前位置开始,因为这些控件位于地图本身上,而不是普通的UIControl。 就像我在上面写的,只有地图在基本视图上。
为了将这些控件放置在卡上,我不得不再次进入文档和示例应用程序。 在这里,我了解了第一个模板-CPMapTemplate。

CPMapTemplate-一个透明的模板,用于在地图上显示一些控件,以及navigationBar的类似物。 它的创建和设置如下:
let mapTemplate = CPMapTemplate() self.interfaceController.setRootTemplate(mapTemplate, animated: false)
接下来,您需要创建这些控件并将其放在卡上。
let zoomInButton = CPMapButton(…) let zoomOutButton = CPMapButton(…) let myLocationButton = CPMapButton(…) self.mapTemplate.mapButtons = [ zoomInButton, zoomOutButton, myLocationButton ]
但是mapButtons数组原来很有趣,因为无论您放入多少元素,它都只会采用前三个元素并将它们显示在屏幕上。 您不会在日志或断言中收到任何错误。
然后,我看看如何使地图移动,并在文档中找到了这一点:
Navigation apps are designed to work with a variety of car input devices, and CarPlay does not support direct user interaction in the base view (apps do not directly receive tap or drag events).
我认为很奇怪,并且必须在CountryRoads示例应用程序中观察它是如何完成的。 答案是通过以下接口:

不是很方便,但是文档不会以其他方式说谎,对吗?
由于我们在地图上的控件位置用完了,因此有必要创建一个按钮,以类似NavigationBar的方式将地图置于“拖动”模式。
let panButton = CPBarButton(…) self.mapTemplate.leadingNavigationBarButtons = [panButton] self.mapTemplate.trailingNavigationBarButtons = []
但是,leadingNavigationBarButtons和trailingNavigationBarButtons的数组也不是没有玩笑:它们中有多少个元素,它们只会取前两个。 日志和断言中也没有错误。
要激活和禁用卡拖放模式,您必须编写:
self.mapTemplate.showPanningInterface(animated: true) self.mapTemplate.dismissPanningInterface(animated: true)
在地图上建立和显示路线
接下来,我开始重用我们现有的API来构建路由。
为了演示并了解操作方法,我决定采取两点并在两者之间建立一条路线。 A点是用户的位置,B点是我们在新西伯利亚的主要办公室。
代号 let choice0 = CPRouteChoice( summaryVariants: ["46 "], additionalInformationVariants: [" "], selectionSummaryVariants: ["1 7 "] ) let choice1 = CPRouteChoice( summaryVariants: ["46 "], additionalInformationVariants: [" "], selectionSummaryVariants: [“1 11 "] ) let startItem = MKMapItem(…) let endItem = MKMapItem(…) endItem.name = ", ” let trip = CPTrip( origin: startItem, destination: endItem, routeChoices: [choice0, choice1] ) let tripPreviewTextConfiguration = CPTripPreviewTextConfiguration( startButtonTitle: " ”, additionalRoutesButtonTitle: “”, overviewButtonTitle: "" ) self.mapTemplate.showTripPreviews( [trip], textConfiguration: tripPreviewTextConfiguration )
在屏幕上,我们获得了带有路线说明的控件:

导航模式
路线不错,但导航器的主要功能是导航。 为了使其显示,您必须编写以下内容:
func mapTemplate( _ mapTemplate: CPMapTemplate, startedTrip trip: CPTrip, using routeChoice: CPRouteChoice ) { self.navigationSession = self.mapTemplate.startNavigationSession(for: trip) }
CPNavigationSession-一个类,通过它可以显示一些仅在导航模式下必需的UI元素。
要显示操作,您必须:
let maneuver = CPManeuver() maneuver.symbolSet = CPImageSet( lightContentImage: icon, darkContentImage: darkIcon ) maneuver.instructionVariants = [". "] maneuver.initialTravelEstimates = CPTravelEstimates(…) self.navigationSession?.upcomingManeuvers = [maneuver]
然后,在收音机的屏幕上,我们得到以下信息:

要更新素材以进行操作,您必须:
let estimates = CPTravelEstimates(…) self.navigationSession?.updateEstimates(estimates, for: maneuver)
就是这样!
当导航器的基本功能准备就绪时,我决定在内部演示中展示此工艺。 演示非常成功:每个人都想到了尽快完成,测试和启动导航器的想法。
首先,我们订购了具有CarPlay支持的真实主机。 然后,正如他们所说,热开始了。

供应资料
由于添加了新的功能密钥,因此需要重新生成配置文件。 在正常的开发中,我们不会考虑它,因为Xcode会自己完成所有事情。 但不是私钥。
Code Signing Error: Automatic signing is unable to resolve an issue with the "v4ios" target's entitlements. Automatic signing can't add the com.apple.developer.carplay-maps entitlement to your provisioning profile. Switch to manual signing and resolve the issue by downloading a matching provisioning profile from the developer website.
这也打破了我们的CI,因为对于本地发行的应用程序版本,我们使用企业帐户,在该帐户中,我们没有请求开发CarPlay应用程序的权限。 但这是一个完全不同的故事。
侦错
您可以通过蓝牙或闪电连接到CarPlay。 实践表明,第二种方法更受欢迎。 我们的蓝牙收音机不知道如何操作,因此在开发过程中我不得不使用Wi-Fi调试。 如果您在比hello world更困难的项目上尝试过它,那么您就会知道它到底是什么。
对于那些没有尝试过的人,我告诉:我通过电线将应用程序收集到手机上,然后才通过Wi-Fi将手机连接到CarPlay,然后将其上传到手机并运行了几分钟。
将应用程序复制到手机大约需要3分钟,将应用程序启动大约需要1分钟,然后才在断点处启动停止之后,才15秒。
然后,对我而言,为什么Apple不制造任何DevKit变得非常有趣(以Apple方式,它就可以正常工作,仅此而已)。 没有它,组装测试架不是很方便。 到现在为止,每隔两周就会出现一些问题-您必须从图片中记住要坚持的内容。 管理员在组装这个架子时告诉他们什么和原因是很好的。
我们做过的最好的框架
最后,当所有东西都组装在真实设备上时,很明显,功能“ 2GIS for CarPlay”一定会存在。 现在是时候做美丽了。
视口问题
必须将地图视口配置为在区域中绘制路线,而无需不必要的控制,而不仅仅是在中间。 简而言之,使它看起来与众不同:

依此类推:

我希望通过当前可见区域获得某种layoutGuide。 因此,他考虑了navigationBar,带有路线的视图以及地图上的控件。 实际上,我什么也没得到。 尚不清楚如何配置视口,因此我们有一个类似的硬代码:
let routeControlsWidth = self.view.frame.width * 0.48 let zoomControlWidth = self.view.frame.width * 0.15
不仅在两点之间构造通道
在第一个版本中,我们决定采用通过CPGridTemplate制成的摩擦片:

收藏夹和主页/通过CPListTemplate工作。

然后通过CPSearchTemplate进行键盘搜索:

我不会显示有关模板的代码,因为它很简单并且有关它的文档写得很好(至少关于某些方面)。
但是,值得一提的是与他们一起工作时发现了什么问题。CPInterfaceController可以在导航中类似于UIKit。 即
self.interfaceController.pushTemplate(listTemplate, animated: true) self.interfaceController.presentTemplate(alertTemplate, animated: true)
但是,如果尝试运行例如CPAlertTemplate,则会在日志中得到断言,即只能以模态表示CPAlertTemplate。
目前尚不清楚,为什么苹果在没有建立如下界面的情况下就不会隐藏交易的逻辑:
self.interfaceController.showTemplate(listTemplate, animated: true)
它还破坏了使用CPTemplate继承人的功能,例如UIKit中的控制器。
例如,当您尝试将继承人放在模板堆栈上时,您会得到以下信息:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unsupported object <YourAwesomeGridTemplate: 0x60000060dce0> <identifier: 6CAC7E3B-FE70-43FC-A8B1-8FC39334A61D, userInfo: (null)> passed to pushTemplate:animated:. Allowed classes: {( CPListTemplate, CPGridTemplate, CPSearchTemplate, CPMapTemplate )}'
测试和错误
由artemenko-aa测试。 他发现的第一个错误之一,我们仍然无法修复。
事实是,当您从CarPlay收音机中断开手机的连接时,看门狗会偶发地钉住我们-无需说明原因。 即使打开了syslog,也不清楚。 因此,如果您对如何解决或了解原因有任何想法,请随时发表评论。
下一个错误在同一位置,但是有特殊的行为。 我在上面写道,当电话与CarPlay断开连接时,将调用CPApplicationDelegate的didDisconnect方法。 在这种方法中,我们将卡从无线电屏幕返回到主应用程序。 想象一下,如果至少五分之一不调用此方法,我们将遇到多少问题。
显然这是iOS的问题,而不是我们应用程序的问题,因为整个系统都认为它已连接到CarPlay。

我什至把它报告为雷达(像所有其他错误一样)。 我被要求丢弃具有这种配置文件的日志,但是一段时间以来我一直无法获得支持,因此他们关闭了雷达。
由于苹果公司不打算做任何事情,因此必须自行解决此问题,因为它经常被复制。
然后我想起了大部分与CarPlay的连接都来自闪电。 这意味着电话在连接时正在充电,而在断开连接时,充电停止。 如果是这样,那么您可以订阅电池状态,并确切了解手机何时停止充电并与CarPlay断开连接。
该计划很脆弱,但我们别无选择。 我们就这样走了,行得通!

幸运的是,这个拐杖早已从代码中删除:Apple开发人员在其中一个iOS版本中修复了所有问题。
两位编辑的故事
第一次重定向与元数据有关。 社论的文字说,我们的描述(不是发行说明)并不表示我们支持CarPlay。 如您所料,评论指南和相同的Google Maps都没有。 我们没有争论(因为它通常比编辑元数据要长),所以将行从发行说明复制到了描述,并开始等待新的审查。
由于城市列表的原因,发生了第二个问题。 2GIS具有非常酷的功能-完全脱机操作模式。 此功能将我们击中腿。
将没有建立城市的应用程序连接到CarPlay时,我们不会显示地图,因为没有内容可显示。 为此,我们已安排好时间。 解决方案很简单:没有按钮的警报,提示您需要下载城市。

你不能谈论的
手势地图运动
大约在同一时间,来自Google Maps的CarPlay下的导航器出现了-您可以在其中用手势在屏幕上移动地图。 我认为专用API很明显! 来自Google的家伙刚从附近的建筑物里来,说了他们需要的东西。 毕竟,文档说:
Navigation apps are designed to work with a variety of car input devices, and CarPlay does not support direct user interaction in the base view (apps do not directly receive tap or drag events).
但是,尽管几乎没有意义,但我仍然决定确保并被谷歌搜索,因为没有关于CarPlay导航应用程序的技术文章。 但是,我设法在Apple网站上突然找到了一些有用的东西。
在指导方针中,我找到了一段录像,说该文件无礼。 视频显示了如何仍然可以使用手势拖动地图。 我意识到自己一无所知,唯一剩下的就是打开CarPlay.framework并查看所有.h文件。
瞧! 我在CPMapTemplate中发现了它的委托CPMapTemplateDelegate,其中似乎有3种方法在尖叫,如果实现它们,您可以控制地图的手势。
3种方法/ * 平移手势开始时调用。 连接到某些CarPlay系统时可能不会被调用。
/
可选的公共功能mapTemplateDidBeginPanGesture(_ mapTemplate:CPMapTemplate)
/ * 平移手势更改时调用。 连接到某些CarPlay系统时可能不会被调用。
/
可选的公共功能mapTemplate(_ mapTemplate:CPMapTemplate,didUpdatePanGestureWith翻译翻译:CGPoint,速度:CGPoint)
/ * 平移手势结束时调用。 连接到某些CarPlay系统时可能不会被调用。
/
可选的公共功能mapTemplate(_ mapTemplate:CPMapTemplate,didEndPanGestureWithVelocity速度:CGPoint
)
我实现了它们,并在模拟器上运行了该应用程序-没有任何效果。 由于没有时间烦恼,我意识到模拟器的质量与文档相同,并将其放在设备上。 一切开始,幸福无止境!
有趣的事实:CarPlay收音机需要四分之一的屏幕才能知道摇摄手势已经开始。 我想指出的是,UIPanGestureRecognizer只需要10点。
不同无线电录音机上UI的一致性
我们收到了有关支持的呼吁:在搜索中,用户只有一个最简单的爬网,尽管可能会有更多。 我想这很奇怪,因为在所有屏幕上只能放一行。 已要求截图:

这与我上面显示的CPSearchTemplate UI完全不同。 尽管在开发过程中仍然无法理解下面板上的细胞数量,但仍必须考虑到这一点。
限速控制
我们查看了统计信息,意识到他们将导航器用于CarPlay,我们需要至少将其带到主应用程序中的导航器级别。 首先,我们决定添加速度限制控制。 当然,有一些问题。
问题一:放在哪里?
再次在CPWindow中的.h文件周围进行混编,我发现了一个奇怪的layoutGuide:
var mapButtonSafeAreaLayoutGuide:UILayoutGuide
事实证明这就是我们所需要的。 我们的控件非常适合:


问题二:这通常合法吗?
事实是,从技术上讲,控件是基于基本视图的。 并且根据文档的基本视图不能包含除地图之外的任何内容:
The base view is where the map is drawn. The base view must be used exclusively to draw a map, and may not be used to display other UI elements. Instead, navigation apps overlay UI elements such as the navigation bar and map buttons using the provided templates.
但是评论者在AppStore中错过了我们,这意味着仍可以内置与导航相关的控件。
语音搜寻


首先,必须很好地完成此功能,但是由于技术上的困难,我们已经积累了多项任务,这些任务阻碍了CarPlay语音搜索的实施。 而且这项任务并不像看起来那样简单。
第一个问题:动画。 事实是,在CPVoiceControlTemplate中无法制作标准动画。 必须从图片中逐帧收集用于语音识别和搜索的动画,并指出它们花了多少时间。
for i in 1...12 { if let image = UIImage(named: "carplay_searching_\(i)") { images.append(image) } } let image = UIImage.animatedImage(with: images, duration: 0.96)
您可能会猜到,它看起来并不是真的,但我不想夸大应用程序的大小。
第二个问题:访问。 手机显示屏上会显示有关麦克风访问和语音识别的警报。 我必须在收音机显示屏上写下用户需要拿起电话,获得许可然后才使用收音机上的导航器的信息。 很舒服!
右驾车。
我们收到了一个截屏,其中整个应用程序的UI都颠倒了!

而且,当然,地图视口仍然是我们对其进行硬编码的方式,因为没有人期望右驾车有单独的设置。 我没有找到如何正确“解决”这个问题的方法,但是我注意到,由于我们的限速控件位于地图控件的layoutGuide中,因此它移到了左侧。
Ultrafix即将推出。 他们无礼地做到了,但是行得通。
let isLeftWheelCar = self.speedControlViewController.view.frame.origin.x > self.view.frame.size.width / 2.0
我真的希望有一个正确的解决方案,而我只是没有阅读。
这就是我的全部。如果您突然打算在CarPlay下制作自己的导航器,请记住文档和框架并不完善。该平台是全新的,没人知道,苹果也不急于分享知识。