Genéricos e protocolos avançados no Swift.

James Rochabrun Blocked Desbloquear Seguir Seguindo 19 de dezembro https://swapi.co/

Bem, aqui vamos nós de novo, mais uma vez eu gostaria de falar sobre genéricos e protocolos no Swift 4.2.

Alguns dias atrás eu dei-me um pequeno desafio, o objetivo? implementar uma API cliente para SWAPI, a “API de Star Wars”, feita por Paul Hallett , evitando a repetição de códigos o máximo possível, mas ainda mantendo as coisas em bom senso.

Neste tutorial iremos implementar uma API cliente usando Swift Meta Types, Protocolos com tipos associados, herança de protocolo, enums com valores associados e, claro, genéricos, se algum destes tópicos não lhe for familiar eu sugiro parar aqui e verificar estes recursos.

Tipos e Meta Tipos no Swift.

Genéricos em Swift.

Enum com valores associados.

Protocolos com Tipos Associados (PAT).

Ok legal, vamos começar clonando ou baixando este repo , Explorar o projeto e você pode ver que ele contém pastas diferentes. As pastas “Protocols” e “Networking” contêm implementações que eu compartilhei antes e que você pode reutilizar para quase todas as implementações da API do cliente. Não vou entrar em detalhes sobre isso, mas você pode ver o que está acontecendo em detalhes neste post. .

Rede Genérica Baseada em Protocolo usando JSONDecoder e Decodable no Swift 4

Rede Genérica Baseada em Protocolos – Parte 2 JSONEncoder e Encodable for Post request no Swift.

Eu também adicionei uma pasta "Modelos" que contém todos os recursos disponíveis na API SWAPI, como filmes, pessoas, naves espaciais e assim por diante, eu também recomendo dar uma olhada na documentação da API SWAPI para entender sua estrutura.

Na pasta “SwapiWrapper” você pode ver três arquivos, ele contém toda a implementação final (comentada até agora), nós iremos remover o comentário enquanto estivermos avançando.

Vamos começar com “SwapiEndpoint”, se você viu os documentos da API do Star Wars você pode ver que é muito fácil de usar e contém diferentes caminhos para diferentes tipos de recursos, você pode obter listas de Planetas e também procurar por uma nave espacial usando um ID ou uma consulta de nome, sim basicamente praticamente todo o conteúdo do ecossistema de Star Wars!

 enum SwapiEndpoint { 
 case people (_ id: String ?, consulta: String?) 
 case films (_ id: String ?, consulta: String?) 
 planetas de casos (_ id: String ?, query: String?) 
 case species (_ id: String ?, consulta: String?) 
 case starships (_ id: String ?, consulta: String?) 
 case vehicles (_ id: String ?, consulta: String?) 
}

Esse enum será aquele que lida com os endpoints que precisaremos para direcionar os pedidos, vamos fazer agora este enum em conformidade com o protocolo Endpoint (aquele no arquivo Networking).

 extensão SwapiEndpoint: Endpoint { 

 var base: String { 
 return "https://swapi.co" // este é o URL base 
 } 
 var path: String { 
 switch self { 
 case .people (let id, _): retornar "/ api / people /  (id ??" ")" 
 case .films (let id, _): retornar "/ api / films /  (id ??" ")" 
 case .planets (let id, _): retornar "/ api / planets /  (id ??" ")" 
 case .species (let id, _): retornar "/ api / species /  (id ??" ")" 
 case .starships (let id, _): retornar "/ api / starships /  (id ??" ")" 
 case .vehicles (let id, _): retornar "/ api / vehicles /  (id ??" ")" 
 } 
 } 

 var queryItems: [URLQueryItem] { 
 var q = "" 
 switch self { 
 case .people (_, deixe a consulta): 
 q = query ?? "" 
 case .films (_, deixe a consulta): 
 q = query ?? "" 
 case .planets (_, deixe a consulta): 
 q = query ?? "" 
 case .species (_, let query): 
 q = query ?? "" 
 case .starships (_, let query): 
 q = query ?? "" 
 case .vehicles (_, let query): 
 q = query ?? "" 
 } 
 return [URLQueryItem (name: "search", valor: q)] 
 } 
 } 

Por estar em conformidade com o protocolo Endpoint ou qualquer outro protocolo que você precisa para fornecer uma implementação para cada propriedade definida nesse protocolo, talvez você possa fazer tudo isso sem precisar de um protocolo, mas é bom definir um para definir regras sobre o que é necessário um determinado modelo. Agora dentro dessa extensão, também temos um init personalizado que usa os tipos Meta de modelos para instanciar nosso enum.

 init? (T: Decodable.Type, id: String ?, consulta: String?) { 
 interruptor T { 
 case is Film.Type: self = .films (id, consulta: consulta) 
 case is People.Type: self = .people (id, consulta: consulta) 
 case is Starship.Type: self = .starships (id, consulta: consulta) 
 case is Vehicle.Type: self = .vehicles (id, consulta: consulta) 
 case is Species.Type: self = .species (id, consulta: consulta) 
 case is Planet.Type: self = .planets (id, consulta: consulta) 
 padrão: return nil 
 } 
 } 

Ao usar funções genéricas para nosso cliente, passaremos como argumentos o tipo meta de um modelo, quando isso acontecer, nós o usaremos para instanciar nosso enum e obter o caminho correto baseado no tipo do modelo.

Agora vamos ao arquivo “Resources”, você pode ver um protocolo como este…

 Recurso do protocolo: Decodable { 
 tipo associado T 
 var count: Int? { pegue } 
 var next: String? { pegue } 
 var anterior: String? { pegue } 
 resultados var: [T]? { pegue } 
 } 

Este é um protocolo com Tipos Associados e é assim que você define genéricos em protocolos. Ele tem como um tipo associado do tipo "T" definiremos "T" em seguida, mas primeiro você está perguntando qual é a contagem, propriedades próximas e anteriores sobre, bem como a SWAPI retorna os dados quando você executa uma solicitação para um lista completa de recursos, o payload contém um dicionário com as teclas “count”, “next”, “previous” e “results”, algo assim…

 { 
"contagem": 87,
"next": "https://swapi.co/api/people/?page=2",
"anterior": null,
"resultados": [
{
"nome": "Luke Skywalker",
"altura": "172",
"massa": "77",
"hair_color": "blond"
"skin_color": "fair",
"eye_color": "azul"
"birth_year": "19BBY",
"género masculino",
"homeworld": "https://swapi.co/api/planets/1/",
"filmes": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
]
"species": [
"https://swapi.co/api/species/1/"
]
"veículos": [
"https://swapi.co/api/vehicles/14/",
"https://swapi.co/api/vehicles/30/"
]
"naves espaciais": [
"https://swapi.co/api/starships/12/",
"https://swapi.co/api/starships/22/"
]
"criado": "2014-12-09T13: 50: 51.644000Z",
"editada": "2014-12-20T21: 17: 56.891000Z",
"url": "https://swapi.co/api/people/1/"
}
{
"nome": "C-3PO",
"altura": "167",
"massa": "75",
"hair_color": "n / a",
"skin_color": "ouro",
"eye_color": "amarelo",
"birth_year": "112BBY",
"gender": "n / a",
"homeworld": "https://swapi.co/api/planets/1/",
"filmes": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/5/",
"https://swapi.co/api/films/4/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/"
]
"species": [
"https://swapi.co/api/species/2/"
]
"veículos": [],
"naves estelares": [],
"criado": "2014-12-10T15: 10: 51.357000Z",
"editada": "2014-12-20T21: 17: 50.309000Z",
"url": "https://swapi.co/api/people/2/"
}
.... 85 outras Awesome Star wars Characters goes here ..

Agora vamos usar a herança de protocolo para criar um modelo que esteja em conformidade com o recurso que decodificará a carga útil.

 public struct Resources <T: Decodível>: Recurso { 
 var count: Int? 
var next: String?
var anterior: String?
resultados var: [T]?
}

Como você pode ver, este modelo está em conformidade com Resource que está em Decodable, então isso significa que este modelo também está em Decodable e pode ser usado para decodificar JSON, tem uma restrição genérica do tipo Decodable e como definimos o tipo para o Decodable. Tipo de recurso associado "T" … o que isso significa? Bem, isso significa que esse objeto está em conformidade com Decodable, mas também contém uma matriz de modelos que estão em conformidade com Decodable.

Você pode estar se perguntando por que não simplificamos isso com algo assim …

 estrutura pública Resource <T: Decodable>: Decodable { 
 var count: Int? 
var next: String?
var anterior: String?
resultados var: [T]?
}

E é uma pergunta justa, por que introduzir o protocolo com tipos associados e herança de protocolo? Bem, quando eu mostrar as funções na API do cliente, talvez faça mais sentido, espero que sim.

Então, por que todo esse problema em primeiro lugar? Bem, digamos que eu quisesse fazer isso sem genéricos ou protocolos e eu quero uma lista de pessoas dos filmes Star Wars usando o payload que eu compartilhei há alguns momentos atrás, eu terei que fazer algo assim…

 https: //swapi.co/api/people 
struct pública PeopleResults: Decodable {
 var count: Int? 
var next: String?
var anterior: String?
var resultados: [Pessoas]? // People está em conformidade com Decodable.
 } 

Isso funciona muito bem, no entanto, quando fiz outra solicitação, vamos dizer agora que os planetas terão que fazer algo assim, porque a carga parecia a mesma e a única diferença era o tipo na matriz de resultados.

 https: //swapi.co/api/planets 
 struct pública PlanetResults: Decodable { 
 var count: Int? 
var next: String?
var anterior: String?
var resultados: [Planeta]? // planeta está em conformidade com Decodable.
}

Eu terei que fazer isso toda vez para cada tipo de resultado, usando genéricos eu evitei tudo isso e agora eu posso lidar com cada tipo sem criar um novo modelo.

Finalmente, vamos usar todo o nosso código para criar a API do cliente, vá para o arquivo “Vader” (claro que eu queria dar um nome legal)…

 classe Vader: GenericAPI { 
 var session: URLSession 
 init (configuração: URLSessionConfiguration) { 
 self.session = URLSession (configuração: configuração) 
 } 
 conveniência pública init () { 
 self.init (configuração: .default) 
 } 
 private func fetch <T: Decodable> (com solicitação: URLRequest, completion: @escaping (Resultado <T ?, APIError>) -> Void) { 
 buscar (com: request, decode: {json -> T? in 
 guarda deixa recurso = json como? Tou {return nil} 
 recurso de retorno 
 }, conclusão: conclusão) 
 } 
}

Esta API está em conformidade com a API genérica (a da pasta Networking). Ela possui um inicializador e também um método genérico que pega uma requisição e decodifica um modelo contanto que esteja em Decodable, vamos adicionar três métodos aqui e será ALL Precisamos ser capazes de buscar todos os tipos diferentes de resultados como, por exemplo, uma lista de pessoas ou pessoas específicas pesquisando por ID ou "Nome" …

 // Obter uma lista de recursos de qualquer tipo 
 public func get <T: Recurso> (valor _: T. Tipo, conclusão: @escaping (Result <T ?, APIError>) -> Void) { 
 guarda let decodedAssociatedType = value.T.self as? Decodable.Type else {return} 
 guarda deixa resource = SwapiEndpoint (T: decodedAssociatedType, id: nil, consulta: nil) else {return} 
 deixe request = resource.request 
 self.fetch (com: pedido, conclusão: conclusão) 
 } 

 // Obter um Recurso de qualquer tipo pesquisando por ID ex. "1" retornará "Luke Skywalker" 
 public func search <T: Decodível> (valor _: T.Tipo, com IDID: Cadeia, conclusão: @escaping (Result <T ?, APIError>) -> Void) { 
 guarda permite resource = SwapiEndpoint (T: valor, id: id, consulta: nil) else {return} 
 deixe request = resource.request 
 self.fetch (com: pedido, conclusão: conclusão) 
 } 
 // Obtenha um Recurso de qualquer tipo pesquisando por Nanme ex. "r2" retornará "R2-D2" 
// Por que eu preciso desse método tem uma restrição de tipo de recurso e não apenas decodificável? porque a carga útil para isso tem a mesma estrutura que os recursos de List, por favor, verifique os documentos SWAPI.
 public func search <T: Recurso> (valor_: T.Tipo, consulta: seqüência de caracteres, conclusão: @escaping (resultado <T ?, APIError>) -> void) { 
 guarda let decodedAssociatedType = value.T.self as? Decodable.Type else {return} 
 guarda deixa resource = SwapiEndpoint (T: decodedAssociatedType, id: nil, consulta: query) else {return} 
 deixe request = resource.request 
 self.fetch (com: pedido, conclusão: conclusão) 
 } 

Lembre-se de quando introduzimos protocolos de herança e protocolos com tipos associados, bem, se você der uma olhada nas funções com uma restrição de tipo de recurso, você pode ver que estamos decodificando o tipo, mas também acessando seu valor associado em uma linha de código…

 guarda let decodedAssociatedType = value.T.self as? Decodable.Type else {return} 

onde valor pode ser do tipo "R esources <Starship>" e value.T é neste caso "Starship".

Então, novamente, por que todo esse problema? bem, vamos pensar de novo como implementamos API's normalmente você terá algo assim…

 func getStarshipsWith (completion: @escaping (Resultado <Starship ?, APIError>) -> Void) { 
// completion ()
}

depois repita o mesmo para outro tipo de modelo…

 func getPlanetssWith (conclusão: @escaping (Result <Planet ?, APIError>) -> Void) { 
// completion ()
}

e assim por diante, para cada tipo definindo o tipo na assinatura da função e modificando o tipo para cada resultado diferente, além de implementar os métodos para pesquisa etc. Misturando todos esses recursos do Swift, agora você só precisa de três métodos em sua API e você pode reutilizar -los para cada tipo diferente, vamos ver como parece buscar Starships e Planetas usando a mesma função, vá para o arquivo ViewController e descomente o código…

 // Obter uma lista de naves espaciais 
Vader (). Get (Recursos <Starship> .self) {
}
 // Obter uma lista de planetas 
Vader (). Get (Recursos <Planet> .self) {
 } 

A mesma função para modelos diferentes!

Aqui, não estamos apenas tirando o melhor da Swift em nossa vantagem, mas também estamos seguindo as diretrizes da Swift, tornando essas funções legíveis quando usadas.

Eu espero que você ache isto útil.

Grande grito de novo para Paul Hallett pela implementação da API SWAPI, muito obrigada!

Boas festas!