In the previous part we discovered a way to separate our application into frameworks and setup the architecture of our app to support dependency injection. In this part of the series we'll be fetching and parsing data from the backend using Alamofire and Codable.
Although in the scope of this tutorial we'll be using mocked data, the application will be completely ready to support calls to REST APIs.
We define our APIClient protocol that serves as a lean interface between data fetching classes and actual implementation.
import RxSwift
public protocol APIClient {
func get(path: String) -> Observable<Any>
}
It returns Observable<Any>
which is a part of RxSwift
. We won't be going through the basics of RxSwift
, so it's beneficial to take a look official documentation before continuing.
The actual implementation is in BaseAPIClient, which uses Alamofire for making HTTP requests. The only method get(path: String)
makes GET
request by concating given path to a base URL.
import RxAlamofire
import RxSwift
public class BaseAPIClient: APIClient {
private let baseUrl: String
public init(baseUrl: String) {
self.baseUrl = baseUrl
}
public func get(path: String) -> Observable<Any> {
return RxAlamofire
.requestJSON(.get, "\(baseUrl)/\(path)")
.map { $1 }
}
}
If you clone the repository, it will use MockAPIClient which takes data from files. Because it uses the same public interface, MockAPIClient
and BaseAPIClient
can be interchanged depending on needs. See ApplicationAssembly which assigns dependencies for APIClient
interface. Depending on different configuration, it can assign any of these two. This little example perfectly illustrates the power of dependency injection
and usage of protocols
.
The main entity in this project is a Trip
. It describes the origin and destination of the flight as well as price and dates.
{
"currency":"EUR",
"created_at":1547991979887,
"airlines":"FR",
"departure_at":1552848000000,
"destination":{
"city":"Malaga",
"country_code":"ES",
"airport_code":"AGP"
},
"flight_number":4048,
"departure":{
"city":"Copenhagen",
"country_code":"DK",
"airport_code":"CPH"
},
"return_at":1553153100000,
"price":72,
"id":"c4449ff0-1cb9-11e9-b9f8-b3ba95b35000",
"expires_at":1739200281000
}
We'll define our entities inside TravelKit
framework. They should be made public, so they could be reached inside other frameworks. We'll use excellent Codable type that starting from Swift 4 provides a powerful and clean way to encode and decode data.
Take a look at Trip class. We don't need to define keys of each values if they match. It's possible to define what naming strategies are used during decoding or encoding process. For example, .convertFromSnakeCase
strategy, as its name suggests, converts keys from snake case and assigns values automatically if they match.
import Foundation
public struct Trip: Codable, Equatable {
public var id: String = ""
public var currency: String = ""
public var price = 0
public var airlines = ""
public var flightNumber = 0
public var destination: TripLocation!
public var departure: TripLocation!
public var createdAt = Date()
public var departureAt = Date()
public var returnAt = Date()
public var expiresAt = Date()
public init() {}
}
public struct TripLocation: Codable, Equatable {
public var city: String!
public var countryCode: String!
public var airportCode: String!
public init() {}
}
After receiving JSON
data we can define decoder
and automatically parse values.
public static var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .millisecondsSince1970
return decoder
}()
let trips = try? decoder.decode([Trip].self, from: data)
With this simple and straightforward Codable API our data is cleanly parsed into statically typed object or array of objects after fetching from API.
Classes that are used to fetch data will be called repositories. In TravelKit
we'll only define the protocols of these repositories. Our UI framework TravelFeatureKit
will only know about TravelKit
and protocols of repositories thus the implementations, defined in TravelDataKit
, will be easily changeable.
Our TripRepository
protocol defines the only way to fetch trips.
import RxSwift
public protocol TripRepository {
func getTrips(in region: String?) -> Observable<[Trip]>
}
Because our UI framework will only know about this protocol, we will be able to provide different types of implementations. TripRepository implementation defined in TravelDataKit
calls the API
to fetch data and parses it using Coadable
. However, FavoriteTripRepository which also implements TripRepository
interface, uses UserDefaults
to fetch locally liked Trips
. It allows us to generate 2 completely different screens in our app. One showing the current feed of flights fetched from the API and another of liked and locally saved trips. Here FavoritesAssembly simply injects necessary dependencies needed for favorites to a FavoriteFeed
feature.
Before continuing creating the app, we'll see how we can quickly create simple application designs using Sketch or similar tools. In the next part of the series we'll overview the approach.