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.
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.
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 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.