Repositorios

API Rest Demo

APIs Públicas

GitHub

Repositorio 100% nativo en Swift en UIKIt en el que se realizan conexiones a diferentes API Rest. Para cada API he utilizado 3 diferentes patrones para manejar la asincronía:

  • Patrón callback
  • Librería Combine
  • Modelo de concurrencia Async-await

Math Tools API

Proporciona en formato json un número generado de manera aleatoria y su detalle como su equivalente en Binario, su version en Romano, Chino, si es primo, perfecto, triangular, etc.

Para la gestión de la asincronía se utiliza Combine, el patrón callback y URLSession: 

   var subscribers = Set<AnyCancellable>()
    
    func getNumberOfTheDay(url: URL = .numbersURL, _ completion: @escaping (Number) -> ()) {
        URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .sink { completed in
                switch completed {
                case .finished: break
                case .failure(let error): print("show \(error)")
                }
            } receiveValue: { data in
                let number = try! JSONDecoder().decode(Number.self, from: data)
                completion(number)
            }.store(in: &subscribers)
    }

Chuck Norris Facts API

Proporciona en formato json un chiste aleatorio relacionado el legendario Chuck Norris. 

Implementación

Patron Callback  y URLSession para la conexión a la API ChuckNorrisNetwork

///#Patrón Callback JSON Genérico
func fetchJson<JSON:Codable>(url: URL, 
                             type: JSON.Type, 
                             session: URLSession, 
                             callback: @escaping ((Result<JSON,PersistenceError>) -> Void)) {
     
     session.dataTask(with: url) { data, response, error in
         guard error == nil else {
             return callback(.failure(.general("error General")))
         }
         guard let response = response as? HTTPURLResponse, 
                response.statusCode == 200 else {
             return callback(.failure(.status))
         }
         guard let data = data else {
             return callback(.failure(.data))
         }
         do {
             let result = try JSONDecoder().decode(JSON.self, from: data)
             callback(.success(result))
         } catch let error {
             callback(.failure(.json(error.localizedDescription)))
         }
     }.resume()
 }

Patrón callback para la notificación del cambio en de los valores en las propiedades.

func updateTableView() {
       modelLogic.tableAction = { [weak self] _ in
           RunLoop.main.perform {
               self?.favoritesJokesTableView.reloadData()
           }
       }
}

Persistencia de datos con un archivo JSON que se guarda en carpeta de documentos de la app. 

TableView para los items persistidos

Unit Tests del ModelLogic y un Mock de URLSession gracias a URLPRotocol ChuckURLSessionMock

API Découpage administratif

Proporciona información sobre la división administrativa de regiones, departamentos y municipios en Francia. Datos por municipio como código postal, población. 

Implementación

Combine y URLSession para la conexión a la API CommunesNetwork

Combine para la notificación del cambio en de los valores en las propiedades. 

  override func viewDidLoad() {
        super.viewDidLoad()
        
        self.clearsSelectionOnViewWillAppear = false
        self.navigationItem.rightBarButtonItem = self.editButtonItem
        
        modelLogic.fetchRegions()
        
        ///Actualiza la tableView` cuando ha descargado las *regions*
        modelLogic.persistence.subject
            .sink { [weak self] _ in
                self?.tableView.reloadData()
            }.store(in: &subscribers)
    }

protocol CommunesFetcher {
    var session: URLSession { get }
    var subject: PassthroughSubject<String,Never> { get }
    func valuesReceived()
    func getJSON<JSON:Decodable>(url: URL, type: JSON.Type, receiveValue: @escaping (JSON) -> ())
    
}

TableView para los listados

Unit Tests del ModelLogic con un Mock en la petición a red CommunesUnitTests

final class CommunesNetworkMock: CommunesFetcher {
    var session: URLSession
    var subject: PassthroughSubject<String, Never>
    func valuesReceived() {
    }
    
    init(subject: PassthroughSubject<String, Never> = PassthroughSubject<String, Never>(), session: URLSession = .shared) {
        self.subject = subject
        self.session = session
    }
    
    func getJSON<JSON>(url: URL, type: JSON.Type, receiveValue: @escaping (JSON) -> ()) where JSON : Decodable {
        switch url {
        case .regionsURL: receiveValue(Region.samples() as! JSON)
        case .departementsURL: receiveValue(Departement.samples() as! JSON)
        case .franceCommunes: receiveValue(Commune.samples()as! JSON)
        default: break
        }
    }
}

Dice Bear (Fun Emoji) 

API que genera un avatar personalizable en formato SVG, PNG o JPG, con opciones como color de fondo, ojos, boca, tamaño, rotación, etc. 

Implementación

Hecho con el modelo de concurrencia Async-await

func getEmojiWithOptions(_ model: DiceBearModel) async throws -> UIImage {
        try await fetchImage(url: DiceBearModel(
            funEmojiWithBackgroundColor: model.backgroundColor,
            backgroundType: model.backgroundType,
            eyes: model.eyes,
            mouth: model.mouth).url, session: session)
    }

NotificationCenter para la notificación del cambio en de los valores en las propiedades. 

Permite la persistencia de datos través de CoreData. APIRestDemoDataBase

//MARK: DiceBear
extension APIRestDemoDataBase {
    func fetchEmojis() throws -> [DiceBearEmojiModel] {
        let request = EmojiEntity.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(keyPath: \EmojiEntity.createdAt, ascending: false)]
        do {
            let emojis = try context.fetch(request)
            return emojis.compactMap { $0.emojiModel }
        } catch {
           throw DataBaseError.fetch
        }
    }
    
    func insert(emoji: DiceBearEmojiModel) throws {
        let newEmojiEntityItem = emoji.getEmojiEntityItem(from: emoji, in: context)
        context.insert(newEmojiEntityItem)
        do {
            try context.save()
        } catch {
            throw DataBaseError.insert
        }
    }
}

CollectionView para mostrar los elementos persistidos

Países

Implementación

Hecho con la funcionalidad Continuations que permite crear un puente entre la función de red con callback para convertirlo a un código asíncrono (async-await). Countries Network

Diferentes APIs de las que se obtiene un listado de países ( 250 ) y a partir de esa información se consulta en otras APIs para la descarga de la bandera del icono de la tabla, la bandera en tamaño 128×96, y una cuarta API con APIKey que permite la consulta de la regiones y ciudades del país seleccionado.

Interfaz de red que implementa un ApiKey para el acceso a las regiones y ciudades.

 static func cities(for ccaCode: String, region: String) -> URL {
        //Validar que haya 3 letras mínimo
        let components = region.components(separatedBy: " ")
        let first = components.first?.prefix(3) 
        let last = components.last?.prefix(3)
        
        let code = last ?? first ?? region.prefix(3)
        let hint = code.count < 3  ? "123" : code //"123" == No region hint
        
        return URL(string: "http://battuta.medunes.net/api/city/\(ccaCode.lowercased())/search/?region=\(hint)&key=\(BATAK)")!
    } 

La APIKey la he almacenado en un json en el documentsDirectory, evitando así ponerla en código

extension URL {
  static let TK = URL.documentsDirectory.appending(path: "TK.json")
}


struct TK: Codable {
    let name: String
    let value: String
}

var BATAK: String {
    if let data = try? Data(contentsOf: .TK),
       let keys = try? JSONDecoder().decode([TK].self, from: data),
       let value = keys.first(where: { $0.name == "BATTK" } )?.value {
        return value
    } else {
        return ""
    }
}

NotificationCenter para la notificación del cambio en de los valores en las propiedades. 

MapKit para mostrar la localización de la ciudad elegida y calcular la distancia hasta la localización del usuario CountryDetailViewController

Marvel Characters

API que obtiene de la API Developer Marvel un listado de imágenes de los personajes, a los que se les puede marcar como favorito.

Implementación

Hecho con el modelo de concurrencia Async-await 

NotificationCenter para la notificación del cambio en de los valores en las propiedades. 

NotificationCenter.default.addObserver(forName: .marvelCharacters, object: nil, queue: .main) { [weak self] _ in
            self?.collectionView.collectionViewLayout.invalidateLayout()
            self?.collectionView.reloadData()
            if let snapshot = self?.modelLogic.snapshot {
                
                self?.dataSource.apply(snapshot, animatingDifferences: true)
                self?.activityIndicator.stopAnimating()
            }
        }

Las CollectionViews están implementadas con la clase DiffableDataSource para la gestión de los elementos. MarvelCharactersModelLogic

El diseño de las celdas de las CollectionViews está implementado en SwiftUI gracias el UIHostingConfiguration MarvelCharactersCollectionViewController

lazy var dataSource: UICollectionViewDiffableDataSource<Int,MarvelCellCharacter> = {
        UICollectionViewDiffableDataSource<Int,MarvelCellCharacter>(collectionView: collectionView) { [self] collectionView, indexPath, character in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "marvelCharacterCell", for: indexPath)
            
            cell.contentConfiguration = UIHostingConfiguration {
                MarvelCharacterCellView(character: character)
            }
            return cell
        }
    }()

Grupos de tareas permiten la concurrencia de  Async-await en la devolución de las imágenes de los personajes MarvelCharactersNetwork

Interfaz de red que implementa un timestamp, ApiKey y hash para el acceso a la API