В днешно време повечето мобилни приложения разчитат до голяма степен на взаимодействието клиент-сървър. Това не само означава, че те могат да разтоварят по-голямата част от тежките си задачи на back-end сървъри, но също така позволява на тези мобилни приложения да предлагат всякакви функции и функционалност, които могат да бъдат достъпни само чрез интернет.
Back-end сървърите обикновено са проектирани да предлагат своите услуги чрез RESTful API . За по-прости приложения често се изкушаваме да създадем код за спагети; смесване на код, който извиква API с останалата част от логиката на приложението. Въпреки това, тъй като приложенията се усложняват и се справят с все повече и повече API, може да се окаже неприятно да се взаимодейства с тези API по неструктуриран, непланиран начин.
Тази статия разглежда архитектурен подход за изграждане на чист REST клиентски мрежов модул за iOS приложения което ви позволява да запазите цялата си логика за взаимодействие клиент-сървър изолирана от останалата част от кода на вашето приложение.
Типично взаимодействие клиент-сървър изглежда по следния начин:
Накратко, цялостният процес може да изглежда прост, но трябва да помислим за детайлите.
Дори да приемем, че API за бекенд сървър работи както е рекламирано (което е не винаги е така!), той често може да бъде лошо проектиран, което го прави неефективен или дори труден за използване. Една често срещана неприятност е, че всички извиквания към API изискват повикващият да предоставя излишно една и съща информация (напр. Как се форматират данните на заявката, маркер за достъп, който сървърът може да използва, за да идентифицира влезлия в момента потребител и т.н.).
Мобилните приложения може да се наложи да използват едновременно множество задни сървъри едновременно за различни цели. Например един сървър може да бъде посветен на удостоверяване на потребителя, докато друг се занимава само със събиране на анализи.
Освен това, типичният REST клиент ще трябва да направи нещо повече от просто извикване на отдалечени API. Възможността за отмяна на чакащи заявки или чист и управляем подход за обработка на грешки са примери за функционалност, която трябва да бъде вградена във всяко стабилно мобилно приложение.
Ядрото на нашия REST клиент ще бъде изградено върху следните компоненти:
Ето как всеки от тези компоненти ще взаимодейства помежду си:
Стрелките от 1 до 10 в изображението по-горе показват идеална последователност от операции между приложението, извикващо услуга, и услугата, която в крайна сметка връща исканите данни като обект на модел. Всеки компонент в този поток има специфична роля за осигуряване разделяне на опасенията в рамките на модула.
Ще внедрим нашия REST клиент като част от нашето въображаемо приложение за социална мрежа, в което ще заредим списък с влезлите в момента приятели на потребителя. Ще приемем, че нашият отдалечен сървър използва JSON за отговори.
Нека започнем с внедряването на нашите модели и парсери.
Първият ни модел, User
, определя структурата на информацията за всеки потребител на социалната мрежа. За да улесним нещата, ще включим само полета, които са абсолютно необходими за този урок (в реално приложение структурата обикновено ще има много повече свойства).
struct User { var id: String var email: String? var name: String? }
Тъй като ще получим всички потребителски данни от бекенд сървъра чрез неговия API, имаме нужда от начин анализирайте отговора на API в валиден User
обект. За целта ще добавим конструктор към User
който приема анализиран JSON обект (Dictionary
) като параметър. Ще определим нашия JSON обект като псевдоним тип:
typealias JSON = [String: Any]
След това ще добавим функцията конструктор към нашите User
структура, както следва:
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
, добавяме конструктора чрез разширение на User
Тип.
След това, за да създадете User
обект от суров API отговор, трябва да изпълним следните две стъпки:
// 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)
Ще определим тип, който да представя различни грешки, които могат да възникнат при опит за взаимодействие с бекенд сървърите. Можем да разделим всички такива грешки на три основни категории:
Можем да дефинираме обектите си за грешки като тип на изброяване. И докато сме в това, е добра идея да направим нашите ServiceError
тип съответства на Error
протокол . Това ще ни позволи да използваме и обработваме тези стойности на грешки, като използваме стандартни механизми, предоставени от Swift (като например използването на throw
за изхвърляне на грешка).
enum ServiceError: Error { case noInternetConnection case custom(String) case other }
За разлика от noInternetConnection
и other
грешки, потребителската грешка има свързана стойност. Това ще ни позволи да използваме отговора на грешката от сървъра като свързана стойност за самата грешка, като по този начин дава на грешката повече контекст.
Сега, нека добавим errorDescription
собственост към ServiceError
изброяване, за да направят грешките по-описателни. Ще добавим твърдо кодирани съобщения за noInternetConnection
и other
грешки и използвайте свързаната стойност като съобщение за custom
грешки.
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 } } }
Има само още нещо, което трябва да приложим в нашите ServiceError
изброяване. В случай на custom
грешка, трябва да трансформираме JSON данните на сървъра в обект за грешка. За целта използваме същия подход, който използвахме в случая на модели:
extension ServiceError { init(json: JSON) { if let message = json['message'] as? String { self = .custom(message) } else { self = .other } } }
Клиентският компонент ще бъде посредник между приложението и бекенд сървъра. Това е критичен компонент, който ще определи как приложението и сървърът ще комуникират, но въпреки това няма да знае нищо за моделите на данни и техните структури. Клиентът ще бъде отговорен за извикване на конкретни URL адреси с предоставени параметри и връщане на входящи JSON данни, анализирани като JSON обекти.
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 } }
Нека разгледаме какво се случва в горния код ...
Първо, декларирахме тип изброяване, RequestMethod
, който описва четири често срещани HTTP метода. Това са сред методите, използвани в REST API.
WebClient
класът съдържа baseURL
свойство, което ще се използва за разрешаване на всички относителни URL адреси, които получава. В случай, че нашето приложение трябва да взаимодейства с множество сървъри, можем да създадем множество копия на WebClient
всеки с различна стойност за baseURL
.
Клиентът има един метод load
, който взема път спрямо baseURL
като параметър, метод на заявка, параметри на заявката и затваряне на завършването. Затварянето на завършването се извиква с анализирания JSON и ServiceError
като параметри. Засега в метода по-горе липсва изпълнение, до което ще стигнем скоро.
Преди да приложите load
метод, имаме нужда от начин да създадем URL
от цялата налична информация за метода. Ще удължим URL
клас за тази цел:
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: Как да изолираме логиката за взаимодействие клиент-сървър в iOS приложения
В днешно време повечето мобилни приложения разчитат до голяма степен на взаимодействието клиент-сървър. Това не само означава, че те могат да разтоварят по-голямата част от тежките си задачи на back-end сървъри, но също така позволява на тези мобилни приложения да предлагат всякакви функции и функционалност, които могат да бъдат достъпни само чрез интернет.
Back-end сървърите обикновено са проектирани да предлагат своите услуги чрез RESTful API . За по-прости приложения често се изкушаваме да създадем код за спагети; смесване на код, който извиква API с останалата част от логиката на приложението. Въпреки това, тъй като приложенията се усложняват и се справят с все повече и повече API, може да се окаже неприятно да се взаимодейства с тези API по неструктуриран, непланиран начин.
Тази статия разглежда архитектурен подход за изграждане на чист REST клиентски мрежов модул за iOS приложения което ви позволява да запазите цялата си логика за взаимодействие клиент-сървър изолирана от останалата част от кода на вашето приложение.
Типично взаимодействие клиент-сървър изглежда по следния начин:
Накратко, цялостният процес може да изглежда прост, но трябва да помислим за детайлите.
Дори да приемем, че API за бекенд сървър работи както е рекламирано (което е не винаги е така!), той често може да бъде лошо проектиран, което го прави неефективен или дори труден за използване. Една често срещана неприятност е, че всички извиквания към API изискват повикващият да предоставя излишно една и съща информация (напр. Как се форматират данните на заявката, маркер за достъп, който сървърът може да използва, за да идентифицира влезлия в момента потребител и т.н.).
Мобилните приложения може да се наложи да използват едновременно множество задни сървъри едновременно за различни цели. Например един сървър може да бъде посветен на удостоверяване на потребителя, докато друг се занимава само със събиране на анализи.
Освен това, типичният REST клиент ще трябва да направи нещо повече от просто извикване на отдалечени API. Възможността за отмяна на чакащи заявки или чист и управляем подход за обработка на грешки са примери за функционалност, която трябва да бъде вградена във всяко стабилно мобилно приложение.
Ядрото на нашия REST клиент ще бъде изградено върху следните компоненти:
Ето как всеки от тези компоненти ще взаимодейства помежду си:
Стрелките от 1 до 10 в изображението по-горе показват идеална последователност от операции между приложението, извикващо услуга, и услугата, която в крайна сметка връща исканите данни като обект на модел. Всеки компонент в този поток има специфична роля за осигуряване разделяне на опасенията в рамките на модула.
Ще внедрим нашия REST клиент като част от нашето въображаемо приложение за социална мрежа, в което ще заредим списък с влезлите в момента приятели на потребителя. Ще приемем, че нашият отдалечен сървър използва JSON за отговори.
Нека започнем с внедряването на нашите модели и парсери.
Първият ни модел, User
, определя структурата на информацията за всеки потребител на социалната мрежа. За да улесним нещата, ще включим само полета, които са абсолютно необходими за този урок (в реално приложение структурата обикновено ще има много повече свойства).
struct User { var id: String var email: String? var name: String? }
Тъй като ще получим всички потребителски данни от бекенд сървъра чрез неговия API, имаме нужда от начин анализирайте отговора на API в валиден User
обект. За целта ще добавим конструктор към User
който приема анализиран JSON обект (Dictionary
) като параметър. Ще определим нашия JSON обект като псевдоним тип:
typealias JSON = [String: Any]
След това ще добавим функцията конструктор към нашите User
структура, както следва:
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
, добавяме конструктора чрез разширение на User
Тип.
След това, за да създадете User
обект от суров API отговор, трябва да изпълним следните две стъпки:
// 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)
Ще определим тип, който да представя различни грешки, които могат да възникнат при опит за взаимодействие с бекенд сървърите. Можем да разделим всички такива грешки на три основни категории:
Можем да дефинираме обектите си за грешки като тип на изброяване. И докато сме в това, е добра идея да направим нашите ServiceError
тип съответства на Error
протокол . Това ще ни позволи да използваме и обработваме тези стойности на грешки, като използваме стандартни механизми, предоставени от Swift (като например използването на throw
за изхвърляне на грешка).
enum ServiceError: Error { case noInternetConnection case custom(String) case other }
За разлика от noInternetConnection
и other
грешки, потребителската грешка има свързана стойност. Това ще ни позволи да използваме отговора на грешката от сървъра като свързана стойност за самата грешка, като по този начин дава на грешката повече контекст.
Сега, нека добавим errorDescription
собственост към ServiceError
изброяване, за да направят грешките по-описателни. Ще добавим твърдо кодирани съобщения за noInternetConnection
и other
грешки и използвайте свързаната стойност като съобщение за custom
грешки.
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 } } }
Има само още нещо, което трябва да приложим в нашите ServiceError
изброяване. В случай на custom
грешка, трябва да трансформираме JSON данните на сървъра в обект за грешка. За целта използваме същия подход, който използвахме в случая на модели:
extension ServiceError { init(json: JSON) { if let message = json['message'] as? String { self = .custom(message) } else { self = .other } } }
Клиентският компонент ще бъде посредник между приложението и бекенд сървъра. Това е критичен компонент, който ще определи как приложението и сървърът ще комуникират, но въпреки това няма да знае нищо за моделите на данни и техните структури. Клиентът ще бъде отговорен за извикване на конкретни URL адреси с предоставени параметри и връщане на входящи JSON данни, анализирани като JSON обекти.
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 } }
Нека разгледаме какво се случва в горния код ...
Първо, декларирахме тип изброяване, RequestMethod
, който описва четири често срещани HTTP метода. Това са сред методите, използвани в REST API.
WebClient
класът съдържа baseURL
свойство, което ще се използва за разрешаване на всички относителни URL адреси, които получава. В случай, че нашето приложение трябва да взаимодейства с множество сървъри, можем да създадем множество копия на WebClient
всеки с различна стойност за baseURL
.
Клиентът има един метод load
, който взема път спрямо baseURL
като параметър, метод на заявка, параметри на заявката и затваряне на завършването. Затварянето на завършването се извиква с анализирания JSON и ServiceError
като параметри. Засега в метода по-горе липсва изпълнение, до което ще стигнем скоро.
Преди да приложите load
метод, имаме нужда от начин да създадем URL
от цялата налична информация за метода. Ще удължим URL
клас за тази цел:
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! } }
Тук просто добавяме пътя към основния URL адрес. За GET и DELETE HTTP методи, ние също добавяме параметрите на заявката към низа на URL.
След това трябва да можем да създадем екземпляри на URLRequest
от зададени параметри. За целта ще направим нещо подобно на това, което направихме за 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 } } }
Тук първо създаваме URL
използвайки конструктора от разширението. След това инициализираме екземпляр от URLRequest
с това URL
, задайте няколко HTTP заглавки, ако е необходимо, и след това в случай на POST или PUT HTTP методи, добавете параметри към тялото на заявката.
След като покрихме всички предпоставки, можем да приложим load
метод:
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
Методът по-горе изпълнява следните стъпки:
noInternetConnection
грешка като параметър. (Забележка: Reachability
в кода е потребителски клас, който използва един от често срещаните подходи за да проверите интернет връзката.)URLRequest
обект, използвайки конструктора от разширението.URLSession
обект за изпращане на данни към сървъра.JSONSerialization
След това проверяваме кода на състоянието на отговора. Ако това е код за успех (т.е. в диапазона между 200 и 299), ние извикваме затваряне на завършването с обекта JSON. В противен случай ние трансформираме обекта JSON в ServiceError
обект и извика затварянето на завършването с този обект за грешка.В случая с нашето приложение се нуждаем от услуга, която да се занимава със задачи, свързани с приятели на потребител. За това създаваме FriendsService
клас. В идеалния случай клас като този ще отговаря за операции като получаване на списък с приятели, добавяне на нов приятел, премахване на приятел, групиране на някои приятели в категория и т.н. За улеснение в този урок ще приложим само един метод :
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
класът съдържа client
свойство от тип WebClient
. Той се инициализира с основния URL адрес на отдалечения сървър, който отговаря за управлението на приятели. Както бе споменато по-горе, в други сервизни класове можем да имаме различен екземпляр от WebClient
инициализиран с различен URL, ако е необходимо.
В случай на приложение, което работи само с един сървър, WebClient
на клас може да се даде конструктор, който се инициализира с URL адреса на този сървър:
final class WebClient { // ... init() { self.baseUrl = 'https://your_server_base_url' } // ... }
loadFriends
метод, когато бъде извикан, подготвя всички необходими параметри и използва FriendService
екземпляра на WebClient
за да направите заявка за API. След като получи отговора от сървъра чрез WebClient
, той трансформира JSON обекта в User
моделира и извиква затварянето на завършването с тях като параметър.
Типично използване на FriendService
може да изглежда по следния начин:
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 } } } }
В горния пример приемаме, че функцията friendsButtonTapped
се извиква всеки път, когато потребителят докосне бутон, предназначен да му покаже списък с приятелите си в мрежата. Също така запазваме препратка към задачата в friendsTask
свойство, за да можем да отменим заявката по всяко време, като се обадим friendsTask?.cancel()
.
Това ни позволява да имаме по-голям контрол върху жизнения цикъл на чакащите заявки, като ни дава възможност да ги прекратим, когато установим, че те са станали без значение.
В тази статия споделих проста архитектура на мрежов модул за вашето iOS приложение, която е едновременно тривиална за изпълнение и може да бъде адаптирана към сложните мрежови нужди на повечето iOS приложения. Ключовият извод от това обаче е, че правилно проектираният REST клиент и придружаващите го компоненти - които са изолирани от останалата част от логиката на вашето приложение - могат да помогнат за поддържането на простия код на взаимодействие клиент-сървър на вашето приложение, дори когато самото приложение става все по-сложно .
Надявам се тази статия да ви бъде полезна при изграждането на следващото ви приложение за iOS. Можете да намерите изходния код на този мрежов модул на GitHub . Вижте кода, разклонете го, променете го, играйте с него.
Ако смятате, че друга архитектура е по-предпочитана за вас и вашия проект, моля, споделете подробностите в раздела за коментари по-долу.
Свързани: Опростяване на използването на RESTful API и постоянството на данните в iOS с Mantle и RealmВ днешно време повечето мобилни приложения разчитат до голяма степен на взаимодействието клиент-сървър. Това не само означава, че те могат да разтоварят по-голямата част от тежките си задачи на back-end сървъри, но също така позволява на тези мобилни приложения да предлагат всякакви функции и функционалност, които могат да бъдат достъпни само чрез интернет.
Back-end сървърите обикновено са проектирани да предлагат своите услуги чрез RESTful API . За по-прости приложения често се изкушаваме да създадем код за спагети; смесване на код, който извиква API с останалата част от логиката на приложението. Въпреки това, тъй като приложенията се усложняват и се справят с все повече и повече API, може да се окаже неприятно да се взаимодейства с тези API по неструктуриран, непланиран начин.
Тази статия разглежда архитектурен подход за изграждане на чист REST клиентски мрежов модул за iOS приложения което ви позволява да запазите цялата си логика за взаимодействие клиент-сървър изолирана от останалата част от кода на вашето приложение.
Типично взаимодействие клиент-сървър изглежда по следния начин:
Накратко, цялостният процес може да изглежда прост, но трябва да помислим за детайлите.
Дори да приемем, че API за бекенд сървър работи както е рекламирано (което е не винаги е така!), той често може да бъде лошо проектиран, което го прави неефективен или дори труден за използване. Една често срещана неприятност е, че всички извиквания към API изискват повикващият да предоставя излишно една и съща информация (напр. Как се форматират данните на заявката, маркер за достъп, който сървърът може да използва, за да идентифицира влезлия в момента потребител и т.н.).
Мобилните приложения може да се наложи да използват едновременно множество задни сървъри едновременно за различни цели. Например един сървър може да бъде посветен на удостоверяване на потребителя, докато друг се занимава само със събиране на анализи.
Освен това, типичният REST клиент ще трябва да направи нещо повече от просто извикване на отдалечени API. Възможността за отмяна на чакащи заявки или чист и управляем подход за обработка на грешки са примери за функционалност, която трябва да бъде вградена във всяко стабилно мобилно приложение.
Ядрото на нашия REST клиент ще бъде изградено върху следните компоненти:
Ето как всеки от тези компоненти ще взаимодейства помежду си:
Стрелките от 1 до 10 в изображението по-горе показват идеална последователност от операции между приложението, извикващо услуга, и услугата, която в крайна сметка връща исканите данни като обект на модел. Всеки компонент в този поток има специфична роля за осигуряване разделяне на опасенията в рамките на модула.
Ще внедрим нашия REST клиент като част от нашето въображаемо приложение за социална мрежа, в което ще заредим списък с влезлите в момента приятели на потребителя. Ще приемем, че нашият отдалечен сървър използва JSON за отговори.
Нека започнем с внедряването на нашите модели и парсери.
Първият ни модел, User
, определя структурата на информацията за всеки потребител на социалната мрежа. За да улесним нещата, ще включим само полета, които са абсолютно необходими за този урок (в реално приложение структурата обикновено ще има много повече свойства).
struct User { var id: String var email: String? var name: String? }
Тъй като ще получим всички потребителски данни от бекенд сървъра чрез неговия API, имаме нужда от начин анализирайте отговора на API в валиден User
обект. За целта ще добавим конструктор към User
който приема анализиран JSON обект (Dictionary
) като параметър. Ще определим нашия JSON обект като псевдоним тип:
typealias JSON = [String: Any]
След това ще добавим функцията конструктор към нашите User
структура, както следва:
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
, добавяме конструктора чрез разширение на User
Тип.
След това, за да създадете User
обект от суров API отговор, трябва да изпълним следните две стъпки:
// 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)
Ще определим тип, който да представя различни грешки, които могат да възникнат при опит за взаимодействие с бекенд сървърите. Можем да разделим всички такива грешки на три основни категории:
Можем да дефинираме обектите си за грешки като тип на изброяване. И докато сме в това, е добра идея да направим нашите ServiceError
тип съответства на Error
протокол . Това ще ни позволи да използваме и обработваме тези стойности на грешки, като използваме стандартни механизми, предоставени от Swift (като например използването на throw
за изхвърляне на грешка).
enum ServiceError: Error { case noInternetConnection case custom(String) case other }
За разлика от noInternetConnection
и other
грешки, потребителската грешка има свързана стойност. Това ще ни позволи да използваме отговора на грешката от сървъра като свързана стойност за самата грешка, като по този начин дава на грешката повече контекст.
Сега, нека добавим errorDescription
собственост към ServiceError
изброяване, за да направят грешките по-описателни. Ще добавим твърдо кодирани съобщения за noInternetConnection
и other
грешки и използвайте свързаната стойност като съобщение за custom
грешки.
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 } } }
Има само още нещо, което трябва да приложим в нашите ServiceError
изброяване. В случай на custom
грешка, трябва да трансформираме JSON данните на сървъра в обект за грешка. За целта използваме същия подход, който използвахме в случая на модели:
extension ServiceError { init(json: JSON) { if let message = json['message'] as? String { self = .custom(message) } else { self = .other } } }
Клиентският компонент ще бъде посредник между приложението и бекенд сървъра. Това е критичен компонент, който ще определи как приложението и сървърът ще комуникират, но въпреки това няма да знае нищо за моделите на данни и техните структури. Клиентът ще бъде отговорен за извикване на конкретни URL адреси с предоставени параметри и връщане на входящи JSON данни, анализирани като JSON обекти.
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 } }
Нека разгледаме какво се случва в горния код ...
Първо, декларирахме тип изброяване, RequestMethod
, който описва четири често срещани HTTP метода. Това са сред методите, използвани в REST API.
WebClient
класът съдържа baseURL
свойство, което ще се използва за разрешаване на всички относителни URL адреси, които получава. В случай, че нашето приложение трябва да взаимодейства с множество сървъри, можем да създадем множество копия на WebClient
всеки с различна стойност за baseURL
.
Клиентът има един метод load
, който взема път спрямо baseURL
като параметър, метод на заявка, параметри на заявката и затваряне на завършването. Затварянето на завършването се извиква с анализирания JSON и ServiceError
като параметри. Засега в метода по-горе липсва изпълнение, до което ще стигнем скоро.
Преди да приложите load
метод, имаме нужда от начин да създадем URL
от цялата налична информация за метода. Ще удължим URL
клас за тази цел:
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! } }
Тук просто добавяме пътя към основния URL адрес. За GET и DELETE HTTP методи, ние също добавяме параметрите на заявката към низа на URL.
След това трябва да можем да създадем екземпляри на URLRequest
от зададени параметри. За целта ще направим нещо подобно на това, което направихме за 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 } } }
Тук първо създаваме URL
използвайки конструктора от разширението. След това инициализираме екземпляр от URLRequest
с това URL
, задайте няколко HTTP заглавки, ако е необходимо, и след това в случай на POST или PUT HTTP методи, добавете параметри към тялото на заявката.
След като покрихме всички предпоставки, можем да приложим load
метод:
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
Методът по-горе изпълнява следните стъпки:
noInternetConnection
грешка като параметър. (Забележка: Reachability
в кода е потребителски клас, който използва един от често срещаните подходи за да проверите интернет връзката.)URLRequest
обект, използвайки конструктора от разширението.URLSession
обект за изпращане на данни към сървъра.JSONSerialization
След това проверяваме кода на състоянието на отговора. Ако това е код за успех (т.е. в диапазона между 200 и 299), ние извикваме затваряне на завършването с обекта JSON. В противен случай ние трансформираме обекта JSON в ServiceError
обект и извика затварянето на завършването с този обект за грешка.В случая с нашето приложение се нуждаем от услуга, която да се занимава със задачи, свързани с приятели на потребител. За това създаваме FriendsService
клас. В идеалния случай клас като този ще отговаря за операции като получаване на списък с приятели, добавяне на нов приятел, премахване на приятел, групиране на някои приятели в категория и т.н. За улеснение в този урок ще приложим само един метод :
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
класът съдържа client
свойство от тип WebClient
. Той се инициализира с основния URL адрес на отдалечения сървър, който отговаря за управлението на приятели. Както бе споменато по-горе, в други сервизни класове можем да имаме различен екземпляр от WebClient
инициализиран с различен URL, ако е необходимо.
В случай на приложение, което работи само с един сървър, WebClient
на клас може да се даде конструктор, който се инициализира с URL адреса на този сървър:
final class WebClient { // ... init() { self.baseUrl = 'https://your_server_base_url' } // ... }
loadFriends
метод, когато бъде извикан, подготвя всички необходими параметри и използва FriendService
екземпляра на WebClient
за да направите заявка за API. След като получи отговора от сървъра чрез WebClient
, той трансформира JSON обекта в User
моделира и извиква затварянето на завършването с тях като параметър.
Типично използване на FriendService
може да изглежда по следния начин:
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 } } } }
В горния пример приемаме, че функцията friendsButtonTapped
се извиква всеки път, когато потребителят докосне бутон, предназначен да му покаже списък с приятелите си в мрежата. Също така запазваме препратка към задачата в friendsTask
свойство, за да можем да отменим заявката по всяко време, като се обадим friendsTask?.cancel()
.
Това ни позволява да имаме по-голям контрол върху жизнения цикъл на чакащите заявки, като ни дава възможност да ги прекратим, когато установим, че те са станали без значение.
В тази статия споделих проста архитектура на мрежов модул за вашето iOS приложение, която е едновременно тривиална за изпълнение и може да бъде адаптирана към сложните мрежови нужди на повечето iOS приложения. Ключовият извод от това обаче е, че правилно проектираният REST клиент и придружаващите го компоненти - които са изолирани от останалата част от логиката на вашето приложение - могат да помогнат за поддържането на простия код на взаимодействие клиент-сървър на вашето приложение, дори когато самото приложение става все по-сложно .
Надявам се тази статия да ви бъде полезна при изграждането на следващото ви приложение за iOS. Можете да намерите изходния код на този мрежов модул на GitHub . Вижте кода, разклонете го, променете го, играйте с него.
Ако смятате, че друга архитектура е по-предпочитана за вас и вашия проект, моля, споделете подробностите в раздела за коментари по-долу.
Свързани: Опростяване на използването на RESTful API и постоянството на данните в iOS с Mantle и Realm .value)) } default: break } self = components.url! } }Тук просто добавяме пътя към основния URL адрес. За GET и DELETE HTTP методи, ние също добавяме параметрите на заявката към низа на URL.
След това трябва да можем да създадем екземпляри на URLRequest
от зададени параметри. За целта ще направим нещо подобно на това, което направихме за 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 } } }
Тук първо създаваме URL
използвайки конструктора от разширението. След това инициализираме екземпляр от URLRequest
с това URL
, задайте няколко HTTP заглавки, ако е необходимо, и след това в случай на POST или PUT HTTP методи, добавете параметри към тялото на заявката.
След като покрихме всички предпоставки, можем да приложим load
метод:
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
Методът по-горе изпълнява следните стъпки:
noInternetConnection
грешка като параметър. (Забележка: Reachability
в кода е потребителски клас, който използва един от често срещаните подходи за да проверите интернет връзката.)URLRequest
обект, използвайки конструктора от разширението.URLSession
обект за изпращане на данни към сървъра.JSONSerialization
След това проверяваме кода на състоянието на отговора. Ако това е код за успех (т.е. в диапазона между 200 и 299), ние извикваме затваряне на завършването с обекта JSON. В противен случай ние трансформираме обекта JSON в ServiceError
обект и извика затварянето на завършването с този обект за грешка.В случая с нашето приложение се нуждаем от услуга, която да се занимава със задачи, свързани с приятели на потребител. За това създаваме FriendsService
клас. В идеалния случай клас като този ще отговаря за операции като получаване на списък с приятели, добавяне на нов приятел, премахване на приятел, групиране на някои приятели в категория и т.н. За улеснение в този урок ще приложим само един метод :
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
класът съдържа client
свойство от тип WebClient
. Той се инициализира с основния URL адрес на отдалечения сървър, който отговаря за управлението на приятели. Както бе споменато по-горе, в други сервизни класове можем да имаме различен екземпляр от WebClient
инициализиран с различен URL, ако е необходимо.
В случай на приложение, което работи само с един сървър, WebClient
на клас може да се даде конструктор, който се инициализира с URL адреса на този сървър:
final class WebClient { // ... init() { self.baseUrl = 'https://your_server_base_url' } // ... }
loadFriends
метод, когато бъде извикан, подготвя всички необходими параметри и използва FriendService
екземпляра на WebClient
за да направите заявка за API. След като получи отговора от сървъра чрез WebClient
, той трансформира JSON обекта в User
моделира и извиква затварянето на завършването с тях като параметър.
Типично използване на FriendService
може да изглежда по следния начин:
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 } } } }
В горния пример приемаме, че функцията friendsButtonTapped
се извиква всеки път, когато потребителят докосне бутон, предназначен да му покаже списък с приятелите си в мрежата. Също така запазваме препратка към задачата в friendsTask
свойство, за да можем да отменим заявката по всяко време, като се обадим friendsTask?.cancel()
.
Това ни позволява да имаме по-голям контрол върху жизнения цикъл на чакащите заявки, като ни дава възможност да ги прекратим, когато установим, че те са станали без значение.
В тази статия споделих проста архитектура на мрежов модул за вашето iOS приложение, която е едновременно тривиална за изпълнение и може да бъде адаптирана към сложните мрежови нужди на повечето iOS приложения. Ключовият извод от това обаче е, че правилно проектираният REST клиент и придружаващите го компоненти - които са изолирани от останалата част от логиката на вашето приложение - могат да помогнат за поддържането на простия код на взаимодействие клиент-сървър на вашето приложение, дори когато самото приложение става все по-сложно .
Надявам се тази статия да ви бъде полезна при изграждането на следващото ви приложение за iOS. Можете да намерите изходния код на този мрежов модул на GitHub . Вижте кода, разклонете го, променете го, играйте с него.
Ако смятате, че друга архитектура е по-предпочитана за вас и вашия проект, моля, споделете подробностите в раздела за коментари по-долу.
Свързани: Опростяване на използването на RESTful API и постоянството на данните в iOS с Mantle и Realm