
Oi Meu nome é Vanya, estou escrevendo um aplicativo móvel 2GIS para iOS. Hoje haverá uma história sobre como nosso navegador apareceu no CarPlay. Vou contar como, com essa documentação e ferramentas inacabadas, criamos um produto funcional e o colocamos na AppStore.
Algumas palavras sobre o CarPlay

Primeiro, um pouco de material para entender alguns aspectos do CarPlay e as razões pelas quais tomamos certas decisões.
O CarPlay não é um sistema operacional dentro de outro sistema operacional, pois muitos artigos escrevem sobre ele. Se, aproximadamente, o CarPlay é um protocolo para trabalhar com um monitor externo da tela da unidade principal; som dos alto-falantes do carro; telas de toque, painéis de toque, arruelas e outros dispositivos de entrada.
Ou seja, todo o código executável está localizado diretamente no aplicativo principal (nem mesmo em uma extensão separada!) Isso é muito legal: para obter novos recursos, você não precisa atualizar o rádio ou a máquina, basta atualizar o iOS.
No WWDC 2018 Keynote, fomos presenteados com a oportunidade de criar aplicativos de navegação para o CarPlay, o que nos deixou muito felizes. Imediatamente após a apresentação, enviamos uma solicitação de permissão para desenvolver o CarPlay. Na solicitação, foi necessário mostrar que nosso aplicativo é capaz de navegação.
Enquanto esperávamos uma resposta da Apple, houve uma palestra em que, usando o aplicativo de exemplo CountryRoads, conversamos sobre como trabalhar com o CarPlay.framework. A palestra não falou sobre as armadilhas e sutilezas ao trabalhar com o CarPlay, mas mencionou que, após conectar-se ao rádio CarPlay, o aplicativo funcionará no modo de segundo plano.
Primeiro pau nas rodas
A aplicação em segundo plano nos decepcionou. Havia duas razões para isso:
- Não trabalhamos em segundo plano. Uma vez deixada essa limitação por razões técnicas e de conservação de energia.
- Nosso mapa está escrito em OpenGL (sim, descontinuado, sim, não Metal, todos sabemos isso), e o OpenGL no estado de segundo plano não funciona. Na melhor das hipóteses, você obtém uma visão em preto e, na pior das hipóteses, falha.
Ainda era possível lidar com o trabalho em segundo plano, mas o cartão definitivamente precisava ser resolvido. Então surgiu a idéia de passar pelo MKMapView padrão. Até você começar a atirar pedras contra nós pela ideia de usar cartões Apple padrão, vou explicar: nós usaríamos o MKMapView, mas não os cartões Apple.
O fato é que o MKMapView pode carregar blocos de terceiros. As telhas são recipientes retangulares especiais para texturas. Acabamos por ser um servochka que sabe dar ladrilhos. Há código de implementação no GitHub.
Resposta da Apple
Recebemos uma resposta da Apple, na qual, além da permissão para desenvolver, também recebemos a documentação "para a elite", o código do aplicativo de exemplo CountryRoads (foi mostrado na palestra da WWDC) e, mais importante, a chave de recurso privado com.apple.developer.carplay-maps
. Essa chave é gravada no arquivo de direitos com o valor YES, para que o sistema entenda que você pode processar eventos do CarPlay quando o aplicativo for iniciado.
Sem esperar pelo sprint com as histórias selecionadas para desenvolvimento, subi para fazer o download do Xcode Beta. A primeira tentativa de coletar 2GIS foi uma falha. Mas o projeto de aplicativo de amostra CoutryRoads pôde ser montado no simulador.
Antes de cada abertura da janela do simulador CarPlay, este último precisava ser personalizado através dessa janela:

Para fazer isso, era necessário escrever uma linha no terminal: os defaults write com.apple.iphonesimulator CarPlayExtraOptions -bool YES
Por alguma razão, isso não funcionou - eu tive que executá-lo quase no menor simulador, com uma resolução de 800 × 480 pontos e uma escala × 2. No momento, essa configuração funciona e ajuda muito.
Tendo criado meu projeto de amostra e munido de documentação, comecei a entender o que estava acontecendo.
A primeira coisa que percebi: os aplicativos de navegação para o CarPlay consistem em camadas de visualização base e modelos.

A vista base é o seu mapa. Nesta camada, deve haver apenas um mapa, sem outras visualizações e controles.
Modelos é um conjunto obrigatório quase não personalizável de elementos da interface do usuário para exibir rotas, manobras, todos os tipos de listas e assim por diante.
Desenvolvimento beta
Vamos passar a escrever código. A primeira coisa a fazer é implementar alguns métodos CPApplicationDelegate necessários no arquivo ApplicationDelegate.
func application( _ application: UIApplication, didConnectCarInterfaceController controller: CPInterfaceController, to window: CPWindow ) {} func application( _ application: UIApplication, didDisconnectCarInterfaceController controller: CPInterfaceController, from window: CPWindow ) {}
Vejamos a assinatura:
Com o UIApplication, tudo fica claro.
O CPWindow é o sucessor do UIWindow, uma janela para a exibição externa da unidade principal do rádio.
CPInterfaceController - algo como um análogo do UINavigationController, apenas no CarPlay.framework.
Agora, prosseguimos diretamente para a implementação do método.
func application( _ application: UIApplication, didConnectCarInterfaceController controller: CPInterfaceController, to window: CPWindow ) { let carMapViewController = CarMapViewController( interfaceController: controller ) let navigationController = UINavigationController( rootViewController: carMapViewController ) window.rootViewController = navigationController }
No didConnect, você precisa escrever um código semelhante ao que costumávamos ver no didFinishLaunching. CarMapViewController é uma visão básica (o controlador está realmente, mas ok), conforme a documentação.
Aqui está a foto que finalmente consegui:

Em algum momento, me ocorreu que o novo sistema de compilação do Xcode está ativado por padrão e, provavelmente, por causa disso, o 2GIS não está habilitado.
Abri o Xcode, instalei um sistema legado (ou melhor, estável, vamos chamar de spade a spade) e minha teoria foi confirmada: o 2GIS foi montado.
Depois de definir a mesma chave de capacidade, iniciei o 2GIS no CarPlay e não vi nenhum registro sobre o aplicativo mudar para o modo de segundo plano. Isso se tornou ainda mais incompreensível, porque os engenheiros da Apple disseram sobre o modo de segundo plano, mas, por outro lado, eles nos prometeram um contentView do UIAlertView e, como resultado, o UIAlertView ficou obsoleto.
Tendo decidido que deveria ser assim, eu não me incomodei com o MKMapView. Isso nos privaria offline e nos faria reescrever a renderização das rotas.
Problema com cartão único
Não tive tempo de me alegrar com a notícia de que o CarPlay terá nosso mapa, pois o seguinte problema me enfrentou: por causa dos recursos técnicos, só pode haver um mapa.
Uma solução rápida para esse problema foi, embora não muito elegante.
Geralmente, ao usar 2GIS no CarPlay, o telefone está bloqueado e fica em algum lugar na prateleira. Portanto, o mapa no momento no telefone não é realmente necessário (não será difícil pesquisar, é claro). Portanto, quando conectamos o telefone ao CarPlay, decidimos pegar o cartão no aplicativo principal e exibi-lo na tela do rádio do CarPlay. E quando desconectado, respectivamente, retorne ao aplicativo no telefone.
Sim, é uma solução para si mesmo, mas é rápido, ainda funciona e não precisava chutar alguns outros comandos para rebitar o MVP.
Controles no mapa
Então, colocamos nosso mapa na tela do rádio. Agora era necessário fazer as primeiras e óbvias coisas de qualquer mapa: controles de zoom, localização atual e movimento do mapa.

Vamos começar com o zoom e a localização atual, porque esses controles estão localizados no próprio mapa e não são UIControl comuns. Como escrevi acima, apenas o mapa está na vista de base.
Para colocar esses controles no cartão, tive que entrar na documentação e no aplicativo de amostra novamente. Lá eu li sobre o primeiro modelo - CPMapTemplate.

CPMapTemplate - um modelo transparente para exibir alguns controles no mapa e no análogo da barra de navegação. Ele é criado e configurado assim:
let mapTemplate = CPMapTemplate() self.interfaceController.setRootTemplate(mapTemplate, animated: false)
Em seguida, você precisa criar esses controles e colocá-los no cartão.
let zoomInButton = CPMapButton(…) let zoomOutButton = CPMapButton(…) let myLocationButton = CPMapButton(…) self.mapTemplate.mapButtons = [ zoomInButton, zoomOutButton, myLocationButton ]
Mas a matriz mapButtons acabou sendo engraçada, porque não importa quantos elementos você coloque nela, ela pegará apenas os três primeiros elementos e os exibirá na tela. Você não receberá nenhum erro no log ou nas asserções.
Então eu pude ver como posso fazer o mapa se mover e achei isso na documentação:
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).
Estranho, pensei, e observei como isso é feito no aplicativo de exemplo CountryRoads. A resposta é através desta interface:

Não é muito conveniente, mas de uma maneira diferente, a documentação não estará, certo?
Como o local para os controles no mapa acabamos, era necessário pressionar um botão para colocar o mapa no modo "arrastar" neste análogo da barra de navegação.
let panButton = CPBarButton(…) self.mapTemplate.leadingNavigationBarButtons = [panButton] self.mapTemplate.trailingNavigationBarButtons = []
Mas as matrizes de LeadNavigationBarButtons e trailingNavigationBarButtons também não ficaram sem uma piada: quantos elementos neles empurram, eles levarão apenas os dois primeiros. Também sem erros no log e asserções.
E para ativar e desativar o modo de arrastar e soltar cartão, você deve escrever:
self.mapTemplate.showPanningInterface(animated: true) self.mapTemplate.dismissPanningInterface(animated: true)
Construindo e exibindo rotas em um mapa
Em seguida, comecei a reutilizar nossa API existente para criar rotas.
Apenas para uma demonstração e entender o que e como fazer, decidi pegar dois pontos e construir uma rota entre eles. O ponto A era a localização do usuário e o ponto B era nosso escritório principal em Novosibirsk.
Código 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 )
Na tela, temos um controle com uma descrição da rota:

Modo de navegação
As rotas são boas, mas a principal característica do navegador é a navegação. Para que apareça, você deve escrever o seguinte:
func mapTemplate( _ mapTemplate: CPMapTemplate, startedTrip trip: CPTrip, using routeChoice: CPRouteChoice ) { self.navigationSession = self.mapTemplate.startNavigationSession(for: trip) }
CPNavigationSession - uma classe com a qual você pode exibir alguns elementos da interface do usuário necessários apenas no modo de navegação.
Para exibir a manobra, você deve:
let maneuver = CPManeuver() maneuver.symbolSet = CPImageSet( lightContentImage: icon, darkContentImage: darkIcon ) maneuver.instructionVariants = [". "] maneuver.initialTravelEstimates = CPTravelEstimates(…) self.navigationSession?.upcomingManeuvers = [maneuver]
Então, na tela do rádio, temos o seguinte:

Para atualizar a gravação para manobra, você deve:
let estimates = CPTravelEstimates(…) self.navigationSession?.updateEstimates(estimates, for: maneuver)
Isso simplesmente funciona!
Quando a funcionalidade básica do navegador estava pronta, decidi mostrar esse ofício em uma apresentação interna. A apresentação foi um sucesso: todos tiveram a ideia de concluir, testar e iniciar o navegador o mais rápido possível.
Antes de tudo, pedimos uma unidade principal real com o suporte do CarPlay. E então, como se costuma dizer, o calor começou.

Perfis de Provisão
Devido à adição de uma nova chave de capacidade, os perfis precisam ser regenerados. No desenvolvimento normal, não pensamos nisso, porque o Xcode fará tudo sozinho. Mas não no caso de uma chave privada.
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.
Ele também quebrou nosso IC, porque, para a distribuição local de versões de aplicativos, usamos uma conta corporativa, na qual não solicitamos permissão para desenvolver o aplicativo para o CarPlay. Mas esta é uma história completamente diferente.
Depuração
Você pode se conectar ao CarPlay via Bluetooth ou Lightning. A prática mostra que o segundo método é muito mais popular. Nosso rádio no Bluetooth não sabia como, portanto, durante o desenvolvimento, tive que usar a depuração de Wi-Fi. Se você tentou em projetos mais difíceis do que o hello world, então você sabe o que diabos é.
E para quem ainda não tentou, digo:Eu coletei o aplicativo por fio no telefone e, somente então, conecte o telefone ao CarPlay, via Wi-Fi, carreguei no telefone e executei por vários minutos.
A cópia do aplicativo para o telefone demorou cerca de 3 minutos, iniciando o aplicativo por cerca de um minuto e somente depois de iniciar a parada nos pontos de interrupção, apenas 15 segundos depois.
E então ficou muito interessante para mim o motivo pelo qual a Apple não criou nenhum DevKit (para que a Apple funcione e é tudo). Não era muito conveniente montar um suporte de teste sem ele. Até agora, uma vez a cada duas semanas, algo cai - você precisa se lembrar pelas fotos em que colar. É bom que o administrador, ao montar esse estande, tenha dito o que e por quê.
A melhor estrutura que já criamos
No final, quando tudo foi montado em um dispositivo real, ficou claro que o recurso "2GIS for CarPlay" seria definitivamente. É hora de fazer beleza.
Problemas na janela de visualização
Era necessário configurar a viewport do mapa para desenhar rotas na área sem controles desnecessários, e não apenas no meio. Em resumo, para fazer com que pareça diferente:

E assim:

Eu esperava obter algum tipo de layoutGuide com a área visível atual. Para que ele leve em consideração a barra de navegação, a visualização com a rota e os controles no mapa. Na verdade, eu não recebi nada. Ainda não está claro como configurar a viewport, portanto, temos um código rígido como:
let routeControlsWidth = self.view.frame.width * 0.48 let zoomControlWidth = self.view.frame.width * 0.15
Construção da passagem não apenas entre dois pontos
Na primeira versão, decidimos levar nosso rubricador feito através do CPGridTemplate:

Favoritos e Casa / Trabalho através do CPListTemplate.

E pesquisa no teclado através do CPSearchTemplate:

Não mostrarei o código sobre modelos, pois é simples e a documentação sobre ele está bem escrita (pelo menos sobre algo).
No entanto, vale mencionar quais problemas foram descobertos ao trabalhar com eles.CPInterfaceController pode na navegação semelhante ao UIKit. isto é
self.interfaceController.pushTemplate(listTemplate, animated: true) self.interfaceController.presentTemplate(alertTemplate, animated: true)
Mas se você tentar executar, por exemplo, CPAlertTemplate, você receberá nos logs que CPAlertTemplate só pode ser representado modalmente.
Não está claro por que a Apple não escondeu a lógica das tranches sob o capô sem ter feito uma interface como:
self.interfaceController.showTemplate(listTemplate, animated: true)
Ele também quebrou a capacidade de usar os herdeiros do CPTemplate, como controladores no UIKit.
Ao tentar, por exemplo, colocar seu herdeiro na pilha de modelos, você obtém o seguinte:
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 )}'
Teste e bugs
Testado por artemenko-aa . Um dos primeiros erros que ele encontrou, ainda não conseguimos consertar.
O fato é que, quando você desconecta o telefone do rádio CarPlay, o Watchdog esporadicamente nos prega - sem explicar o motivo. Mesmo os syslogs abertos, nada está claro. Portanto, se você tem uma idéia de como corrigir ou entender o motivo, sinta-se à vontade para comentar.
O próximo bug estava no mesmo lugar, mas com um comportamento especial. Escrevi acima que o método didDisconnect de CPApplicationDelegate é chamado quando o telefone é desconectado do CarPlay. E neste método, retornamos o cartão da tela do rádio de volta ao aplicativo principal. Imagine quantos problemas teríamos se esse método não fosse chamado pelo menos uma vez em cada cinco.
Ficou claro que este é um problema do iOS, e não especificamente do nosso aplicativo, pois todo o sistema acreditava estar conectado ao CarPlay.

Eu até relatei como radar (como todos os outros bugs). Me pediram para soltar logs com esse perfil, mas não consegui responder ao suporte por um tempo, então eles fecharam o radar.
Como a Apple não planejava fazer nada, o problema tinha que ser contornado por conta própria, pois era reproduzido com bastante frequência.
E então lembrei que a maior parte das conexões com o CarPlay passa pelo Lightning. Isso significa que o telefone está carregando no momento da conexão e, no momento da desconexão, o carregamento cessa. Nesse caso, você pode assinar o status da bateria e descobrir exatamente quando o telefone parou de carregar e desconectou o CarPlay.
O esquema é frágil, mas não tivemos escolha. Fomos por aqui, e funcionou!

Felizmente, essa muleta foi removida do código há muito tempo: os desenvolvedores da Apple consertaram tudo em um dos lançamentos do iOS.
A história de dois editores
O primeiro redirecionamento estava relacionado aos metadados. O texto do editorial dizia que nossa descrição (não notas de versão) não diz que apoiamos o CarPlay. Como você pode imaginar, nem a diretriz de revisão nem o mesmo Google Maps possuíam isso. Não discutimos (porque geralmente é mais longo do que editar os metadados), copiamos a linha das Notas da versão para a Descrição e começamos a aguardar uma nova revisão.
O segundo redirecionamento aconteceu por causa da lista de cidades. O 2GIS possui um recurso muito interessante - modo offline completo de operação. Esse recurso nos atingiu na perna.
Ao conectar um aplicativo sem uma cidade estabelecida ao CarPlay, não mostramos o mapa, porque não há nada para mostrar. E para isso estávamos programados. A solução foi simples: um alerta sem botões, que indica que você precisa fazer o download da cidade.

Sobre o que você não pode falar
Movimento do mapa de gestos
Na mesma época, saiu o navegador do Google Maps no CarPlay do Google Maps - e era possível mover o mapa com gestos pela tela. APIs privadas, pensei, isso é óbvio! Os caras do Google vieram de um prédio próximo e disseram o que precisavam. Afinal, a documentação diz:
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).
No entanto, ainda decidi me certificar e fui pesquisado no Google, embora fosse quase inútil, porque não havia artigos técnicos sobre o CarPlay Navigation Apps. No entanto, consegui encontrar algo útil e, de repente, no site da Apple .
Nas diretrizes, encontrei um vídeo que diz que a documentação está mentindo descaradamente. O vídeo mostra como você ainda pode arrastar o mapa com gestos. Percebi que não entendia nada, e a única coisa que me restava era abrir o CarPlay.framework e revisar todos os arquivos .h.
E eis que eis! Encontro no CPMapTemplate seu delegado CPMapTemplateDelegate, no qual existem três métodos que parecem gritar que, se você implementá-los, poderá obter o controle dos gestos do mapa.
3 métodos/ * Chamado quando um gesto de panorâmica é iniciado. Não pode ser chamado quando conectado a alguns sistemas CarPlay.
/
função pública opcional mapTemplateDidBeginPanGesture (_ mapTemplate: CPMapTemplate)
/ * Chamado quando um gesto de panorâmica é alterado. Não pode ser chamado quando conectado a alguns sistemas CarPlay.
/
função pública opcional func mapTemplate (_ mapTemplate: CPMapTemplate, didUpdatePanGestureWith Translation translation: CGPoint, speed: CGPoint)
/ * Chamado quando um gesto de pan termina. Não pode ser chamado quando conectado a alguns sistemas CarPlay.
/
função pública opcional func mapTemplate (_ mapTemplate: CPMapTemplate, didEndPanGestureWithVelocity speed: CGPoint
)
Eu os implementei e executei o aplicativo em um simulador - nada funcionou. Não tendo tempo para ficar chateado, percebi que o simulador pode ser da mesma qualidade da documentação e o coloquei no dispositivo. Tudo começou, a felicidade não tinha limites!
Curiosidade: um rádio CarPlay precisa de um quarto da tela para entender que um gesto de panorâmica foi iniciado. Quero observar que o UIPanGestureRecognizer precisa de apenas 10 pontos.
Uniformidade da interface do usuário em diferentes gravadores de rádio
Recebemos um apelo em apoio: o usuário tem apenas um sajest rastejando na pesquisa, embora possa ter havido mais. É estranho, pensei, porque em todas as telas apenas uma linha se encaixa. Solicitaram uma captura de tela:

E isso é completamente diferente da interface do usuário do CPSearchTemplate que mostrei acima. E isso deve ser levado em consideração durante o desenvolvimento, embora ainda seja impossível entender quantas células na placa abaixo podem caber na tela.
Controle de limite de velocidade
Examinamos as estatísticas e percebemos que elas usam o navegador para CarPlay e precisamos trazê-las pelo menos para o nível do navegador no aplicativo principal. Primeiro, decidimos adicionar o controle de limite de velocidade. Claro, houve alguns problemas.
Pergunta número um: onde colocar?
Vasculhando os arquivos .h no CPWindow novamente, encontrei um curioso layoutGuide:
var mapButtonSafeAreaLayoutGuide: UILayoutGuide
E isso acabou sendo o que precisávamos. Nosso controle se encaixa perfeitamente:


Pergunta número dois: isso é geralmente legal?
O fato é que tecnicamente o controle está na visão de base. E a vista base de acordo com a documentação não pode conter nada, exceto um mapa:
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.
Mas os revisores sentiram a nossa falta na AppStore, o que significa que os controles relacionados à navegação ainda podem ser incorporados.
Pesquisa por voz


De uma maneira boa, esse recurso precisou ser executado antes de tudo, mas acumulamos várias tarefas da dívida técnica que impediram a implementação da pesquisa por voz no CarPlay. E essa tarefa não era tão simples quanto parecia.
O primeiro problema: animações. O fato é que no CPVoiceControlTemplate não há como fazer animações padrão. A animação para reconhecimento de voz e pesquisa teve que ser coletada quadro a quadro das imagens e indica quanto tempo elas passaram.
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)
Parece, como você pode imaginar, não realmente, mas não quero aumentar o tamanho do aplicativo.
O segundo problema: acessos. Alertas para acesso ao microfone e reconhecimento de fala são exibidos no visor do telefone. Eu tive que escrever no visor do rádio que o usuário precisa pegar o telefone, dar permissão e só então usar o navegador no rádio. Muito confortável!
Carros com volante à direita.
Foi-nos enviada uma captura de tela na qual a interface do usuário de todo o aplicativo foi virada de cabeça para baixo!

E, é claro, a janela de visualização do mapa permaneceu da maneira que a codificamos, porque ninguém esperava que houvesse uma configuração separada para os carros com volante à direita. Não encontrei como contornar isso “corretamente”, mas notei que, como nosso controle de limite de velocidade está no layoutGuide para controles de mapa, ele foi movido para o lado esquerdo.
Ultrafix não demorou a chegar. Eles fizeram isso com grosseria, mas funciona.
let isLeftWheelCar = self.speedControlViewController.view.frame.origin.x > self.view.frame.size.width / 2.0
Eu realmente espero que exista uma solução certa e simplesmente não a li.
Isso é tudo para mim. Se de repente você planeja tornar seu navegador no CarPlay, lembre-se de que a documentação e a estrutura são imperfeitas. A plataforma é completamente nova, ninguém sabe de nada, e a Apple não tem pressa em compartilhar conhecimento.