Cara, cadê minha view? Unknown class no iOS 12

Quando o XCFramework foi anunciado, e o “Build Libraries For Distribution” foi colocado a disposição do desenvolvedor, tudo era perfeito demais para ser verdade. Acabar com a ”gambiarra” dos fat frameworks, permitir um único pacote com várias arquiteturas, simplificar builds. Tudo mágico. E o melhor de tudo: você poderia utilizar mesmo mesmo suportando versões anteriores do iOS. Maravilhoso, não?

Sim, se você não tem que lidar com storyboards e views customizadas. 🥲

Cara, cadê minha view?

É isso mesmo que você leu! Sua view, que aparece linda, reluzente e totalmente funcional, simplesmente fica invisível para o storyboard/xib no iOS 12.

Mas calma, não é algo tão trivial assim, e eu vou explicar o que acontece.

Antes de mais nada, vamos pra uma pequena receitinha de bolo para explicar como reproduzir esse erro.

  1. Crie um framework, adicione uma struct, e altere a flag Build Libraries for Distribution para YES.
  2. Importe esse framework para o projeto do seu app
  3. Crie uma custom view (usando XIB ou View Code, fica a seu critério) e adicione ele em um XIB ou Storyboard.
  4. Na classe da sua custom view, adicione a struct que você criou no framework anterior.
  5. Rode seu código em um device ou simulador com iOS 12 ou anterior.

Parece um cenário específico demais pra acontecer no seu dia a dia né? Mas acredite, é mais normal do que você imagina!

Por exemplo: se você tem um framework onde estão seus Models (que geralmente são structs), e usa numa View que você criou pra passar ou guardar algum dado, você já corre o risco de ter esse problema!

Se você percebe que uma View não é apresentada, e esse problema acontece apenas no iOS 12, dê uma olhadinha nos logs do seu app. Será que você vai encontrar algo como a mensagem abaixo?

A imagem mostra um log do console do lldb, informando o erro “Unknown class _TtC15TesteFramework212MyCustomView in” in Interface Builder file.

Pois é. Você lê a mensagem e já automaticamente pensa: “MALDITO SEJA INTERFACE BUILDER”. Mas calma lá companheiro…

Interface Builder: a causa do problema ou apenas mais uma vítima?

O Interface Builder aqui é tão inocente nessa história quanto você. Vamos entender então o porquê.

O Interface Builder diz que a classe é desconhecida pra ele. Mas ela está lá. Você consegue usar. Você consegue acessar. Então por que não é conhecida?

Por que não damos uma olhada no runtime do objc pra ver o que ele nos diz?

E não é que ela era desconhecida mesmo? Mas espera lá. Se eu pedir pra ele identificar a classe, no formato “Módulo.NomeDaClasse”, ele encontra ela. E depois disso, se eu tentar novamente identificar a classe, ele encontra! Mas como assim?

Será que é assim também no iOS 13? Vamos ver abaixo.

Sim, no iOS 13 ele encontra a classe de primeira. Sem precisar de workarrounds. Sem problemas.

Mas o que diabos está acontecendo aqui????

A explicação lógica

Quando você gera um framework com Build Libraries for Distribution, você está dizendo que o framework deve habilitar a função do ABI Stability do Swift. Essa função gera um arquivo swiftinterface, literalmente um arquivo texto, contendo o descritivo das suas classes públicas, de modo que futuras versões do Swift e seu compilador consigam entender o seu conteúdo, e você possa utilizá-lo sem precisar recompilar sua biblioteca. O que é fantástico, mas com um custo. O objc runtime de versões anteriores do SO talvez não estejam tão familiarizados e preparados pra essa tecnologia.

Como essa tecnologia surgiu com o iOS 13 e o Xcode 11, é natural que as versões anteriores não consigam utilizar todos os recursos envolvidos. No entanto, quando isso acontece, o próprio Xcode emite um alerta informando que esse recurso está disponível apenas para o iOS 13 ou superior, mas não é o caso aqui.

Não há muito o que fazer, já que se trata de uma limitação do SO, mas temos algumas soluções “alternativas” para o caso.

As soluções “alternativas”

Temos aqui três possíveis soluções “alternativas” para o caso: ou trocamos nossa struct para class, ou forçamos o objc runtime a encontrar a nossa classe “na mão”, ou deixamos de usar o Build Libraries for Distribution.

Trocando struct por class

A primeira solução é bem auto-explicativa, sem nenhum mistério: substituímos o struct por class.

Dessa forma, seu código que se parece com o abaixo:

public struct MyStruct { … }

ficará parecida com esse:

public class MyStruct { … }

Horrível né? Mas se prepare, porque a outra solução é pior ainda! 😈

Forçando o runtime a encontrar sua classe “na mão”

Há basicamente duas formas de fazer isso. Você pode usar o objc_getClass e passar uma string contendo o nome do seu módulo (target) e o nome da sua classe de custom view (se a custom view estiver no próprio app, o target é o seu app, por exemplo MyApp.MyCustomView), separados por ponto.

if #available(iOS 13, *) { } else {
    objc_getClass(“TesteFramework2.MyCustomViewClass”)
}

Se você não quiser usar o objc runtime, você pode simplesmente usar o NSClassFromString, e passar o nome da classe que você quer carregar, da mesma forma anterior.

if #available(iOS 13, *) { } else {
    NSClassFromString(“TesteFramework2.MyCustomViewClass”)
}

As duas formas presumem que você saiba exatamente quais classes você precisa forçar a serem encontradas, o que pode ser inviável durante o desenvolvimento, mas pode ser útil para corrigir um bug bem pontual.

Deixando de usar o Build Libraries for Distribution

Uma possibilidade também é deixar de usar o Build Libraries for Distribution, setando essa flag para NO no Build Settings.

Aqui temos um ponto de atenção, porque deixar de usar o Build Libraries for Distribution implica em não poder usar Xcframeworks e não usar o ABI Stability; ou seja, você perde duas super funcionalidades por causa dessa decisão.

Pense bem antes de seguir esse caminho, e use mesmo como último recurso.

BÔNUS: deixar de dar suporte ao iOS 12 (???)

Em um mundo ideal, onde flores desabrocham nos campos, pessoas cantarolam com os cabelos ao vento, e unicórnios voam deixando rastros de arco-íris pelo céu, você poderia simplesmente dropar o iOS 12 do suporte do seu app 🌈🦄.

Mas convenhamos, a realidade é outra. Aqui é só tiro, porrada e bomba! 👊💣

Deixar de dar suporte ao iOS 12 significa deixar de suportar dois devices importantes, que tem uma base bem expressiva, principalmente em países emergentes: iPhone 5S e iPhone 6.

Sua base de usuários nesses devices é tão irrelevante que vale a pena dropar essa plataforma? Então essa é uma boa oportunidade.

Só fica a dica aí: a Apple ainda libera correções de segurança pro iOS 12 (a última foi em 14 de junho de 2021). Se a própria Apple ainda trabalha em corrigir brechas de segurança nessa versão, vale mesmo a pena você deixar de dar suporte?

Não acredita? Confira você mesmo!

Se você quiser testar e ver com seus próprios olhos, eu subi um exemplo no Github pra você poder acompanhar AO VIVAÇO!!!

https://github.com/AndreasLS/struct-ib-ios12-bug

Baixe o repositório (e baixe também o simulador do iOS 12) e testa aí. 😉