Содержание:
Здесь перечислены основные моменты и вспомогательная информация о том, каким образом работать с этой бибилиотекой.
Проект содержит Playground
в котором написаны несколько вариантов запросов - можно посмотреть туда в качестве интерактивного примера
Библиотека подразумевает работу с двумя слоями моделей:
- Application Layer Models - модели прикладного уровня, которые используются по всему приложению
- Raw Layer Models (DTO) - модели низкого уровня, на которые (или из которых) мапятся данные для (или от) сервера.
Но допускается возможность использование только одного модельного слоя.
Так же допускается возможность не использовать модели вообще.
За определение модели из этого слоя отвечают два протокола:
Существует также алиас RawMappable
Для сущностей, удовлетворяющих протоколам Codable
есть реализация маппинга по-умолчанию.
Например:
enum Type: Int, Codable {
case owner
case member
}
struct PhotoEntry: Codable {
let id: String
let ref: String
}
extension PhotoEntry: RawDecodable {
public typealias Raw = Json
}
struct UserEntry: Codable {
let name: String
let age: Int
let type: Type
let photos: [PhotoEntry]
}
extension UserEntry: RawDecodable {
public typealias Raw = Json
}
Этого кода будет достаточно для того, чтобы замапить ответ сервера на сущности UserEntry
и PhotoEntry
Хорошим тоном считается добавление постфикса Entry к DTO-сущности.
За определение модели из этого слоя отвечают два протокола:
Существует также алиас DTOConvertible
Продолжим пример:
struct Photo {
let id: String
let image: String
}
extension Photo: DTODecodable {
public typealias DTO = PhotoEntry
static func from(dto: PhotoEntry) throws -> Photo {
return .init(id: dto.id, image: dto.ref)
}
}
struct User {
let name: String
let age: Int
let type: Type
let photos: [Photo]
}
extension User: DTODecodable {
public typealias DTO = UserEntry
static func from(dto: UserEntry) throws -> Photo {
return try .init(name: dto.name,
age: dto.age,
type: dto.type,
photos: .from(dto: dto.photos))
}
}
Таким образом мы получаем связку из двух моделей, где:
UserEntry: RawDecodable
- DTO-слой.User: DTODecodable
- App-слой.
Более подробно об этом можно прочесть тут
Массивы с элемантами типа DTOConvertible
и RawMappable
также удовлетворяют этим протоколам и имеют реализацию по-умолчанию для их методов.
Отправка запроса в сеть начинается с того, что мы описываем:
- Маршрут - URI до нужного нам сервиса
- HTTP-метод - метод запроса (GET, PUT, e.t.c.)
- Кодировку - куда необходимо положить параметры и в каком виде (JSON in Body, String In Query, e.t.c)
- Метаданные - или хедеры запроса.
CoreNetKit построен таким образом, что одинаковую модель можно использовать для любого транспортного протокола, исключая или добавляя шаги при необходимости.
Далее я опишу толькко 1 и 3, потому что остальное не нуждается в объяснении.
Для того, чтобы абстрагировать способ задачи маршрута (например в gRPC нет явных URL) маршрут - generic-тип данных, однако в случае URL-запросов ожидается UrlRouteProvider
Такой подход делает работу с URL адресами немного элегантнее. Например:
enum RegistrationRoute {
case auth
case users
case user(String)
}
extension RegistrationRoute: UrlRouteProvider {
func url() throws -> URL {
let base = URL(string: "http://example.com")
switch self {
case .auth:
return try base + "/user/auth"
case .users:
return try base + "/user/users"
case .taskState:
return try base + "/tasks"
case .user(let id):
return try base + "/user/\(id)"
}
}
Хорошией практикой является разбиение маршрутов по сервисам или по отдельным файлам.
Для упрощения работы с URL в CoreNetKit есть расширение для конкатенации URL
и String
CoreNetKit предоставляет следующие виды кодировок:
json
- сериализует параметры запроса в JSON и прикрепляет к телу запроса. Является кодировкой по-умолчаниюformUrl
- сериализует парамтеры запроса в формат FormUrlEncoding иприкрепляет к телу запроса.urlQuery
- конвертирует параметры в строку, зменяя определенные символы на специальные последовательности (образует URL-encoded string)
Эти параметры находятся в ParametersEncoding
Для отправки запроса нужно вызывать цепочку и передать ей параметры, которые были описаны выше.
В качестве примера напишем сервис.
class ExampleService {
var builder: UrlChainsBuilder<RegistrationRoute> {
return .init()
}
func auth(user: User) -> Observer<Void> {
return self.builder
.route(.post, .auth)
.build()
.process(user)
.map { [weak self] (user: User) in
self?.saveToKeychain(user)
return ()
}
}
func getUser(by id: String) -> Observer<User> {
return self.builder
.route(.get, .user(id))
.build()
.process()
}
func getUsers() -> Observer<[User]> {
return self.builder
.route(.get, .users)
.build()
.process()
}
func updateState(by params:[String], descending: Bool, by map: [String: Any], max: Int, users: [User]) -> Observer<Void> {
return self.builder
.set(query: ["params": params], "desc": descending, "map": map, "max": maxCount)
.set(boolEncodingStartegy: .asBool)
.set(arrayEncodingStrategy: .noBrackets)
.route(.post, RegistrationRoute.taskState)
.build()
.process(users)
}
}
Ответ от сервиса приходит в DispatchQueue.main
, если поведение по-умолчанию не изменялось.
Сама цепочка с самого начинает свою работу в DispatchQueue.global(qos: .userInitiated)
(по-умолчанию)
Для выполнения запроса используются цепочки узлов.
Для работы с сервисом предлагается использовать абстрактную сущность - Observer<T>
.
Это Rx-Like объект, который имеет 4 возможных события:
onCompleted
- когда запрос выполнилсяonError
- когда произошла ошибкаdefer
- вызывается и в случае ошибки, и в случае успешного выполнения (аналогfinaly
вtry-catch
)onCanceled
- вызывается в случае, если операция,за которой наблюдаетObserver
была отменена
На самом деле этот объект повсеместно используется в библиотеке, а в качестве его реализации используется Context<T>
.
Документацую можно увидеть здесь и здесь
Так же более детальное описание работы контекстов находится тут
Рассмотрим как будет выглядеть работа с сервисом из презентера (или любой другой сущности, которая общается с сервером)
private let service = ExampleService()
func loadUsers() {
self.showLoader()
self.service.getUsers()
.onCompleted { [weak self] model in
self?.show(users: model)
}.onError { [weak self] error in
self?.show(error: error)
}.defer { [weak self] in
self?.hideLoader()
}
}
Библиотека предоставляет систему логгирования, которая более детально описана здесь