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.



