.key, value: String(describing:

Kako izolirati logiku interakcije klijent-poslužitelj u iOS aplikacijama

Danas se većina mobilnih aplikacija u velikoj mjeri oslanja na interakciju klijent-poslužitelj. To ne samo da znači da većinu svojih teških zadataka mogu pretočiti na pozadinske poslužitelje, već omogućuje i da ove mobilne aplikacije nude sve vrste značajki i funkcionalnosti koje mogu biti dostupne samo putem Interneta.

Pozadinski poslužitelji obično su dizajnirani da nude svoje usluge putem RESTful API-ji . Za jednostavnije primjene često osjećamo iskušenje da stvorimo špageti kod; miješanje koda koji poziva API s ostatkom logike aplikacije. Međutim, kako aplikacije postaju složene i bave se sve više i više API-ja, može postati smetnja u interakciji s tim API-ima na nestrukturiran, neplaniran način.

Neka vaš iOS kôd aplikacije bude neuredan s dobro dizajniranim REST klijentskim mrežnim modulom.



Neka vaš iOS kôd aplikacije bude neuredan s dobro dizajniranim REST klijentskim mrežnim modulom. Cvrkut

Ovaj članak govori o arhitektonskom pristupu za izgradnju čistog REST klijentskog mrežnog modula za iOS aplikacije koji vam omogućuje da svu logiku interakcije klijent-poslužitelj držite izoliranom od ostatka vašeg aplikacijskog koda.

Klijent-poslužiteljske aplikacije

Tipična interakcija klijent-poslužitelj izgleda otprilike ovako:

  1. Korisnik izvrši neku radnju (npr. Tapkanjem na neki gumb ili izvođenjem neke druge geste na zaslonu).
  2. Aplikacija priprema i šalje HTTP / REST zahtjev kao odgovor na radnju korisnika.
  3. Poslužitelj obrađuje zahtjev i u skladu s tim odgovara na aplikaciju.
  4. Aplikacija prima odgovor i na temelju njega ažurira korisničko sučelje.

Na brzinu, cjelokupni postupak može izgledati jednostavno, ali moramo razmisliti o detaljima.

Čak i pod pretpostavkom da API pozadinskog poslužitelja radi onako kako je oglašen (što je ne uvijek slučaj!), često može biti loše dizajniran što ga čini neučinkovitim ili čak teškim za upotrebu. Jedna česta smetnja je što svi pozivi API-ja zahtijevaju od pozivatelja da redundantno pruži iste podatke (npr. Kako se formatiraju podaci zahtjeva, pristupni token koji poslužitelj može koristiti za identifikaciju trenutno prijavljenog korisnika i tako dalje).

Mobilne aplikacije također će trebati istodobno koristiti više pozadinskih poslužitelja za različite svrhe. Na primjer, jedan poslužitelj može biti posvećen autentifikaciji korisnika, dok se drugi bavi samo prikupljanjem analitike.

Nadalje, tipični REST klijent trebat će učiniti više od pukog pozivanja udaljenih API-ja. Mogućnost otkazivanja zahtjeva na čekanju ili čist i upravljiv pristup rješavanju pogrešaka primjeri su funkcionalnosti koje treba ugraditi u bilo koju robusnu mobilnu aplikaciju.

Pregled arhitekture

Jezgra našeg REST klijenta bit će izgrađena na sljedećim komponentama:

Evo kako će svaka od ovih komponenata međusobno komunicirati:

Strelice od 1 do 10 na gornjoj slici pokazuju idealan slijed operacija između aplikacije koja poziva uslugu i usluge koja na kraju vraća tražene podatke kao objekt modela. Svaka komponenta u tom protoku ima određenu ulogu koja osigurava razdvajanje zabrinutosti unutar modula.

Provedba

Svoj ćemo REST klijent implementirati kao dio naše zamišljene aplikacije za društvenu mrežu u koju ćemo učitati popis trenutno prijavljenih korisnikovih prijatelja. Pretpostavit ćemo da naš udaljeni poslužitelj koristi JSON za odgovore.

Počnimo s primjenom naših modela i parsera.

Od sirovog JSON-a do objektnih modela

Naš prvi model, User, definira strukturu podataka za bilo kojeg korisnika društvene mreže. Da bi stvari bile jednostavne, uključit ćemo samo polja koja su prijeko potrebna za ovaj vodič (u stvarnoj aplikaciji struktura bi obično imala puno više svojstava).

struct User { var id: String var email: String? var name: String? }

Budući da ćemo sve korisničke podatke primati sa pozadinskog poslužitelja putem njegovog API-ja, trebamo put do raščlanite API odgovor u valjani User objekt. Da bismo to učinili, dodat ćemo konstruktor u User koji prihvaća raščlanjeni JSON objekt (Dictionary) kao parametar. Svoj JSON objekt definirat ćemo kao alias tip:

typealias JSON = [String: Any]

Zatim ćemo dodati funkciju konstruktora u naš User strukturirati kako slijedi:

extension User { init?(json: JSON) { guard let id = json['id'] as? String else { return nil } self.id = id self.email = json['email'] as? String self.name = json['name'] as? String } }

Da bismo sačuvali izvorni zadani konstruktor User, dodajemo konstruktor kroz proširenje na User tip.

Dalje, za stvaranje User objekt iz sirovog API odgovora, moramo izvršiti sljedeća dva koraka:

// Transform raw JSON data to parsed JSON object using JSONSerializer (part of standard library) let userObject = (try? JSONSerialization.jsonObject(with: data, options: [])) as? JSON // Create an instance of `User` structure from parsed JSON object let user = userObject.flatMap(User.init)

Pojednostavljeno rukovanje pogreškama

Definirat ćemo tip koji predstavlja različite pogreške koje se mogu pojaviti pri pokušaju interakcije s pozadinskim poslužiteljima. Sve takve pogreške možemo podijeliti u tri osnovne kategorije:

Naše objekte pogrešaka možemo definirati kao tip nabrajanja. I dok smo u tome, dobra je ideja napraviti naš ServiceError tip u skladu s Error protokol . To će nam omogućiti upotrebu i obradu ovih vrijednosti pogrešaka pomoću standardnih mehanizama koje pruža Swift (kao što je upotreba throw za uklanjanje pogreške).

enum ServiceError: Error { case noInternetConnection case custom(String) case other }

Za razliku od noInternetConnection i other pogreške, prilagođena pogreška ima vrijednost povezanu s njom. To će nam omogućiti da odgovor na pogrešku poslužitelja koristimo kao pridruženu vrijednost za samu pogrešku, dajući time pogrešku više konteksta.

Dodajmo sada errorDescription svojstvo na ServiceError nabrajanje kako bi pogreške bile opisivije. Dodati ćemo kodirane poruke za noInternetConnection i other pogreške i pridruženu vrijednost upotrijebite kao poruku za custom pogreške.

extension ServiceError: LocalizedError { var errorDescription: String? { switch self { case .noInternetConnection: return 'No Internet connection' case .other: return 'Something went wrong' case .custom(let message): return message } } }

Postoji još samo jedna stvar koju moramo implementirati u naš ServiceError nabrajanje. U slučaju a custom pogreška, moramo transformirati JSON podatke poslužitelja u objekt pogreške. Da bismo to učinili, koristimo isti pristup koji smo koristili u slučaju modela:

extension ServiceError { init(json: JSON) { if let message = json['message'] as? String { self = .custom(message) } else { self = .other } } }

Premošćavanje praznine između aplikacije i pozadinskog poslužitelja

Klijentska komponenta bit će posrednik između aplikacije i pozadinskog poslužitelja. To je kritična komponenta koja će definirati kako će aplikacija i poslužitelj komunicirati, ali pritom neće znati ništa o modelima podataka i njihovim strukturama. Klijent će biti odgovoran za pozivanje određenih URL-ova s ​​navedenim parametrima i vraćanje dolaznih JSON podataka raščlanjenih kao JSON objekti.

enum RequestMethod: String { case get = 'GET' case post = 'POST' case put = 'PUT' case delete = 'DELETE' } final class WebClient { private var baseUrl: String init(baseUrl: String) { self.baseUrl = baseUrl } func load(path: String, method: RequestMethod, params: JSON, completion: @escaping (Any?, ServiceError?) -> ()) -> URLSessionDataTask? { // TODO: Add implementation } }

Ispitajmo što se događa u gornjem kodu ...

Prvo smo proglasili tip nabrajanja, RequestMethod, koji opisuje četiri uobičajene HTTP metode. To su među metodama koje se koriste u REST API-ima.

The WebClient razred sadrži baseURL svojstvo koje će se koristiti za rješavanje svih relativnih URL-ova koje primi. U slučaju da naša aplikacija treba interakciju s više poslužitelja, možemo stvoriti više primjeraka WebClient svaki s različitom vrijednošću za baseURL.

Klijent ima jednu metodu load, koja uzima put u odnosu na baseURL kao parametar, metoda zahtjeva, parametri zahtjeva i zatvaranje završetka. Zatvaranje završetka poziva se raščlanjenim JSON-om i ServiceError kao parametri. Za sada gornjoj metodi nedostaje implementacija, do koje ćemo doći uskoro.

Prije primjene load metodu, potreban nam je način za stvaranje URL iz svih podataka dostupnih metodi. Proširit ćemo URL razred u tu svrhu:

extension URL { init(baseUrl: String, path: String, params: JSON, method: RequestMethod) { var components = URLComponents(string: baseUrl)! components.path += path switch method { case .get, .delete: components.queryItems = params.map { URLQueryItem(name: $0.key, value: String(describing: $0.value)) } default: break } self = components.url! } }

Ovdje jednostavno dodamo put osnovnom URL-u. Za GET i DELETE HTTP metode također dodajemo parametre upita u niz URL-a.

Dalje, trebamo biti u mogućnosti stvoriti instance URLRequest iz zadanih parametara. Da bismo to učinili, napravit ćemo nešto slično onome što smo radili za URL:

extension URLRequest { init(baseUrl: String, path: String, method: RequestMethod, params: JSON) { let url = URL(baseUrl: baseUrl, path: path, params: params, method: method) self.init(url: url) httpMethod = method.rawValue setValue('application/json', forHTTPHeaderField: 'Accept') setValue('application/json', forHTTPHeaderField: 'Content-Type') switch method { case .post, .put: httpBody = try! JSONSerialization.data(withJSONObject: params, options: []) default: break } } }

Ovdje prvo stvaramo URL pomoću konstruktora iz nastavka. Tada inicijaliziramo instancu URLRequest s ovim URL, postavite nekoliko HTTP zaglavlja po potrebi, a zatim u slučaju POST ili PUT HTTP metoda dodajte parametre u tijelo zahtjeva.

Sad kad smo pokrili sve preduvjete, možemo implementirati load metoda:

final class WebClient { private var baseUrl: String init(baseUrl: String) { self.baseUrl = baseUrl } func load(path: String, method: RequestMethod, params: JSON, completion: @escaping (Any?, ServiceError?) -> ()) -> URLSessionDataTask? { // Checking internet connection availability if !Reachability.isConnectedToNetwork() { completion(nil, ServiceError.noInternetConnection) return nil } // Adding common parameters var parameters = params if let token = KeychainWrapper.itemForKey('application_token') { parameters['token'] = token } // Creating the URLRequest object let request = URLRequest(baseUrl: baseUrl, path: path, method: method, params: params) // Sending request to the server. let task = URLSession.shared.dataTask(with: request) { data, response, error in // Parsing incoming data var object: Any? = nil if let data = data { object = try? JSONSerialization.jsonObject(with: data, options: []) } if let httpResponse = response as? HTTPURLResponse, (200..<300) ~= httpResponse.statusCode { completion(object, nil) } else { let error = (object as? JSON).flatMap(ServiceError.init) ?? ServiceError.other completion(nil, error) } } task.resume() return task } }

The load gore navedena metoda izvodi sljedeće korake:

  1. Provjerite dostupnost internetske veze. Ako internetska povezanost nije dostupna, zatvaranje zatvaranja pozivamo odmah s noInternetConnection pogreška kao parametar. (Napomena: Reachability u kodu je prilagođena klasa koja koristi jedan od uobičajenih pristupa za provjeru internetske veze.)
  2. Dodajte uobičajene parametre. . To može uključivati ​​uobičajene parametre kao što su token aplikacije ili korisnički ID.
  3. Stvorite URLRequest objekt, pomoću konstruktora iz nastavka.
  4. Pošaljite zahtjev poslužitelju. Koristimo URLSession objekt za slanje podataka na poslužitelj.
  5. Analizirajte dolazne podatke. Kada poslužitelj odgovori, prvo analiziramo korisni teret odgovora u JSON objekt pomoću JSONSerialization. Zatim provjeravamo statusni kod odgovora. Ako je to kod uspješnosti (tj. U rasponu između 200 i 299), zatvaranje završetka nazivamo JSON objektom. Inače, JSON objekt pretvaramo u ServiceError objekt i pozovite zatvaranje završetka s tim objektom pogreške.

Utvrđivanje usluga za logički povezane operacije

U slučaju naše prijave potrebna nam je usluga koja će se baviti zadacima povezanim s prijateljima korisnika. Za to kreiramo FriendsService razred. Idealno bi bilo da je ovakav razred zadužen za operacije poput dobivanja popisa prijatelja, dodavanja novog prijatelja, uklanjanja prijatelja, grupiranja nekih prijatelja u kategoriju itd. Radi jednostavnosti u ovom uputstvu, implementirat ćemo samo jednu metodu :

final class FriendsService { private let client = WebClient(baseUrl: 'https://your_server_host/api/v1') @discardableResult func loadFriends(forUser user: User, completion: @escaping ([User]?, ServiceError?) -> ()) -> URLSessionDataTask? { let params: JSON = ['user_id': user.id] return client.load(path: '/friends', method: .get, params: params) { result, error in let dictionaries = result as? [JSON] completion(dictionaries?.flatMap(User.init), error) } } }

The FriendsService razred sadrži client svojstvo tipa WebClient. Inicijalizira se osnovnim URL-om udaljenog poslužitelja koji je zadužen za upravljanje prijateljima. Kao što je prethodno spomenuto, u ostalim uslužnim razredima možemo imati drugačiji primjerak WebClient ako je potrebno inicijalizirano drugim URL-om.

U slučaju aplikacije koja radi samo s jednim poslužiteljem, WebClient klasi može se dati konstruktor koji se inicijalizira URL-om tog poslužitelja:

final class WebClient { // ... init() { self.baseUrl = 'https://your_server_base_url' } // ... }

The loadFriends metoda, kada se pozove, priprema sve potrebne parametre i koristi FriendService primjerak WebClient za podnošenje zahtjeva za API. Nakon što primi odgovor od poslužitelja putem WebClient, transformira JSON objekt u User modelira i poziva parametar zatvaranje završetka s njima.

Tipična upotreba FriendService može izgledati otprilike ovako:

let friendsTask: URLSessionDataTask! let activityIndicator: UIActivityIndicatorView! var friends: [User] = [] func friendsButtonTapped() { friendsTask?.cancel() //Cancel previous loading task. activityIndicator.startAnimating() //Show loading indicator friendsTask = FriendsService().loadFriends(forUser: currentUser) {[weak self] friends, error in DispatchQueue.main.async { self?.activityIndicator.stopAnimating() //Stop loading indicators if let error = error { print(error.localizedDescription) //Handle service error } else if let friends = friends { self?.friends = friends //Update friends property self?.updateUI() //Update user interface } } } }

U gornjem primjeru pretpostavljamo da je funkcija friendsButtonTapped poziva se svaki put kada korisnik kucne na gumb namijenjen pokazivanju popisa njegovih prijatelja u mreži. Također upućujemo na zadatak u friendsTask svojstvo tako da zahtjev možemo u bilo kojem trenutku otkazati pozivom friendsTask?.cancel().

To nam omogućuje veću kontrolu nad životnim ciklusom zahtjeva na čekanju, omogućujući nam da ih prekinemo kad utvrdimo da su postali nevažni.

Zaključak

U ovom sam članku podijelio jednostavnu arhitekturu mrežnog modula za vašu iOS aplikaciju koja je trivijalna za primjenu i može se prilagoditi zamršenim mrežnim potrebama većine iOS aplikacija. Međutim, ključno je od toga što pravilno dizajnirani REST klijent i komponente koje ga prate - koje su izolirane od ostatka logike vašeg programa - mogu pomoći da kôd interakcije klijent-poslužitelj vaše aplikacije bude jednostavan, iako sama aplikacija postaje sve složenija .

Nadam se da će vam ovaj članak biti od pomoći u izradi vašeg sljedećeg iOS programa. Izvorni kod ovog mrežnog modula možete pronaći na GitHubu . Pogledajte kod, rastavite ga, promijenite, igrajte se s njim.

Ako smatrate da je neka druga arhitektura poželjnija za vas i vaš projekt, podijelite detalje u odjeljku za komentare u nastavku.

Povezano: Pojednostavljivanje upotrebe RESTful API-ja i postojanosti podataka na iOS-u uz Mantle i Realm .value)) } default: break } self = components.url! } }

Ovdje jednostavno dodamo put osnovnom URL-u. Za GET i DELETE HTTP metode također dodajemo parametre upita u niz URL-a.

Dalje, trebamo biti u mogućnosti stvoriti instance URLRequest iz zadanih parametara. Da bismo to učinili, napravit ćemo nešto slično onome što smo radili za URL:

extension URLRequest { init(baseUrl: String, path: String, method: RequestMethod, params: JSON) { let url = URL(baseUrl: baseUrl, path: path, params: params, method: method) self.init(url: url) httpMethod = method.rawValue setValue('application/json', forHTTPHeaderField: 'Accept') setValue('application/json', forHTTPHeaderField: 'Content-Type') switch method { case .post, .put: httpBody = try! JSONSerialization.data(withJSONObject: params, options: []) default: break } } }

Ovdje prvo stvaramo URL pomoću konstruktora iz nastavka. Tada inicijaliziramo instancu URLRequest s ovim URL, postavite nekoliko HTTP zaglavlja po potrebi, a zatim u slučaju POST ili PUT HTTP metoda dodajte parametre u tijelo zahtjeva.

Sad kad smo pokrili sve preduvjete, možemo implementirati load metoda:

final class WebClient { private var baseUrl: String init(baseUrl: String) { self.baseUrl = baseUrl } func load(path: String, method: RequestMethod, params: JSON, completion: @escaping (Any?, ServiceError?) -> ()) -> URLSessionDataTask? { // Checking internet connection availability if !Reachability.isConnectedToNetwork() { completion(nil, ServiceError.noInternetConnection) return nil } // Adding common parameters var parameters = params if let token = KeychainWrapper.itemForKey('application_token') { parameters['token'] = token } // Creating the URLRequest object let request = URLRequest(baseUrl: baseUrl, path: path, method: method, params: params) // Sending request to the server. let task = URLSession.shared.dataTask(with: request) { data, response, error in // Parsing incoming data var object: Any? = nil if let data = data { object = try? JSONSerialization.jsonObject(with: data, options: []) } if let httpResponse = response as? HTTPURLResponse, (200..<300) ~= httpResponse.statusCode { completion(object, nil) } else { let error = (object as? JSON).flatMap(ServiceError.init) ?? ServiceError.other completion(nil, error) } } task.resume() return task } }

The load gore navedena metoda izvodi sljedeće korake:

  1. Provjerite dostupnost internetske veze. Ako internetska povezanost nije dostupna, zatvaranje zatvaranja pozivamo odmah s noInternetConnection pogreška kao parametar. (Napomena: Reachability u kodu je prilagođena klasa koja koristi jedan od uobičajenih pristupa za provjeru internetske veze.)
  2. Dodajte uobičajene parametre. . To može uključivati ​​uobičajene parametre kao što su token aplikacije ili korisnički ID.
  3. Stvorite URLRequest objekt, pomoću konstruktora iz nastavka.
  4. Pošaljite zahtjev poslužitelju. Koristimo URLSession objekt za slanje podataka na poslužitelj.
  5. Analizirajte dolazne podatke. Kada poslužitelj odgovori, prvo analiziramo korisni teret odgovora u JSON objekt pomoću JSONSerialization. Zatim provjeravamo statusni kod odgovora. Ako je to kod uspješnosti (tj. U rasponu između 200 i 299), zatvaranje završetka nazivamo JSON objektom. Inače, JSON objekt pretvaramo u ServiceError objekt i pozovite zatvaranje završetka s tim objektom pogreške.

Utvrđivanje usluga za logički povezane operacije

U slučaju naše prijave potrebna nam je usluga koja će se baviti zadacima povezanim s prijateljima korisnika. Za to kreiramo FriendsService razred. Idealno bi bilo da je ovakav razred zadužen za operacije poput dobivanja popisa prijatelja, dodavanja novog prijatelja, uklanjanja prijatelja, grupiranja nekih prijatelja u kategoriju itd. Radi jednostavnosti u ovom uputstvu, implementirat ćemo samo jednu metodu :

final class FriendsService { private let client = WebClient(baseUrl: 'https://your_server_host/api/v1') @discardableResult func loadFriends(forUser user: User, completion: @escaping ([User]?, ServiceError?) -> ()) -> URLSessionDataTask? { let params: JSON = ['user_id': user.id] return client.load(path: '/friends', method: .get, params: params) { result, error in let dictionaries = result as? [JSON] completion(dictionaries?.flatMap(User.init), error) } } }

The FriendsService razred sadrži client svojstvo tipa WebClient. Inicijalizira se osnovnim URL-om udaljenog poslužitelja koji je zadužen za upravljanje prijateljima. Kao što je prethodno spomenuto, u ostalim uslužnim razredima možemo imati drugačiji primjerak WebClient ako je potrebno inicijalizirano drugim URL-om.

U slučaju aplikacije koja radi samo s jednim poslužiteljem, WebClient klasi može se dati konstruktor koji se inicijalizira URL-om tog poslužitelja:

final class WebClient { // ... init() { self.baseUrl = 'https://your_server_base_url' } // ... }

The loadFriends metoda, kada se pozove, priprema sve potrebne parametre i koristi FriendService primjerak WebClient za podnošenje zahtjeva za API. Nakon što primi odgovor od poslužitelja putem WebClient, transformira JSON objekt u User modelira i poziva parametar zatvaranje završetka s njima.

Tipična upotreba FriendService može izgledati otprilike ovako:

let friendsTask: URLSessionDataTask! let activityIndicator: UIActivityIndicatorView! var friends: [User] = [] func friendsButtonTapped() { friendsTask?.cancel() //Cancel previous loading task. activityIndicator.startAnimating() //Show loading indicator friendsTask = FriendsService().loadFriends(forUser: currentUser) {[weak self] friends, error in DispatchQueue.main.async { self?.activityIndicator.stopAnimating() //Stop loading indicators if let error = error { print(error.localizedDescription) //Handle service error } else if let friends = friends { self?.friends = friends //Update friends property self?.updateUI() //Update user interface } } } }

U gornjem primjeru pretpostavljamo da je funkcija friendsButtonTapped poziva se svaki put kada korisnik kucne na gumb namijenjen pokazivanju popisa njegovih prijatelja u mreži. Također upućujemo na zadatak u friendsTask svojstvo tako da zahtjev možemo u bilo kojem trenutku otkazati pozivom friendsTask?.cancel().

To nam omogućuje veću kontrolu nad životnim ciklusom zahtjeva na čekanju, omogućujući nam da ih prekinemo kad utvrdimo da su postali nevažni.

Zaključak

U ovom sam članku podijelio jednostavnu arhitekturu mrežnog modula za vašu iOS aplikaciju koja je trivijalna za primjenu i može se prilagoditi zamršenim mrežnim potrebama većine iOS aplikacija. Međutim, ključno je od toga što pravilno dizajnirani REST klijent i komponente koje ga prate - koje su izolirane od ostatka logike vašeg programa - mogu pomoći da kôd interakcije klijent-poslužitelj vaše aplikacije bude jednostavan, iako sama aplikacija postaje sve složenija .

Nadam se da će vam ovaj članak biti od pomoći u izradi vašeg sljedećeg iOS programa. Izvorni kod ovog mrežnog modula možete pronaći na GitHubu . Pogledajte kod, rastavite ga, promijenite, igrajte se s njim.

Ako smatrate da je neka druga arhitektura poželjnija za vas i vaš projekt, podijelite detalje u odjeljku za komentare u nastavku.

Povezano: Pojednostavljivanje upotrebe RESTful API-ja i postojanosti podataka na iOS-u uz Mantle i Realm