.key, value: String(describing:

Kuidas isoleerida kliendi-serveri interaktsiooniloogikat iOS-i rakendustes

Tänapäeval toetub enamik mobiilirakendusi suuresti kliendi-serveri interaktsioonidele. See ei tähenda mitte ainult seda, et nad saavad suurema osa oma rasketest ülesannetest laadida back-end-serveritesse, vaid võimaldab neil mobiilirakendustel pakkuda igasuguseid funktsioone ja funktsioone, mida saab teha kättesaadavaks ainult Interneti kaudu.

Taustserverid on tavaliselt loodud oma teenuseid pakkuma RESTful API-d . Lihtsamate rakenduste jaoks tunneme sageli kiusatust spagetikoodi loomisega; segamiskood, mis kutsub API-d ülejäänud rakenduse loogikaga. Kuid kui rakendused muutuvad keerukaks ja tegelevad üha enamate API-dega, võib nende API-dega struktureerimata ja planeerimata suhtlemine osutuda häirivaks.

Hoidke oma iOS-i rakenduse kood segadust hästi kujundatud REST-i kliendivõrgu mooduliga.



Hoidke oma iOS-i rakenduse kood segadust hästi kujundatud REST-i kliendivõrgu mooduliga. Piiksuma

Selles artiklis käsitletakse arhitektuurset lähenemisviisi puhta REST-kliendi võrgumooduli loomiseks iOS-i rakendused mis võimaldab teil kogu oma kliendi-serveri suhtlusloogika ülejäänud rakenduskoodist eraldatuna hoida.

Kliendi-serveri rakendused

Tüüpiline kliendi-serveri suhtlus näeb välja umbes selline:

  1. Kasutaja sooritab mõne toimingu (nt puudutab mõnda nuppu või teostab ekraanil mõnda muud žesti).
  2. Rakendus valmistab ja saadab vastuseks kasutaja toimingule HTTP / REST päringu.
  3. Server töötleb päringut ja vastab rakendusele vastavalt.
  4. Rakendus saab vastuse ja värskendab selle põhjal kasutajaliidest.

Kiire pilguga võib kogu protsess tunduda lihtne, kuid peame siiski detailidele mõtlema.

Isegi eeldades, et taustserveri API töötab reklaamituna (mis on mitte alati nii!), võib see sageli olla valesti kujundatud, muutes selle kasutamise ebaefektiivseks või isegi keeruliseks. Üks levinud tüütus on see, et kõik API-le tehtud kõned nõuavad, et helistaja esitaks üleliigselt sama teavet (nt kuidas päringuandmed vormindatakse, juurdepääsuluba, mida server saab kasutada praegu sisselogitud kasutaja tuvastamiseks jne).

Mobiilirakendused võivad vajada ka mitmete taustserverite samaaegset kasutamist erinevatel eesmärkidel. Üks server võib näiteks olla pühendatud kasutaja autentimisele, teine ​​aga ainult analüütika kogumisele.

Lisaks peab tüüpiline REST-i klient tegema midagi enamat kui lihtsalt kaug-API-de kutsumine. Võimalus ootel taotlusi tühistada või puhas ja hallatav lähenemine vigade käsitlemisele on näited funktsionaalsusest, mis tuleb sisse seada igasse jõulisse mobiilirakendusse.

Ülevaade arhitektuurist

Meie REST-kliendi tuum ehitatakse üles järgmistele komponentidele:

Nii toimivad need komponendid omavahel:

Nooled 1 kuni 10 ülaltoodud pildil näitavad ideaalset toimingute järjestust teenust kutsuva rakenduse ja teenuse, mis lõpuks tagastab nõutud andmed mudeliobjektina, vahel. Selle voolu igal komponendil on kindel roll murede lahusus mooduli piires.

Rakendamine

Rakendame oma REST-i kliendi osana oma kujuteldavast suhtlusvõrgustiku rakendusest, kuhu laadime praegu sisse logitud kasutajate sõprade loendi. Eeldame, et meie kaugserver kasutab vastuste jaoks JSON-i.

Alustame oma mudelite ja parserite juurutamisest.

Alates toorest JSONist kuni mudelobjektideni

Meie esimene mudel User määratleb sotsiaalse võrgustiku kasutaja jaoks teabe struktuuri. Asjade lihtsuse huvides lisame ainult need väljad, mis on selle õpetuse jaoks hädavajalikud (reaalses rakenduses oleks struktuuril tavaliselt palju rohkem omadusi).

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

Kuna me saame kõik kasutajaandmed taustaprogrammist selle API kaudu, vajame selleks võimalust sõeluda API vastus kehtivaks User objekt. Selleks lisame User -i konstruktori mis aktsepteerib parameetrina sõelutud JSON-objekti (Dictionary). Määratleme oma JSON-objekti pseudonüümina:

typealias JSON = [String: Any]

Seejärel lisame konstruktori funktsiooni meie User struktureerida järgmiselt:

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 } }

User Algse vaikekonstruktori säilitamiseks lisame konstruktori User laienduse kaudu tüüp.

Järgmisena loo User objekti toorest API-vastusest, peame tegema järgmised kaks sammu:

// 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)

Sujuvam veakäsitlus

Määratleme tüübi, mis esindab erinevaid vigu, mis võivad ilmneda taustserveritega suhtlemisel. Kõik sellised vead saame jagada kolme põhikategooriasse:

Saame määratleda oma veaobjektid loenduse tüübina. Ja kui me oleme selle juures, on hea mõte teha oma ServiceError tüüp vastab Error protokoll . See võimaldab meil neid veaväärtusi kasutada ja käsitseda Swifti pakutavate standardsete mehhanismide abil (näiteks vea viskamiseks throw

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

Erinevalt noInternetConnection ja other vigu, on kohandatud tõrjega seotud väärtus. See võimaldab meil kasutada serveri tõrketeavet vea enda seotud väärtusena, andes seeläbi veale rohkem konteksti.

Lisame nüüd errorDescription vara ServiceError loetelu, et muuta vead kirjeldavamaks. Lisame noInternetConnection jaoks kõvakoodiga sõnumid ja other vigu ja kasutage seotud väärtust sõnumina custom vigu.

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 } } }

On veel üks asi, mida peame oma ServiceError -s rakendama loendamine. Juhul, kui custom viga, peame muutma serveri JSON-i andmed veaobjektiks. Selleks kasutame sama lähenemist, mida kasutasime mudelite puhul:

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

Rakenduse ja taustserveri vahelise lõhe ületamine

Kliendikomponent on vahendaja rakenduse ja taustserveri vahel. See on kriitiline komponent, mis määrab, kuidas rakendus ja server suhtlevad, kuid ei tea andmemudelitest ja nende struktuuridest midagi. Klient vastutab pakutavate parameetritega konkreetsete URL-ide kutsumise ja JSON-objektidena parsitud sissetulevate JSON-andmete tagastamise eest.

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 } }

Uurime ülaltoodud koodis toimuvat ...

Esiteks kuulutasime välja loenduse tüübi RequestMethod, mis kirjeldab nelja levinumat HTTP-meetodit. Need on REST API-des kasutatavate meetodite hulgas.

WebClient klass sisaldab baseURL atribuut, mida kasutatakse kõigi saadud suhteliste URL-ide lahendamiseks. Juhul kui meie rakendus peab suhtlema mitme serveriga, võime luua WebClient mitu eksemplari igaühel on erinev väärtus baseURL

Kliendil on üks meetod load, mis kulgeb tee suhtes baseURL parameetrina päringu meetod, päringu parameetrid ja lõpetamise sulgemine. Parsitud JSON ja ServiceError käivitatakse lõpetamise lõpetamine parameetritena. Praegu puudub ülaltoodud meetodil juurutamine, milleni peagi jõuame.

Enne load rakendamist meetodi jaoks vajame viisi URL loomiseks kogu meetodile kättesaadava teabe põhjal. Laiendame URL klass selleks:

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! } }

Siinkohal lisame lihtsalt tee URL-ile. GET ja DELETE HTTP meetodite jaoks lisame URL-i stringi ka päringuparameetrid.

Järgmisena peame suutma luua URLRequest eksemplare antud parameetritest. Selleks teeme midagi sarnast sellele, mida tegime 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 } } }

Siin loome kõigepealt URL kasutades laiendist pärit konstruktorit. Seejärel lähtestame URLRequest eksemplari selle URL abil määrake vajadusel mõned HTTP-päised ja seejärel lisage POST- või PUT-HTTP-meetodite korral päringu kehasse parameetrid.

Nüüd, kui oleme kõik eeldused täitnud, saame rakendada load meetod:

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 } }

load ülaltoodud meetod täidab järgmisi samme:

  1. Kontrollige Interneti-ühenduse olemasolu. Kui Interneti-ühendus pole saadaval, helistame lõpuleviimise lõpetamiseks kohe nupuga noInternetConnection viga parameetrina. (Märkus: Reachability koodis on kohandatud klass, mis kasutab üks levinumaid lähenemisviise Interneti-ühenduse kontrollimiseks.)
  2. Lisage ühised parameetrid. . See võib hõlmata levinud parameetreid, näiteks rakenduse märki või kasutaja ID.
  3. Looge URLRequest objekt, kasutades laiendist pärit konstruktorit.
  4. Saada päring serverile. Kasutame URLSession serverile andmete saatmiseks.
  5. Sõeluge sissetulevad andmed. Kui server vastab, parsime esmalt vastuse kasuliku koormuse JSON-i objektiks, kasutades JSONSerialization Seejärel kontrollime vastuse olekukoodi. Kui see on edukood (s.o vahemikus 200 kuni 299), nimetame lõpetamise sulgemiseks objekti JSON. Vastasel korral teisendame JSON-i objekti ServiceError -ks objekt ja helistage selle tõrkeobjektiga lõpetamise sulgemine.

Loogiliselt lingitud toimingute teenuste määratlemine

Meie rakenduse jaoks vajame teenust, mis tegeleb kasutaja sõpradega seotud ülesannetega. Selleks loome FriendsService klass. Ideaalis vastutab selline klass selliste toimingute eest nagu sõprade loendi hankimine, uue sõbra lisamine, sõbra eemaldamine, sõprade grupeerimine kategooriasse jne. Selles õpetuses on lihtsuse huvides rakendatud ainult üks meetod :

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) } } }

FriendsService klass sisaldab client tüüpi tüüp WebClient. See lähtestatakse kaugserveri põhi-URL-iga, mis vastutab sõprade haldamise eest. Nagu varem mainitud, võib teistes teenuseklassides olla erinev WebClient eksemplar vajadusel lähtestatakse teise URL-iga.

Ainult ühe serveriga töötava rakenduse korral kuvatakse WebClient klassile saab anda konstruktori, kes lähtestab selle serveri URL-i:

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

loadFriends meetod, kui see käivitatakse, valmistab ette kõik vajalikud parameetrid ja kasutab FriendService 'i eksemplari WebClient API-päringu tegemiseks. Pärast serverilt vastuse saamist WebClient kaudu muudab see JSON-i objekti User mudeleid ja kutsub parameetrina nende lõpetamist.

FriendService Tüüpiline kasutus võib välja näha umbes järgmine:

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 } } } }

Eespool toodud näites eeldame, et funktsioon friendsButtonTapped käivitatakse alati, kui kasutaja puudutab nuppu, mille eesmärk on näidata nende võrgus olevate sõprade loendit. Viidet ülesandele hoiame ka friendsTask -s vara, et saaksime taotluse igal ajal tühistada, helistades friendsTask?.cancel()

See võimaldab meil saadaolevate taotluste elutsükli üle suuremat kontrolli, võimaldades meil need lõpetada, kui leiame, et need on muutunud ebaolulisteks.

Järeldus

Selles artiklis olen jaganud teie iOS-i rakenduse jaoks lihtsat võrgumooduli arhitektuuri, mida on nii tühine rakendada kui ka kohandada enamiku iOS-i rakenduste keerukate võrguvajadustega. Selle peamine võte on aga see, et korralikult kujundatud REST-klient ja sellega kaasnevad - ülejäänud rakenduse loogikast eraldatud - komponendid võivad aidata teie rakenduse kliendi-serveri suhtluskoodi lihtsana hoida, isegi kui rakendus ise muutub üha keerukamaks .

Loodetavasti leiate sellest artiklist abi järgmise iOS-i rakenduse loomisel. Selle võrgumooduli lähtekoodi leiate saidil GitHub . Kontrollige koodi, hargitage see, muutke seda, mängige sellega.

Kui leiate, et mõni muu arhitektuur on teie ja teie projekti jaoks eelistatavam, jagage palun üksikasju allpool olevas kommentaaride jaotises.

Seotud: RESTful API kasutamise ja andmete püsivuse lihtsustamine iOS-is koos Mantle ja Realmiga .value)) } default: break } self = components.url! } }

Siinkohal lisame lihtsalt tee URL-ile. GET ja DELETE HTTP meetodite jaoks lisame URL-i stringi ka päringuparameetrid.

Järgmisena peame suutma luua URLRequest eksemplare antud parameetritest. Selleks teeme midagi sarnast sellele, mida tegime 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 } } }

Siin loome kõigepealt URL kasutades laiendist pärit konstruktorit. Seejärel lähtestame URLRequest eksemplari selle URL abil määrake vajadusel mõned HTTP-päised ja seejärel lisage POST- või PUT-HTTP-meetodite korral päringu kehasse parameetrid.

Nüüd, kui oleme kõik eeldused täitnud, saame rakendada load meetod:

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 } }

load ülaltoodud meetod täidab järgmisi samme:

  1. Kontrollige Interneti-ühenduse olemasolu. Kui Interneti-ühendus pole saadaval, helistame lõpuleviimise lõpetamiseks kohe nupuga noInternetConnection viga parameetrina. (Märkus: Reachability koodis on kohandatud klass, mis kasutab üks levinumaid lähenemisviise Interneti-ühenduse kontrollimiseks.)
  2. Lisage ühised parameetrid. . See võib hõlmata levinud parameetreid, näiteks rakenduse märki või kasutaja ID.
  3. Looge URLRequest objekt, kasutades laiendist pärit konstruktorit.
  4. Saada päring serverile. Kasutame URLSession serverile andmete saatmiseks.
  5. Sõeluge sissetulevad andmed. Kui server vastab, parsime esmalt vastuse kasuliku koormuse JSON-i objektiks, kasutades JSONSerialization Seejärel kontrollime vastuse olekukoodi. Kui see on edukood (s.o vahemikus 200 kuni 299), nimetame lõpetamise sulgemiseks objekti JSON. Vastasel korral teisendame JSON-i objekti ServiceError -ks objekt ja helistage selle tõrkeobjektiga lõpetamise sulgemine.

Loogiliselt lingitud toimingute teenuste määratlemine

Meie rakenduse jaoks vajame teenust, mis tegeleb kasutaja sõpradega seotud ülesannetega. Selleks loome FriendsService klass. Ideaalis vastutab selline klass selliste toimingute eest nagu sõprade loendi hankimine, uue sõbra lisamine, sõbra eemaldamine, sõprade grupeerimine kategooriasse jne. Selles õpetuses on lihtsuse huvides rakendatud ainult üks meetod :

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) } } }

FriendsService klass sisaldab client tüüpi tüüp WebClient. See lähtestatakse kaugserveri põhi-URL-iga, mis vastutab sõprade haldamise eest. Nagu varem mainitud, võib teistes teenuseklassides olla erinev WebClient eksemplar vajadusel lähtestatakse teise URL-iga.

Ainult ühe serveriga töötava rakenduse korral kuvatakse WebClient klassile saab anda konstruktori, kes lähtestab selle serveri URL-i:

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

loadFriends meetod, kui see käivitatakse, valmistab ette kõik vajalikud parameetrid ja kasutab FriendService 'i eksemplari WebClient API-päringu tegemiseks. Pärast serverilt vastuse saamist WebClient kaudu muudab see JSON-i objekti User mudeleid ja kutsub parameetrina nende lõpetamist.

FriendService Tüüpiline kasutus võib välja näha umbes järgmine:

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 } } } }

Eespool toodud näites eeldame, et funktsioon friendsButtonTapped käivitatakse alati, kui kasutaja puudutab nuppu, mille eesmärk on näidata nende võrgus olevate sõprade loendit. Viidet ülesandele hoiame ka friendsTask -s vara, et saaksime taotluse igal ajal tühistada, helistades friendsTask?.cancel()

See võimaldab meil saadaolevate taotluste elutsükli üle suuremat kontrolli, võimaldades meil need lõpetada, kui leiame, et need on muutunud ebaolulisteks.

Järeldus

Selles artiklis olen jaganud teie iOS-i rakenduse jaoks lihtsat võrgumooduli arhitektuuri, mida on nii tühine rakendada kui ka kohandada enamiku iOS-i rakenduste keerukate võrguvajadustega. Selle peamine võte on aga see, et korralikult kujundatud REST-klient ja sellega kaasnevad - ülejäänud rakenduse loogikast eraldatud - komponendid võivad aidata teie rakenduse kliendi-serveri suhtluskoodi lihtsana hoida, isegi kui rakendus ise muutub üha keerukamaks .

Loodetavasti leiate sellest artiklist abi järgmise iOS-i rakenduse loomisel. Selle võrgumooduli lähtekoodi leiate saidil GitHub . Kontrollige koodi, hargitage see, muutke seda, mängige sellega.

Kui leiate, et mõni muu arhitektuur on teie ja teie projekti jaoks eelistatavam, jagage palun üksikasju allpool olevas kommentaaride jaotises.

Seotud: RESTful API kasutamise ja andmete püsivuse lihtsustamine iOS-is koos Mantle ja Realmiga