Oi, pessoal. Eu sou o Ayumu Kitabayashi, do Japão, e desenvolvo sozinho o minulo, um app de iPhone que tira da tela os Reels do Instagram, os Shorts do YouTube e o "Para você" do TikTok, mas deixa as DMs funcionando. O app é feito em Expo e React Native, então este texto é principalmente para quem vive nesse ecossistema e um dia pensou: "dá pra fazer um app que esconde só uma parte de outro app?".

A resposta curta é: não do jeito que você imagina. Mas existem três caminhos que funcionam, e a escolha entre eles define quase tudo no projeto, da política de privacidade até quanto trabalho de manutenção você vai ter toda semana.

Antes de tudo: o iOS não deixa você mexer no app dos outros

Se você vem do Android, talvez esteja pensando em serviços de acessibilidade, aqueles que leem a tela de outro app e reagem. No iOS isso simplesmente não existe para terceiros.

O que a Apple oferece é a API de Tempo de Uso (FamilyControls, ManagedSettings e DeviceActivity). Com ela você bloqueia um app inteiro, uma categoria inteira ou um domínio inteiro no Safari. Não existe parâmetro para "Instagram, menos a aba de Reels". É tudo ou nada.

Então o único lugar onde dá para esconder um pedaço é na versão web da rede social. Lá é HTML e CSS, e um seletor resolve. A pergunta que sobra é: quem desenha essa página?

  1. O Safari, aplicando as regras de um content blocker
  2. O Safari, rodando o script de uma extensão web
  3. O seu próprio app, dentro de um WKWebView

Caminho 1: content blocker do Safari

Um content blocker é uma lista de regras em JSON. Você entrega a lista, o Safari compila e aplica sozinho. O seu código nunca entra na página.

[{ "trigger": { "url-filter": "^https://www\\.youtube\\.com/" }, "action": { "type": "css-display-none", "selector": "a[href^='/shorts']" } }] 

O ponto forte é de arquitetura, não de performance: a extensão não consegue ler a página. Para um app que fica aberto em cima das suas conversas privadas, isso vale muito.

Agora, os limites que encontrei montando as regras:

  • Só existe display: none. Não dá para mudar outra propriedade. Numa das redes, as abas da versão mobile eram posicionadas pela ordem; quando escondi uma, as vizinhas não se ajustaram e ficou um buraco. Esconder o contêiner deixou um retângulo cinza no lugar. A solução era desativar a aba sem removê-la, e isso pede JavaScript.
  • O url-filter é um regex reduzido. Não tem alternância com |, nem \d, nem quantificador {n}. Duas rotas viram duas regras.
  • O formato não perdoa. Uma chave fora da especificação pode fazer o Safari descartar a lista inteira, sem aviso nenhum na página. Por isso guardo os metadados (de qual rede veio cada regra) num arquivo paralelo, na mesma ordem, e a build falha se aparecer qualquer chave além de trigger e action.

Caminho 2: extensão web do Safari

Com uma extensão web você injeta CSS e JavaScript de verdade. Dá para observar o DOM, desativar botões, redirecionar /reels/ para a caixa de mensagens. Tudo o que o content blocker não faz.

O preço: o script lê a página, inclusive as DMs, se a pessoa estiver nelas. O Safari pede permissão por site e avisa que a extensão pode ver o conteúdo. Para quem instalou um app justamente para usar o celular com mais calma, é um primeiro contato pesado.

A regra que eu seguiria nesse cenário: tudo que cabe num seletor fica no content blocker, e só o que exige lógica vai para o script. Quanto mais coisa no script, menos vale o argumento de privacidade.

O problema que os dois caminhos do Safari têm em comum

Os dois só funcionam dentro do Safari. Só que o hábito que a gente quer quebrar é o polegar indo direto no ícone do Instagram, e esse ícone abre o app nativo, onde extensão de Safari não chega.

Ou seja, a pessoa teria que lembrar de abrir instagram.com no navegador justo no momento em que ela menos lembra de qualquer coisa. No celular, é aí que o plano desmorona.

Caminho 3: o seu app, com react-native-webview

É o que o minulo faz. O app abre a versão web oficial de cada rede num WKWebView (via react-native-webview) e injeta o próprio CSS e JS. O login acontece na página de login da própria rede, dentro desse WebView.

O que faz isso funcionar no iPhone é juntar com o Tempo de Uso: o minulo bloqueia o Instagram nativo, e a tela de bloqueio oferece um botão que leva para o minulo. O hábito do ícone continua, mas termina num lugar sem Reels.

Algumas coisas que aprendi no caminho, pensando em quem está no Expo:

Duas injeções, em momentos diferentes. O CSS entra por injectedJavaScriptBeforeContentLoaded, antes da primeira pintura. Se entrar depois, o Reel pisca na tela antes de sumir. O comportamento entra por injectedJavaScript com um MutationObserver, porque essas páginas continuam adicionando elementos muito depois do carregamento.

SPA não recarrega. A injeção roda uma vez por documento, mas a rede troca de rota sem recarregar. Uma regra que vale só em /explore/ precisa de um script que observe a URL e troque um <style> específico por conta própria.

Seletor quebrado não dá erro. Quando a rede renomeia uma classe, o CSS simplesmente para de casar. Eu conto quantos elementos cada regra encontra e trato como quebrada a regra que fica em zero.

O código injetado roda no mundo da página. O JavaScript da própria rede pode chamar window.ReactNativeWebView.postMessage tão bem quanto o seu. No lado do app, toda mensagem é suspeita: confiro um token gerado para aquela tela e uma lista fechada de tipos aceitos. É só a primeira camada.

User agent atualizado. Eu usava um user agent fixo, de um Safari antigo. Hoje o app informa a mesma versão de Safari do aparelho, para não chamar atenção à toa.

Tempo de Uso no Expo pede dev client. A parte de bloqueio usa a biblioteca react-native-device-activity e extensões nativas (ShieldConfiguration para a aparência da tela de bloqueio, ShieldAction para os botões). Isso não roda no Expo Go; é expo-dev-client e build nativa. E um detalhe que me custou tempo: de dentro da ShieldAction não dá para abrir o seu app direto pela URL. O pedido é ignorado sem erro. A saída foi disparar uma notificação local que a pessoa toca para entrar no minulo.

Uma fonte de regras, três saídas

Para não ficar preso a uma escolha, as regras vivem num único arquivo TypeScript. As que são só seletor podem virar JSON de content blocker, as que precisam de lógica podem virar script, e todas são injetadas no WebView do app. A mesma declaração (urlFilter, css, redirect) é lida pelas três saídas. Foi assim que um bug real ficou visível: o app ignorava o urlFilter enquanto as outras saídas respeitavam, e uma regra escondia coisas em todas as páginas no iPhone.

Resumo

Content blocker Extensão web App com WebView
Lê a página Não Sim Sim
Desativa e redireciona Não Sim Sim
Funciona ao tocar no ícone do Instagram Não Não Sim, com Tempo de Uso
Manutenção Baixa Média Alta

Se o alvo é desktop, comece pelo content blocker e só adicione script onde for inevitável. No iPhone, quem decide é o ícone: a solução tem que estar onde o polegar vai.

Se quiser ver como ficou: minulo. Esconder os Reels e os Shorts é grátis. Perguntas sobre Expo, WebView ou Tempo de Uso, deixa aqui nos comentários.