Listando classes no Swift

Listando classes no Swift

Há momentos que precisamos listar as classes no Swift, e às vezes precisamos ser específicos, como "quero todas as classes que herdam da Foo" ou "quero todas as classes que assinam o protocolo Proto". Porém, como fazer isso?
Nessa postagem mostrarei a solução que encontrei e explicarei sobre o funcionamento dela.

A postagem é curta e aborda assuntos místicos, então recomendo que leia escutando remix de Undertale.

Solução

Objective-C Runtime API

Leia a struct ClassInfo. Leu? Você verá que ela é, em grande parte, trivial, porém, preciso ressaltar alguns pontos, como a computed variable superclassInfo. Nela estamos usando a função class_getSuperclass, que é uma Objective-C Runtime API. Em outras partes do código também estamos usando outras Objc Runtime API, que no caso são as class_getName e objc_copyClassList. Mas o que diabos é essa tal de Objective-C Runtime API!? Eu estou programando em Swift!

O Obj-C Runtime é uma Runtime Library. Isso é, uma biblioteca que está linkada em tempo de execução, normalmente usadas para fornecer interfaces básicas de uma linguagem. Isso é importante para o Obj-C, pois algumas decisões precisam ser processadas em tempo de execução por ela ser uma linguagem de tipagem dinâmica, e essa é uma tarefa para a sua runtime library.
Apesar da Obj-C Runtime ser muito importante para o funcionamento em baixo nível, ela também é acessível quando estamos desenvolvendo uma aplicação mais alto nível. Como já é de esperar, com ela conseguimos manipular o baixo nível da linguagem, como por exemplo, obter todas as classes escritas, o que é útil para o nosso problema. Supreendemente, as funções do Obj-C Runtime API funciona muito bem em classes escritas em Swift, mesmo que não herdem do NSObject.
Foge do escopo dessa postagem explicar detalhadamente o funcionamento e uso do Obj-C Runtime API. Caso queira saber mais, recomendo essa postagem.

Há várias funções do Obj-C Runtime API, como pode-se ver na documentação da Apple.

Passando como parâmetro um protocolo

Eu queria escrever uma função chamada de "subscribers" para listar todas as classes que assinam determinado protocolo. Como eu já tinha a função subclasses(of:) funcionando graças às Obj-C Runtime API, ficava trivial desenvolver a subscribers - bastava usar a função objc_copyClassList e pecorrer cada classe perguntando se assina ou não o procolo. Porém... como posso passar um protocol como parâmetro?

Pesquisando no Google, várias pessoas diziam para usar o atributo @objc. Com esse atributo eu torno meu pedaço de código Swift visível para o Obj-C assim como para a Obj-c Runtime API. Um exemplo é o seguinte:

@objc protocol Foo {
    var bar: Int { get set }
}

O protocolo Foo fica visível em Obj-C através de:

@protocol Foo
@property (nonatomic) NSInteger bar;
@end

Porém, para o meu projeto especificamente, eu não podia usar o @objc, pois meus protocolos têm property, e quando um protocolo tem property não pode ser representado pelo Obj-C e, desse modo, não pode-se usar o @objc. Então como resolver isso?

A função subscribers(of:) era mais ou menos assim:

protocol Proto { }
class Foo: Proto { }
class Bar { }

func subscribers(of proto: Protocol) -> [AnyClass] {
    let listClass: [AnyClass] = [Foo.self, Bar.self]

    return listClass.filter { $0 as? proto }
}

print(subscribers(of: Proto.self)) /* not work! */

Passei várias horas (sério, foram várias horas!) olhando para a tela do monitor, filosofando, e tentando. Uma das minhas tentativas foi usando ponteiros. Efeturei várias tentativas usando UnsafePointer e OpaquePointer, para passar o meu protocolo como ponteiro, porém, não consegui.
No Objective-C protocolos possuem uma região reservada na memória e, assim, é possível criar um ponteiro para um protocolo. Porém, não consegui efetuar o mesmo em Swift. Honestamente, eu não sei como os protocolos funcionam no baixo nível do Swift, nem sei se faz sentido um ponteiro para um protocolo. Isso é algo que eu ainda preciso estudar.

E eu estava concentrado em saber o modo correto de declarar o parâmetro proto: Protocol, pois o do código acima não funcionava. Eu não conseguia passar o protocolo Proto, então pensei... Que tal não passar o protocolo, mas sim fazer a verificação fora, usando closures? Então o código ficou assim:

protocol Proto { }
class Foo: Proto { }
class Bar { }

func subscribers(cond protocolCond: (AnyObject) -> Any?) -> [AnyClass] {
    let listClass: [AnyClass] = [Foo.self, Bar.self]

    return listClass.filter { protocolCond($0) != nil }
}

print(subscribers(cond: { $0 as? Proto.Type }))

E funcionou!! Porém, ainda não estava satisfeito. Beleza, funcionou, porém, o código está muito estranho.
Sem ter mais nenhuma ideia em mente, decidi aproveitar o suporte técnico da Apple (já que em breve minha licença vai expirar) e perguntar por alguma solução melhor. Após algumas trocas de mensagens, o engenheiro da apple me passou o seguinte e-mail:

You can declare a function that accepts a Proto.Type, which is the type of all types conforming to Proto. However, Swift does not provide any type checking facilities that accept an expression (that evaluates to a type) so there is no way to check if $0 is a where is an expression. Both is and as require a type on the right side. You can't write this for example:

func filter(classes: [AnyClass], byConformanceToProtoOrDerived proto: Proto.Type) {
    return classes.filter { $0 is proto }
}

error: MyPlayground.playground:10:35: error: use of undeclared type 'proto'
    return classes.filter { $0 is proto }

If the type can be provided as a generic parameter, then you could do this:

func filter<T>(classes: [AnyClass], byConformanceTo: T.Type) -> [AnyClass] {
    return classes.filter { $0 is T }
}

filter(classes: [Foo.self, Bar.self, Baz.self], byConformanceTo: Proto.Type.self)
// Returns: [Foo.Type, Bar.Type]

O que foi bem interessante. Eu até havia pensando em generics, porém, não sabia como se aplicaria a esse problema. E essa é a melhor solução para esse problema, com código bem mais legível e seguro.