Gnomon
Yet another networking library?
Why did we decide to develop a new networking library?
-
HTTP cache for better UX. It's better to display outdated content than showing to user infinite loading indicator. To achieve this Gnomon is able to perform two-in-one requests: first request to receive cached content (optional) and second request to make an HTTP call. Furthermore, the library will return cached version even if there is no internet connection.
-
RxSwift interface. We decided that it is much easier to handle such compound responses with help of Rx observables: the first
.next
event comes with a cached response, the second – with an actual response from web server. -
Models instead of Data as request result. This is the usual step after app received Data – parse it to some model. We found it better to let library user skip this phase: every request has a generic parameter – response model type. As result subscriber receives model or array of models instead of plain Data.
Installation
pod 'Gnomon', '~> 3.0'
Usage
Define a model
We defined flexible generic protocol BaseModel
and several extensions for it (JSONModel
, XMLModel
, DecodableModel
).
In most cases, we use JSONModel
, which uses SwiftyJSON as a parser and provides a lightweight interface for model properties parsing.
import Gnomon
import SwiftyJSON
struct UserModel: JSONModel {
let id: Int
let login: String
let avatarUrl: URL?
let profileUrl: URL?
init(_ json: JSON) throws {
id = json["id"].intValue
login = json["login"].stringValue
avatarUrl = json["avatar_url"].url
profileUrl = json["html_url"].url
}
}
Prepare a request
We have a flexible RequestBuilder
which requires only URL string as an argument but provides builder interface to construct your complex HTTP request.
There are 4 Result
types in the library – they configure how the library will parse your response:
SingleResult<T>
single modelT
, the request fails if parsing failsMultipleResults<T>
array of modelT
, the request fails if parsing failsSingleOptionalResult<T>
single optional modelT?
, request returnsnil
if parsing failsMultipleOptionalResults<T>
array of optional modelsT?
, request returns a mixed array of models andnil
s if parsing fails
func prepareRequest() throws -> Request<MultipleOptionalResults<UserModel>> {
return try RequestBuilder()
.setURLString("https://api.github.com/users").build()
}
This API call returns us array of dictionaries, but we often meet calls which return multi-layered JSON where we need to parse only one part. In this case you can add setXPath()
to your builder with path divided by /
(e.g. "document/result/data"
).
Make an observable and subscribe to it
As we wrote above Gnomon interface is RxSwift-based – when you want to make a request you need to create an observable and then subscribe to it.
import RxSwift
let disposeBag = DisposeBag()
func loadData() {
do {
let request = try prepareRequest()
Gnomon.cachedThenFetch(request).subscribe(onNext: { response in
print(response.result.models)
}).disposed(by: disposeBag)
} catch {
print("can't prepare request: \(error)")
}
}
When we run this code first time we will see two logs in stdout: empty array and array of optional UserModel
s.
After first call URLSession will store response in shared URLCache and on next call we will receive equal arrays twice.
You can optimize your UI updating logic by omitting UI update if you received cached version as second response. It possible because of URLSession internal logic: it checks cache expiration / validity by itself and can return cached response twice.
You can detect HTTP cache result by response.responseType
property.
import RxSwift
let disposeBag = DisposeBag()
func loadData() {
do {
let request = try prepareRequest()
Gnomon.cachedThenFetch(request).subscribe(onNext: { [weak self] response in
guard response.responseType != .httpCache else { return }
self?.updateUI(with: response.result.models)
}).disposed(by: disposeBag)
} catch {
print("can't prepare request: \(error)")
}
}
func updateUI(with models: [UserModel?]) {}
Astrolabe
Gnomon is the best friend of Astrolabe :)
You can easily transform your models to cells for UITableView or UICollectionView with help of Astrolabe and Loaders.
import Astrolabe
import Gnomon
class UsersViewController: UIViewController, Loader {
let tableView = TableView<LoaderDecoratorSource<TableViewSource>>
override func viewDidLoad() {
super.viewDidLoad()
tableView.source.loader = self
}
func load(for intent: LoaderIntent) -> SectionObservable? {
return Astrolabe.load(pLoader: self, intent: intent)
}
}
extension UsersViewController: PLoader {
typealias PLResult = MultipleOptionalResults<UserModel>
func request(for loadingIntent: LoaderIntent) throws -> Request<PLResult> {
return try RequestBuilder()
.setURLString("https://api.github.com/users").build()
}
func sections(from result: PLResult, loadingIntent: LoaderIntent) -> [Sectionable]? {
let users = result.models.flatMap { $0 }
return [Section(cells: users.map { TableCell<UserTableViewCell>(data: $0) })]
}
}