Recentemente criei uma pequena classe em Swift para trabalhar com objetos paginados. A classe usa generics, portanto não precisa de subclasses para ser usada. Nesse projeto usamos o ObjectMapper para fazer o mapeamento JSON<-> modelo Swift, e inicialmente a classe ficou assim:
import ObjectMapper class Paginated<T: Mappable>: Mappable { var hasNext: Bool? var limit: Int? var page: Int? var data: [T]? required init?(_ map: Map) { } func mapping(map: Map) { hasNext <- map["hasNext"] limit <- map["limit"] page <- map["page"] data <- map["data"] } }
Com isso, o data recebe um array com objetos que podem ser mapeados respeitando algum tipo T que também obedece ao protocolo Mappable, criado pelo ObjectMapper.
Digamos que temos uma lista de produtos vindo no array data e que criamos a classeProduct, que obedece ao protocolo Mappable. Ao receber o JSON, basta transformar para um objeto local assim:
if let paginated = data as? Paginated<Product> { //paginated.data contem array de Product
Mas depois eu passei a usar essa classe em um outro projeto que tinha uma “feature“: o nome do campo array no JSON de resposta do backend não é sempre o mesmo.
Decidi usar enum como forma de forçar segurança de tipo e garantir mais qualidade no código, evitando a “String Oriented Programming“. Criei, então, a variação abaixo:
enum PaginatedItemType: String { case Product case Category func itemsFieldName() -> String { switch self { case .Product: return "products" case .Category: return "categories" } } } class Paginated<T: Mappable>: Mappable { var currentPage: Int? var quantityItems: Int? var quantityPages : Int? var pageSize : Int? var lastPrice: String? var items: [T]? var itemsFieldName = "" required init?(_ map: Map) { if let name = PaginatedItemType(rawValue: String(T)) { itemsFieldName = name.itemsFieldName() } else { fatalError("It's not current possible to paginate") } } func mapping(map: Map) { items <- map[itemsFieldName] currentPage <- map["pagina_atual"] quantityItems <- map["quantidade_itens"] quantityPages <- map["quantidade_paginas"] pageSize <- map["tamanho_pagina"] lastPrice <- map["ultimo_preco"] } }
A linha if let name = PaginatedItemType(rawValue: String(T)) garante que o nome tipo T pode ser usada para inicializar uma variável do tipo PaginatedItemType, ou seja, todo tipo paginado deve estar incluído em PaginatedItemType. Isso força o desenvolvedor – no caso, eu – a corrigir a implementação e falar qual o nome do campo que vem no JSON dentro do método itemsFieldName().
Os cases do enum PaginatedItemType são os nomes das classes que podem ser passados para Paginated, e como pode ser visto no init? (usado pelo ObjectMapper), se não for uma das classes “suportadas” (case definido no enum e o nome do campo definido no método itemsFieldName()), vai dar problema já na compilação.
String(T) consegue converter o nome da classe usada com esse genérico para uma String. Como defini o enum “herdando” de String, posso usar oPaginatedItemType(rawValue:) para iniciar via o nome da classe, mas se o nome da classe não estiver definida em um case, o enum retorna nil.
A princípio, não lembrei um jeito mais direto de converter direto do enum iniciando com o nome do tipo e devolvendo o nome do campo, então resolvi discutir com o pessoal aqui da Concrete Solutions. Foi então que o Daniel sugeriu adotar o protocoloCustomStringConvertible no enum. Tentei, então, esse código:
extension PaginatedItemType: CustomStringConvertible { var description: String { switch self { case .Product: return "products" case .Category: return "categories" } } }
Com a inicialização assim:
if let name = PaginatedItemType(rawValue: String(T)) as? String { itemsFieldName = name } else { itemsFieldName = "fuem" }
Mas não funcionou. A compilação falhava com a seguinte mensagem: Cast from ‘PaginatedItemType’ to unrelated type ‘String’ always fails.
Apesar de adotar o protocolo CustomStringConvertible, não é possível fazer a conversão direta usando um simples type casting. O correto é inicializar uma String passando como parâmetro um objeto que obedeça ao protocolo CutomStringConvertible. Assim, ficamos com:
let name = String(PaginatedItemType(rawValue: String(T)))
Melhorando a implementação com o CustomStringConvertible:
if let page = PaginatedItemType(rawValue: String(T)) { let name = String(page) itemsFieldName = name }
Com isso, acabei com a versão final:
enum PaginatedItemType: String { case Product case Category } extension PaginatedItemType: CustomStringConvertible { var description: String { switch self { case .Product: return "products" case .Category: return "categories" } } } class Paginated<T: Mappable>: Mappable { var currentPage: Int? var quantityItems: Int? var quantityPages : Int? var pageSize : Int? var lastPrice: String? var items: [T]? var itemsFieldName = "" required init?(_ map: Map) { if let page = PaginatedItemType(rawValue: String(T)) { itemsFieldName = String(page) } else { fatalError("It's not current possible to paginate \(String(T))") } } func mapping(map: Map) { items <- map[itemsFieldName] currentPage <- map["pagina_atual"] quantityItems <- map["quantidade_itens"] quantityPages <- map["quantidade_paginas"] pageSize <- map["tamanho_pagina"] lastPrice <- map["ultimo_preco"] } }
Muito mais seguro e flexível, usando alguns truques que não tínhamos em Objective-C (no passado, porque Objective-C tem evoluído e adicionou recentemente suporte agenerics).
Obrigado ao Daniel pela discussão, e espero que gostem.
Qualquer dúvida, sugestão ou comentário, só aproveitar os campos abaixo.