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.
- Crie um framework, adicione uma struct, e altere a flag Build Libraries for Distribution para YES.
- Importe esse framework para o projeto do seu app
- Crie uma custom view (usando XIB ou View Code, fica a seu critério) e adicione ele em um XIB ou Storyboard.
- Na classe da sua custom view, adicione a struct que você criou no framework anterior.
- 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?
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í. 😉