Sådan foretages et API-opkald i Swift

Hvis du ønsker at blive en iOS-udvikler, er der nogle grundlæggende færdigheder, der er værd at vide. For det første er det vigtigt at være fortrolig med at oprette tabelvisninger. For det andet skal du vide, hvordan du udfylder disse tabelvisninger med data. For det tredje er det godt, hvis du kan hente data fra en API og bruge disse data i din tabelvisning.

Det tredje punkt er, hvad vi vil dække i denne artikel. Siden introduktionen af Codablei Swift 4 er det meget nemmere at foretage API-opkald. Tidligere brugte de fleste mennesker bælg som Alamofire og SwiftyJson (du kan læse om, hvordan du gør det her). Nu er den hurtige måde meget pænere ud af kassen, så der er ingen grund til at downloade en pod.

Lad os gennemgå nogle byggesten, der ofte bruges til at foretage et API-opkald. Vi dækker først disse begreber, da de er vigtige dele for at forstå, hvordan man foretager et API-opkald.

  • Afslutningshåndterere
  • URLSession
  • DispatchQueue
  • Bevar cyklusser

Endelig sætter vi det hele sammen. Jeg bruger open source Star Wars API til at opbygge dette projekt. Du kan se min fulde projektkode på GitHub.

Ansvarsfraskrivelsesalarm: Jeg er ny inden for kodning og er stort set selvlært. Undskyld, hvis jeg forkert gengiver nogle begreber.

Afslutningshåndterere

Kan du huske episoden af ​​Friends, hvor Pheobe er limet til telefonen i flere dage og venter på at tale med kundeservice? Forestil dig, at en dejlig person ved navn Pip lige i starten af ​​dette telefonopkald sagde: "Tak for at ringe. Jeg aner ikke, hvor længe du skal vente på vent, men jeg ringer tilbage, når vi er klar for dig." Det ville ikke have været så sjovt, men Pip tilbyder at være færdigbehandler for Pheobe.

Du bruger en kompletteringshåndterer i en funktion, når du ved, at den funktion vil tage et stykke tid at fuldføre. Du ved ikke hvor længe, ​​og du vil ikke sætte dit liv på pause og vente på, at det er færdigt. Så du beder Pip om at banke dig på skulderen, når hun er klar til at give dig svaret. På den måde kan du gå om dit liv, løbe nogle ærinder, læse en bog og se tv. Når Pip banker på dig på skulderen med svaret, kan du tage hendes svar og bruge det.

Dette er hvad der sker med API-opkald. Du sender en URL-anmodning til en server og beder den om nogle data. Du håber, at serveren returnerer dataene hurtigt, men du ved ikke, hvor lang tid det tager. I stedet for at få din bruger til at vente tålmodigt på, at serveren giver dig dataene, bruger du en kompletteringshåndterer. Dette betyder, at du kan fortælle din app at gå ud og gøre andre ting, såsom at indlæse resten af ​​siden.

Du beder færdiggørelseshandleren om at trykke på din app på skulderen, når den har de ønskede oplysninger. Du kan angive, hvad disse oplysninger er. På den måde, når din app bliver tappet på skulderen, kan den tage informationen fra færdiggørelseshandleren og gøre noget med den. Normalt skal du genindlæse tabelvisningen, så dataene vises for brugeren.

Her er et eksempel på, hvordan en færdiggøringshandler ser ud. Det første eksempel er at opsætte selve API-opkaldet:

func fetchFilms(completionHandler: @escaping ([Film]) -> Void) { // Setup the variable lotsOfFilms var lotsOfFilms: [Film] // Call the API with some code // Using data from the API, assign a value to lotsOfFilms // Give the completion handler the variable, lotsOfFilms completionHandler(lotsOfFilms) }

Nu vil vi kalde funktionen fetchFilms. Nogle ting at bemærke:

  • Du behøver ikke henvise, completionHandlernår du ringer til funktionen. Den eneste gang, du refererer til, completionHandlerer inde i funktionserklæringen.
  • Færdiggørelsesbehandleren giver os nogle data tilbage, der skal bruges. Baseret på den funktion, vi har skrevet ovenfor, ved vi at forvente data af typen [Film]. Vi er nødt til at navngive dataene, så vi kan henvise til dem. Nedenfor bruger jeg navnet films, men det kan være randomDataeller ethvert andet variabelnavn, jeg gerne vil have.

Koden vil se sådan ud:

fetchFilms() { (films) in // Do something with the data the completion handler returns print(films) }

URLSession

URLSessioner som lederen af ​​et hold. Lederen gør ikke noget alene. Hendes job er at dele arbejdet med folkene i hendes team, og de får jobbet gjort. Hendes hold er dataTasks. Hver gang du har brug for nogle data, skal du skrive til chefen og bruge den URLSession.shared.dataTask.  

Du kan give de dataTaskforskellige typer oplysninger, der hjælper dig med at nå dit mål. At give information til dataTaskkaldes initialisering. Jeg initialiserer mit dataTaskswebadresser. dataTasksBrug også færdigbehandlingshandlere som en del af initialiseringen. Her er et eksempel:

let url = URL(string: "//www.swapi.co/api/films") let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in // your code here }) task.resume()

dataTasksbruge færdiggørelse handlere, og de altid returnere de samme typer af information: data, responseog error. Du kan give disse datatyper forskellige navne, ligesom (data, res, err)eller (someData, someResponse, someError). Af hensyn til konventionen er det bedst at holde sig til noget indlysende snarere end at gå slyngel med nye variabelnavne.

Lad os starte med error. Hvis dataTaskreturnerer en error, vil du gerne vide det på forhånd. Det betyder, at du kan dirigere din kode til at håndtere fejlen yndefuldt. Det betyder også, at du ikke gider at prøve at læse dataene og gøre noget med det, da der er en fejl i returneringen af ​​dataene.

Nedenfor håndterer jeg fejlen virkelig ved blot at udskrive en fejl til konsollen og afslutte funktionen. Der er mange andre måder, du kan håndtere fejlen på, hvis du vil. Tænk på, hvor grundlæggende disse data er for din app. For eksempel, hvis du har en bankapp, og dette API-opkald viser brugerne deres saldo, vil du muligvis håndtere fejlen ved at præsentere et modal for brugeren, der siger "Beklager, vi oplever et problem lige nu. Prøv igen senere. "

if let error = error { print("Error accessing swapi.co: /(error)") return }

Dernæst ser vi på svaret. Du kan kaste svaret til at være et httpResponse. På den måde kan du se på statuskoderne og træffe nogle beslutninger baseret på koden. For eksempel, hvis statuskoden er 404, ved du, at siden ikke blev fundet.

Koden nedenfor bruger a guardtil at kontrollere, at der findes to ting. Hvis begge findes, tillader det koden at fortsætte til næste sætning efter guardklausulen. Hvis en af ​​udsagnene mislykkes, afslutter vi funktionen. Dette er en typisk brugstilfælde af en guardklausul. Du forventer, at koden efter en vagtklausul er flowet med glade dage (dvs. let flow uden fejl).

 guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { print("Error with the response, unexpected status code: \(response)") return }

Endelig håndterer du selve dataene. Bemærk, at vi ikke har brugt færdiggørelsesbehandleren til erroreller response. Det er fordi færdiggørelseshandleren venter på data fra API'en. Hvis det ikke kommer til datadelen af ​​koden, er der ingen grund til at påkalde handleren.

Til dataene bruger vi til JSONDecoderat analysere dataene på en god måde. Dette er ret fedt, men kræver, at du har oprettet en model. Vores model hedder FilmSummary. Hvis det JSONDecoderer nyt for dig, så kig online, hvordan du bruger det, og hvordan du bruger det Codable. Det er virkelig simpelt i Swift 4 og derover sammenlignet med Swift 3 dage.

I nedenstående kode kontrollerer vi først, om dataene findes. Vi er ret sikre på, at det skulle eksistere, fordi der ikke er nogen fejl og ingen mærkelige HTTP-svar. For det andet kontrollerer vi, at vi kan analysere de data, vi modtager på den måde, vi forventer. Hvis vi kan, returnerer vi filmoversigten til færdiggørelsesbehandleren. Bare hvis der ikke er nogen data, der skal returneres fra API'et, har vi en tilbageførselsplan for det tomme array.

if let data = data, let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) { completionHandler(filmSummary.results ?? []) }

Så den fulde kode til API-opkald ser sådan ud:

func fetchFilms(completionHandler: @escaping ([Film]) -> Void) { let url = URL(string: domainUrlString + "films/")! let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in if let error = error { print("Error with fetching films: \(error)") return } guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { print("Error with the response, unexpected status code: \(response)") return } if let data = data, let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) { completionHandler(filmSummary.results ?? []) } }) task.resume() }

Bevar cyklusser

NB: Jeg er ekstremt ny til at forstå retain-cyklusser! Her er kernen i det, jeg undersøgte online.

Det er vigtigt at bevare cyklusser for hukommelsesstyring. Dybest set vil du have, at din app skal rydde op i hukommelse, som den ikke længere har brug for. Jeg antager, at dette gør appen mere performant.

There are lots of ways that Swift helps you do this automatically. However there are many ways that you can accidentally code retain cycles into your app. A retain cycle means that your app will always hold on to the memory for a certain piece of code. Generally it happens when you have two things that have strong pointers to each other.

To get around this, people often use weak. When one side of the code is weak, you don't have a retain cycle and your app will be able to release the memory.

For our purpose, a common pattern is to use [weak self] when calling the API. This ensures that once the completion handler returns some code, the app can release the memory.

fetchFilms { [weak self] (films) in // code in here }

DispatchQueue

Xcode uses different threads to execute code in parallel. The advantage of multiple threads means you aren't stuck waiting on one thing to finish before you can move on to the next. Hopefully you can start to see the links to completion handlers here.

These threads seem to be also called dispatch queues. API calls are handled on one queue, typically a queue in the background. Once you have the data from your API call, most likely you'll want to show that data to the user. That means you'll want to refresh your table view.

Table views are part of the UI, and all UI manipulations should be done in the main dispatch queue. This means somewhere in your view controller file, usually as part of the viewDidLoad function, you should have a bit of code that tells your table view to refresh.

We only want the table view to refresh once it has some new data from the API. This means we'll use a completion handler to tap us on the shoulder and tell us when that API call is finished. We'll wait until that tap before we refresh the table.

The code will look something like:

fetchFilms { [weak self] (films) in self.films = films // Reload the table view using the main dispatch queue DispatchQueue.main.async { tableView.reloadData() } }

viewDidLoad vs viewDidAppear

Finally you need to decide where to call your fetchfilms function. It will be inside a view controller that will use the data from the API. There are two obvious places you could make this API call. One is inside viewDidLoad and the other is inside viewDidAppear.

These are two different states for your app. My understanding is viewDidLoad is called the first time you load up that view in the foreground. viewDidAppear is called every time you come back to that view, for example when you press the back button to come back to the view.

If you expect your data to change in between the times that the user will navigate to and from that view, then you may want to put your API call in viewDidAppear. However I think for almost all apps, viewDidLoad is sufficient. Apple recommends viewDidAppear for all API calls, but that seems like overkill. I imagine it would make your app less performant as it's making many more API calls that it needs to.

Combining all the steps

First: write the function that calls the API. Above, this is fetchFilms. This will have a completion handler, which will return the data you are interested in. In my example, the completion handler returns an array of films.

Second: call this function in your view controller. You do this here because you want to update the view based on the data from the API. In my example, I am refreshing a table view once the API returns the data.

Third: decide where in your view controller you would like to call the function. In my example, I call it in viewDidLoad.

Fourth: decide what to do with the data from the API. In my example, I am refreshing a table view.

Inside NetworkManager.swift (this function can be defined in your view controller if you'd like, but I am using the MVVM pattern).

func fetchFilms(completionHandler: @escaping ([Film]) -> Void) { let url = URL(string: domainUrlString + "films/")! let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in if let error = error { print("Error with fetching films: \(error)") return } guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { print("Error with the response, unexpected status code: \(response)") return } if let data = data, let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) { completionHandler(filmSummary.results ?? []) } }) task.resume() }

Inside FilmsViewController.swift:

final class FilmsViewController: UIViewController { private var films: [Film]? override func viewDidLoad() { super.viewDidLoad() NetworkManager().fetchFilms { [weak self] (films) in self?.films = films DispatchQueue.main.async { self?.tableView.reloadData() } } } // other code for the view controller }

Gosh, vi klarede det! Tak fordi du holder fast ved mig.