Cucinare client iOS per GraphQL

Sono sicuro che ognuno di noi almeno una volta ha avuto problemi con l'API REST. Battaglie eterne con la schiena per il formato API richiesto, diverse richieste per lo schermo e altro ancora. D'accordo che non è raro, ma una routine quotidiana. E più recentemente Tribuna Digital ha lanciato un nuovo progetto: Betting Insider. Inizialmente, il progetto è stato implementato su iOS e Android, e in seguito è iniziato lo sviluppo della versione web. L'API esistente si è rivelata molto scomoda per il web. Tutto ciò ha portato al fatto che abbiamo deciso di organizzare un esperimento e provare GraphQL insieme ad un client di Apollo. Se vuoi familiarizzare con questa tecnologia in iOS più vicino, allora benvenuto sotto il taglio!

Un po 'di GraphQL

GraphQL - (Graph Query Language) è il linguaggio di query e l'istanza per l'elaborazione di queste query. In termini più semplici, questo è il livello tra i nostri server e il client, che fornisce al client ciò di cui ha bisogno dal server. La comunicazione con questo livello avviene direttamente nel linguaggio GraphQL. Se vuoi saperne di più su GraphQL Language e altro, o non sapere nulla, leggi qui e qui . Lì tutto è abbastanza dettagliato e con immagini.

Di cosa abbiamo bisogno?

Per ulteriore lavoro, abbiamo bisogno di NodeJS , Node Package Manager , CocoaPods .
Se si desidera evidenziare query graphql, quindi installare questo plug-in . Inoltre, è necessario scaricare un progetto pre-preparato . E la cosa più importante è ottenere la tua chiave personale. Ecco allora l'istruzione e chiedere qui è un tale Skopje

utente
public_repo
pronti contro termine
repo_deployment
repo: stato
repo: repo_hook
repo: org
repo: public_key
repo: gpg_key


Cosa faremo?

Scriveremo un piccolo client per github, nel quale possiamo trovare i primi 20 repository per qualsiasi parola chiave. Per provare l'API Github e GraphQL, puoi giocare qui .

Impostazione del progetto

Prima di iniziare a scrivere codice, installa alcuni pacchetti e configura il progetto.
La libreria Apollo utilizza il codice che genera il suo strumento apollo-codegen. Installalo usando npm:

npm install -g apollo-codegen

Successivamente, vai alla cartella del progetto e apri Sorgente. Scarica lo schema del tuo server GraphQL e invece di <token> inserisci la tua chiave personale, che avresti dovuto fare sopra:

apollo-codegen download-schema https://api.github.com/graphql --output schema.json --header "Authorization: Bearer <token>"

Creare la cartella GraphQL nella cartella Origine.

Successivamente, è necessario registrare uno script per il nostro generatore di codice:

APOLLO_FRAMEWORK_PATH="$(eval find $FRAMEWORK_SEARCH_PATHS -name "Apollo.framework" -maxdepth 1)"

if [ -z "$APOLLO_FRAMEWORK_PATH" ]; then
echo "error: Couldn't find Apollo.framework in FRAMEWORK_SEARCH_PATHS; make sure to add the framework to your project."
exit 1
fi

cd "${SRCROOT}/Source"
$APOLLO_FRAMEWORK_PATH/check-and-run-apollo-codegen.sh generate $(find . -name '*.graphql') --schema schema.json --output GraphQL/API.swift





Quando si esegue per la prima volta una build di progetto, questo script crea un file API.swift che conterrà il codice necessario per lavorare con le query e verrà aggiornato con ogni build. A questo punto, Xcode produrrà un errore in fase di assemblaggio, poiché ci manca qualcosa.

richieste

Il generatore di codice per la creazione dell'API richiede un file in cui verranno registrate tutte le query in GraphQL. Crea un nuovo file vuoto nella cartella Source / GraphQL (nella parte inferiore dell'elenco dei file) denominato github.graphql. Successivamente, scriviamo una query in questo file, che fornirà i primi 20 repository per la query di ricerca e inoltre determinerà immediatamente il frammento per il repository.

query SearchRepos($searchText: String!) {
    search(first: 20, query: $searchText, type: REPOSITORY) {
        nodes {
            ... on Repository {
                ...RepositoryDetail
            }
        }
    }
}

fragment RepositoryDetail on Repository {
    id
    nameWithOwner
    viewerHasStarred
    stargazers {
        totalCount
    }
}

Successivamente, porteremo il progetto insieme in modo che il codice di cui abbiamo bisogno sia generato. Aggiungi il file API.swift risultante al progetto.

IMPORTANTE: se hai aggiunto un nuovo codice a * .graphql, allora prima costruisci il progetto e solo dopo inizia a scrivere un nuovo codice!

cliente

Devi creare un client Apollo. Crealo in AppDelegate.swift sull'annuncio AppDelegate. Poiché la comunicazione con github richiede l'autorizzazione, è necessario aggiungere un'intestazione a tutte le nostre richieste. Riceveremo qui tale pezzo:

let apollo: ApolloClient = {
    let configuration = URLSessionConfiguration.default
   
    configuration.httpAdditionalHeaders = ["Authorization": "bearer <your token>"]
   
    let url = URL(string: "https://api.github.com/graphql")!
   
    return ApolloClient(networkTransport: HTTPNetworkTransport(url: url, configuration: configuration))
}()

Query di ricerca

Ora dobbiamo assicurarci che quando clicchi sul pulsante Cerca, inviamo una richiesta al server. Creare un file ReposListViewModel.swift e il relativo ReposListViewModel. Questa sarà una classe che invierà le nostre richieste al server, oltre a gestire lo stato delle query già esistenti.

Innanzitutto, importa Apollo, quindi crea una variabile di tipo Cancellable e chiamala currentSearchCancellable:

import Apollo

class ReposListViewModel {
    private var currentSearchCancellable: Cancellable?
}

Quindi, crea una funzione che richiederà il testo da cercare e nella callback trasferiremo l'array ricevuto. Infine, andiamo direttamente all'invio della richiesta! In primo luogo, è necessario creare una query. Il nostro generatore di codice ha generato una query che corrisponde al nome che abbiamo dato nel file graphql: SearchReposQuery e inizializziamo utilizzando il testo di ricerca. Successivamente, per ottenere la risposta, chiamiamo il recupero della funzione Apollo del client, a cui passiamo la nostra query, e inoltre selezioniamo la coda di esecuzione principale e il criterio di caching corrispondente. In callback, fetch restituisce il risultato e l'errore opzionali. Non penseremo ancora all'elaborazione dell'errore, ma lo stamperemo solo in caso di problemi. Recupero del risultato ricevuto da RepositoryDetail, che ha anche generato un generatore di codice e passarli alla funzione di ricerca callback. Si scopre che qui è una funzione del genere
:
func search(for text: String, completion: @escaping ([RepositoryDetail]) -> Void) {
        currentSearchCancellable?.cancel()
        let query = SearchReposQuery(searchText: text)
        currentSearchCancellable = apollo.fetch(query: query, cachePolicy: .returnCacheDataAndFetch, queue: .main, resultHandler: { (result, error) in
            if let result = result, let data = result.data {
                let repositoryDetails = (data.search.nodes ?? [SearchReposQuery.Data.Search.Node?]()).map{$0?.asRepository}.filter{$0 != nil}.map{($0?.fragments.repositoryDetail)!}
                completion(repositoryDetails)
            } else {
                print(error as Any)
                completion([RepositoryDetail]())
            }
        })
    }

Ora creeremo un viewModel, come il parametro ReposListViewController, e anche un array per la memorizzazione di RepositoryDetail:

let viewModel = ReposListViewModel()
var repositoryDetails = [RepositoryDetail]()

Quindi, modificare numberOfRowsInSection su:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return repositoryDetails.count
    }

È inoltre necessario aggiornare ReposListCell:

class ReposListCell: UITableViewCell {
   
    @IBOutlet weak var repoName: UILabel!
    @IBOutlet weak var starCount: UILabel!
    @IBOutlet weak var starButton: UIButton!
       
    var repositoryDetail: RepositoryDetail! {
        didSet {
            repoName.text = repositoryDetail.nameWithOwner
            starCount.text = "\(repositoryDetail.stargazers.totalCount)"
           
            if repositoryDetail.viewerHasStarred {
                starButton.setImage( imageLiteral(resourceName: "ic_full_star"), for: .normal)
            } else {
                starButton.setImage( imageLiteral(resourceName: "ic_empty_star"), for: .normal)
            }
        }
    }
}

E cellForRow prenderà questo punto di vista:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ReposListCell") as! ReposListCell
        cell.repositoryDetail = repositoryDetails[indexPath.row]
        return cell
    }

Resta da richiedere a viewModel i dati necessari per il clic:

 func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        viewModel.search(for: searchBar.text ?? "") { [unowned self] repositoryDetails in
            self.repositoryDetails = repositoryDetails
            self.tableView.reloadData()
        }
    }

Fatto! Compiliamo e cerchiamo i primi 20 repository sulla tua richiesta!

Metti le stelle

Come già sapevi, la stella nel layout della cella non è solo per questo. Aggiungiamo questa funzionalità alla nostra applicazione. Per fare ciò, crea una nuova richiesta di mutazione, ma prima prendi il tuo ID qui . Ora puoi inserire una query di ricerca con il corrispondente clientMutationID e assemblare il progetto:

mutation AddStar( $repositoryId: ID!) {
    addStar(input: {clientMutationId: “<your ID>”, starrableId: $repositoryId}) {
        starrable {
            ... on Repository {
               ...RepositoryDetail
            }
        }
    }
}

mutation RemoveStar($repositoryId: ID!) {
    removeStar(input: {clientMutationId: “<your ID>”, starrableId: $repositoryId}) {
        starrable {
            ... on Repository {
                ...RepositoryDetail
            }
        }
    }
}

Aggiungi l'esecuzione di queste richieste nel ViewModel. La logica è la stessa della query, ma invece di recuperare, ora chiamiamo perform:

func addStar(for repositoryID: String, completion: @escaping (RepositoryDetail?) -> Void ) {
        currentAddStarCancellable?.cancel()
        let mutation = AddStarMutation(repositoryId: repositoryID)
        currentAddStarCancellable = apollo.perform(mutation: mutation, queue: .main, resultHandler: { (result, error) in
            if let result = result, let data = result.data {
                let repositoryDetails = data.addStar?.starrable.asRepository?.fragments.repositoryDetail
                completion(repositoryDetails)
            } else {
                print(error as Any)
                completion(nil)
            }
        })
    }
   
    func removeStar(for repositoryID: String, completion: @escaping (RepositoryDetail?) -> Void ) {
        currentRemoveStarCancellable?.cancel()
        let mutation = RemoveStarMutation(repositoryId: repositoryID)
        currentAddStarCancellable = apollo.perform(mutation: mutation, queue: .main, resultHandler: { (result, error) in
            if let result = result, let data = result.data {
                let repositoryDetails = data.removeStar?.starrable.asRepository?.fragments.repositoryDetail
                completion(repositoryDetails)
            } else {
                print(error as Any)
                completion(nil)
            }
        })
    }

Ora dobbiamo far sapere al ViewController che abbiamo cliccato sul pulsante. Facciamolo diventare un delegato della cellula. Per fare ciò, crea un protocollo e aggiungilo al file ReposListCell.swift:

protocol ReposListCellDelegate: class {
    func starTapped(for cell: ReposListCell)
}

Aggiungiamo un nuovo parametro di classe di cella e l'elaborazione di un tasto:
weak var delegate: ReposListCellDelegate?
override func awakeFromNib() {
        super.awakeFromNib()
       
        starButton.addTarget(self, action: #selector(starTapped), for: .touchUpInside)
    }
   
    @objc func starTapped() {
        delegate?.starTapped(for: self)
    }

Ora assegniamo il controller al delegato in cellForRow:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ReposListCell") as! ReposListCell
        cell.repositoryDetail = repositoryDetails[indexPath.row]
        cell.delegate = self
        return cell
    }

Aggiungi una funzione che aggiornerà i dati nella tabella:
func updateTableView(for newDetail: RepositoryDetail?) {
        if let repositoryDetail = newDetail {
            for (index, detail) in repositoryDetails.enumerated() {
                if detail.id == repositoryDetail.id {
                    self.repositoryDetails[index] = repositoryDetail
                    for visibleCell in tableView.visibleCells {
                        if (visibleCell as! ReposListCell).repositoryDetail.id == repositoryDetail.id {
                            (visibleCell as! ReposListCell).repositoryDetail = repositoryDetail
                        }
                    }
                }
            }
        }
    }

E rimane da fare l'estensione controller per il controller creato in precedenza:

extension ReposListViewController: ReposListCellDelegate {
    func starTapped(for cell: ReposListCell) {
        if cell.repositoryDetail.viewerHasStarred {
            viewModel.removeStar(for: cell.repositoryDetail.id) { [unowned self] repositoryDetail in
                self.updateTableView(for: repositoryDetail)
            }
        } else {
            viewModel.addStar(for: cell.repositoryDetail.id) { [unowned self] repositoryDetail in
                self.updateTableView(for: repositoryDetail)
            }
        }
    }
}

Fatto! Possiamo eseguire l'applicazione, cercare i primi 20 repository e inserire / rimuovere le stelle!

Questo non è tutto. È interessante usare questa libreria in somma con RxSwift, risolvendo il problema dell'impaginazione, e Apollo supporta anche il caching, ma su ogni cosa la prossima volta!




Коментарі

Популярні дописи з цього блогу

Автомобілі на польській реєстрації.

Малий Прикордонний Рух

база номеров мобильных телефонов украины скачать