PWA no iOS: Como deixá-lo mais Nativo

Dicas e truques de como melhorar a UX de PWA no iOS!

Eduardo Rabelo
9 min readMay 28, 2018
Suporte básico chegou no iPhone, e agora? (Créditos do banner)

Em 30 de Março, a atualização do iOS 11.3 foi lançada com suporte para recursos básicos de PWA em iPhones e iPads — Service Workers e arquivos de manifesto do aplicativo. É ótimo ter esse suporte, mas a experiência do usuário com PWA no iOS ainda não é perfeita.

Isso significa que muitos dos PWAs lançados ainda estão tendo sérios problemas nos dispositivos da Apple, ao mesmo tempo, eles fornecem uma experiência quase nativa no Android. Na comunidade frontend, comentários de decepção já foram levantados, e a lista de problemas e bugs é longa. Não perca a sua esperança! Abaixo, você encontrará algumas dicas sobre como corrigir esses problemas e tornar o seu PWA o mais próximo possível de um aplicativo nativo no iOS.

O que há de errado com o suporte de PWA no iOS?

Na última atualização do iOS, a Apple adicionou suporte para Service Workers e arquivos de manifesto do aplicativo. Agora você pode aproveitar o armazenamento em cache com os service workers e fazer o seu PWA funcionar sem conexão com a Internet. Vamos lembrar — este é um requisito básico dentro da definição de PWA. E infelizmente há algumas desvantagens na implementação da Apple.

O suporte a Service Workers é muito limitado em comparação com o Android. Você só pode persistir dados do aplicativo e armazenar em cache seus arquivos (sem tarefas em segundo plano). Há também um limite de 50MB e “algumas semanas” para o armazenamento.

O iOS 11.3 também introduziu suporte para o arquivo de manifesto. Mas nossos testes mostraram que está longe de ser perfeito. Os ícones não estão funcionando perfeitamente (ou nem funcionam) e não há suporte para a tela de inicialização — você terá apenas uma tela em branco quando o aplicativo estiver sendo carregado. O aplicativo é recarregado toda vez que sai do segundo plano e não há suporte para notificações por push e muitas outras funcionalidades que são essenciais para um aplicativo para dispositivos móveis. Para resumir, o UX em geral, é muito ruim!

E daí? Quer dizer que fornecer uma experiência quase nativa no PWA no iOS não é possível?

Felizmente, há muita coisa que você pode fazer para melhorar a aparência do seu aplicativo e fornecer uma melhor UX. Com alguns truques simples, você pode criar um aplicativo que, em muitos casos, será indistinguível de um aplicativo nativo.

Acredito que os PWAs são o futuro da Web inclusiva, performática e intuitiva, e é por isso que eu gostaria de compartilhar algumas dicas para superar as limitações do iOS em termos de PWA.

1. Torne o ícone do aplicativo excelente novamente (em todos os dispositivos)

Descobrimos que o iOS não usa ícones do manifesto e faz com que um atalho para o seu aplicativo na tela inicial se pareça muito ruim. Existe uma solução simples para superar esse problema, basta adicionar a meta tag apple-touch-icon com a imagem apropriada. Mas evite ícones com transparência, eles não funcionarão.

<!-- coloque isso dentro de `head` -->
<link rel="apple-touch-icon" href="touch-icon-iphone.png">
<link rel="apple-touch-icon" sizes="152x152" href="touch-icon-ipad.png">
<link rel="apple-touch-icon" sizes="180x180" href="touch-icon-iphone-retina.png">
<link rel="apple-touch-icon" sizes="167x167" href="touch-icon-ipad-retina.png">

Agora seu aplicativo ficará perfeito desde o começo!

2. Corrija a tela de inicialização

A tela de inicialização é exibida antes que o aplicativo esteja totalmente carregado e pronto para uso. Infelizmente, o iOS não suporta a tela de inicialização gerada a partir do manifesto, como no Android — em vez disso, ele mostra uma tela branca. Essa definitivamente não é a experiência que gostaríamos de oferecer aos nossos usuários.

Felizmente encontramos a solução descrita na página de desenvolvedores da Apple. A Apple suporta meta tags personalizadas para especificar a tela inicial pré-gerada — apple-touch-startup-image. Então você só tem que gerar imagens splash em tamanhos adequados, a lista você pode encontrar abaixo:

Quando você tem suas incríveis telas de inicialização prontas, a única coisa que resta a fazer é adicioná-las na seção head:

<!-- coloque isso dentro de `head` -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<link href="/apple_splash_2048.png" sizes="2048x2732" rel="apple-touch-startup-image" />
<link href="/apple_splash_1668.png" sizes="1668x2224" rel="apple-touch-startup-image" />
<link href="/apple_splash_1536.png" sizes="1536x2048" rel="apple-touch-startup-image" />
<link href="/apple_splash_1125.png" sizes="1125x2436" rel="apple-touch-startup-image" />
<link href="/apple_splash_1242.png" sizes="1242x2208" rel="apple-touch-startup-image" />
<link href="/apple_splash_750.png" sizes="750x1334" rel="apple-touch-startup-image" />
<link href="/apple_splash_640.png" sizes="640x1136" rel="apple-touch-startup-image" />

Agora você pode ver que aquela tela branca feia, se foi:

Nosso PWA está muito melhor, não acha?? 🎉🚀🤗

3. Crie o seu próprio “Add to home screen”!

No Android, há um pop-up nativo que incentiva o usuário a adicionar um aplicativo a uma tela inicial e informa a ele que nossa página é um PWA. Infelizmente, no iPhone não existe tal coisa, por isso o nosso visitante nem sequer está ciente das capacidades da nossa aplicação. Além disso, no iOS, são necessários até três toques para adicionar o aplicativo à tela inicial.

Mas não se preocupe, nós temos uma solução para isso! Podemos adicionar um pop-up personalizado que indicará que nosso aplicativo pode ser adicionado à tela inicial.

Você pode projetar esse pop-up como desejar, nosso exemplo é mostrado abaixo. A parte difícil é exibi-lo apenas no Safari e não no modo autônomo (quando o aplicativo já está adicionado à tela inicial). Você pode verificar se o seu aplicativo está no modo autônomo com window.navigator.standalone.

Dê uma olhada neste trecho:

// Detecta se o dispositivo está no iOS
const isIos = () => {
const userAgent = window.navigator.userAgent.toLowerCase();
return /iphone|ipad|ipod/.test( userAgent );
}
// Detects if device is in standalone mode
const isInStandaloneMode = () => ('standalone' in window.navigator) && (window.navigator.standalone);
// Verifica se deve exibir notificação popup de instalação:
if (isIos() && !isInStandaloneMode()) {
this.setState({ showInstallMessage: true });
}

Depois de criar o componente pop-up adequado e colar o código de detecção no método de ciclo de vida adequado, ele ficará assim:

Tenha em mente que no iPad o botão de compartilhamento está localizado na parte superior de uma tela, ao lado da barra de endereços, então você deve mudar a localização do popup de acordo com o dispositivo.

4. Persistir tudo

Como o iOS reinicia seu aplicativo em toda inicialização / saida do plano de fundo, você deve manter seu estado por conta própria. Se você usa React e Redux, existem alguns ótimos pacotes para ajudar você nisso — redux-persist e react-router-redux. Para o Vue, você pode usar os semelhantes vuex-persist e vuex-router-sync. Claro, você pode criar sua própria solução, que seria melhor ajustada para o seu caso.

Em React, por padrão, react-router-redux redirecionará o usuário na montagem do seu app para a rota especificada na URL. Como pode ser ok para uso regular, gostaríamos de redirecionar o usuário para a rota que foi persistida em Redux. Este é um exemplo de como conseguir isso:

import { ConnectedRouter, push } from 'react-router-redux';class PersistedConnectedRouter extends ConnectedRouter {
componentWillMount() {
const { store: propsStore, history, isSSR } = this.props;
this.store = propsStore || this.context.store;
if (!isSSR) {
this.unsubscribeFromHistory = history.listen(this.handleLocationChange);
}
// este é o tweak que irá preferir a rota persistida em vez daquela no url:
const location = this.store.getState().router.location || {};
if (location.pathname !== history.location.pathname) {
this.store.dispatch(push(location.pathname));
}
this.handleLocationChange(history.location);
// --
}
}
export default PersistedConnectedRouter;

5. Cuidar da navegação

Para garantir que seu aplicativo seja utilizável no modo autônomo, você precisa verificar se a navegação foi implementada corretamente. Nos dispositivos da Apple, não há um botão “Voltar”, portanto, você precisa garantir que o usuário possa voltar de qualquer tela usando a navegação integrada ao seu aplicativo.

Você pode fazer isso exibindo o botão voltar ou adicionando menus adicionais se estiver no iOS.

6. Modo offline

Para fornecer a verdadeira experiência nativa, você deve estar preparado para uma conexão fraca ou sem conexão alguma. Em alguns casos simples, os dados de armazenamento persistentes serão suficientes, mas é nesse ponto que podemos fazer uso da nova API de Service Workers no iOS — podemos armazenar em cache solicitações de rede, por exemplo, chamadas de API.

Para aproveitar o armazenamento em cache com service workers, você pode usar o código de exemplo fornecido pelo Google. Se quiser saber mais sobre o código abaixo, leia o artigo em link.

// source: https://googlechrome.github.io/samples/service-worker/basic/
/*
Copyright 2016 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Nomes dos dois caches usados ​​nesta versão do service worker.
// Altera para v2, etc. quando você atualizar algum dos recursos locais, o que
// por sua vez aciona o evento de instalação novamente.
const PRECACHE = 'precache-v1';
const RUNTIME = 'runtime';
// Uma lista de recursos locais que sempre queremos que sejam armazenados em cache.
const PRECACHE_URLS = [
'index.html',
'./', // Alias for index.html
'styles.css',
'../../styles/main.css',
'demo.js'
];
// O evento de instalação que cuida do precaching dos recursos que sempre precisamos.
self.addEventListener('install', event => {
event.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(self.skipWaiting())
);
});
// O evento de ativação que cuida da limpeza de caches antigos.
self.addEventListener('activate', event => {
const currentCaches = [PRECACHE, RUNTIME];
event.waitUntil(
caches.keys().then(cacheNames => {
return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
}).then(cachesToDelete => {
return Promise.all(cachesToDelete.map(cacheToDelete => {
return caches.delete(cacheToDelete);
}));
}).then(() => self.clients.claim())
);
});
// O evento de busca de recursos exibe respostas para recursos de mesma origem de um cache.
// Se nenhuma resposta for encontrada, ele preencherá o cache em tempo de execução com a resposta
// da rede antes de retornar para a página.
self.addEventListener('fetch', event => {
// Ignora solicitações de origem cruzada, como as do Google Analytics.
if (event.request.url.startsWith(self.location.origin)) {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return caches.open(RUNTIME).then(cache => {
return fetch(event.request).then(response => {
// Coloque uma cópia da resposta no cache em tempo de execução.
return cache.put(event.request, response.clone()).then(() => {
return response;
});
});
});
})
);
}
});

Depois de definir os caminhos adequados para os recursos que você gostaria de armazenar em cache e registrar o Service Worker, você deve estar pronto para o modo offline! Também é bom indicar que o aplicativo está offline — você pode adicionar eventos para escutar quando estiver online/offline para exibir uma notificação adequada.

componentDidMount() {
window.addEventListener('online', () => this.setOnlineStatus(true));
window.addEventListener('offline', () => this.setOnlineStatus(false));
}
componentWillUnmount() {
window.removeEventListener('online');
window.removeEventListener('offline');
}
setOnlineStatus = isOnline => this.setState({ online: isOnline })

E assim, o nosso resultado final:

Questões pendentes

Essas dicas ajudarão você a fornecer uma UX praticamente nativa para o seu PWA no iOS. O fato é, ainda existem alguns problemas que poderiam ser resolvidos. Ainda não temos suporte para:

  • Sincronização de plano fundo
  • Notificações via Push
  • Algumas APIs como o TouchID, o ARKit, o In App Payments e o Split View no iPad
  • Bloqueio de orientação
  • Mudança de cor na barra de status
  • Tela de aplicativo adequada no gerenciador de tarefas (que mostraria a tela atual do aplicativo, não a imagem inicial)

Além disso, você deve ter em mente que os caches e todos os dados armazenados localmente não são compartilhados entre o Safari, o WebView e o Web.App. Então, depois de adicionar seu aplicativo para a tela inicial, o usuário terá que, por exemplo, efetuar login novamente, o que pode causar problemas de UX em alguns casos.

Tenha uma ideia? compartilhe abaixo!

⭐️ Créditos

--

--