iOS Interview Assessment (Part 2)

Building a Network Layer

Aryaman Sharda
5 min readMar 28, 2020

Let’s begin with the foundation of our take home assessment — the network layer. As an interviewer, I’d want to see, for a project like this, a light-weight extensible network layer. If a candidate used something like Alamofire, I’d take it to suggest they’re just unfamiliar with URLSession which would be a red flag.

Let’s consider how we should organize our endpoints. We know that generally speaking for these take home assessments, the endpoints we’ll be using are performing pretty basic CRUD operations. So, we can keep our endpoint model pretty simple and start off with something like this:

protocol Endpoint {
/// HTTP or HTTPS
var scheme: String {
get
}
// Example: "api.flickr.com"
var baseURL: String {
get
}
// "/services/rest/"
var path: String {
get
}
// [URLQueryItem(name: "api_key", value: API_KEY)]
var parameters: [URLQueryItem] {
get
}
// "GET"
var method: String {
get
}
}

With this infrastructure in place, let’s try and define an actual endpoint, in this case Flickr’s search results endpoint: With

enum FlickrEndpoint: Endpoint {
case getSearchResults(searchText: String, page: Int)
var scheme: String {
switch self {
default: return "https"
}
}
var baseURL: String {
switch self {
default: return "api.flickr.com"
}
}
var path: String {
switch self {
case.getSearchResults:
return "/services/rest/"
}
}
var parameters: [URLQueryItem] {
let apiKey = "123456789012345678901234567890"
switch self {
case.getSearchResults(let searchText,
let page):
return [URLQueryItem(name: "text", value: searchText),
URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "method", value: "flickr.photos.search"),
URLQueryItem(name: "format", value: "json"),
URLQueryItem(name: "per_page", value: "20"),
URLQueryItem(name: "nojsoncallback", value: "1"),
URLQueryItem(name: "api_key", value: apiKey)
]
}
}
var method: String {
switch self {
case.getSearchResults:
return "GET"
}
}
}

Remember that in Swift, enums can implement protocols just like structs and classes. So, we can create a FlickrEndpoint entity that implements the protocol endpoint we defined earlier.

Now, for every new endpoint we want to support, we can just introduce a new case to the enum which will force us to handle it in the code above helping us prevent unintended states.

Additionally, it doesn’t always mean we need to write a lot of new code because we can rely on the default case for an enum like we do in the case of the baseURL. We can only make changes to the part of the endpoint requests that change and let everything else defer to the default implementation.

Another point to mention is how we might store our API Key if the project requires it. I think for a take home project, it’s totally fine just to include it in the project in plain text, but you should mention either through a comment or in the interview, that in the real-world implementation you’d prefer a much more rigorous authentication flow and that including the API key in the client is inherently unsecure.

Now at this point we have a standard for defining an endpoint and a readable and extensible way of adding additional endpoint requests to our application.

Let’s now take a look at how we’d make the network request itself.

Nowadays, I tend to avoid using libraries like SwiftyJSON and prefer to use just Codables and from an interviewing standpoint, the interviewer is probably looking to see that you’ve got a strong grasp on the functionality that Apple gives you and endorses.

Let’s go ahead and define some models. As you can see they all implement the Codable protocol and are derived from looking at the JSON the Flickr API sends back.

struct FlickrResponse: Codable {
let photos: FlickrResultsPage ?
}
struct FlickrResultsPage: Codable {
let page: Int
let pages: Int
let photo: [FlickPhoto]
}
struct FlickPhoto: Codable {
let id: String
let owner: String
let secret: String
let server: String
let farm: Int
}

A few things to note here, we’re making them structs because we want our models to be immutable. We don’t want to accidentally change their values elsewhere in the code without realizing it.

Another reason for creating these Codable models here is that our interaction with our API endpoints will likely be sending or receiving some JSON object, so we can think about that communication as “transacting” with Codable objects which means our network request calls could be viewed as generic functions operating on Codables

Now we’re finally ready to get to the Network Engine

  1. We create a function called request that operates on Codables. It takes in something that implements the Endpoint protocol and it returns a completion block with a Codable object if the network request succeeded, or an error.
  2. Next, we build our URL by assigning our Endpoint properties to a URLComponents object
  3. Then, we have a check to make sure the URL was constructed without issues and is not nil
  4. Then, we use our URL object to create a URLRequest and set the HTTP Method
  5. Then, we get a reference to URLSession and define our data task. In the closure we handle the case of getting the data correctly, grabbing the HTTP response, and handling the error.
  6. After some basic error handling and validation, we can try and convert the data from the response into the expected Codable object “T”, in this case
  7. If it succeeds, we provide that value in the completion block through the Result enum’s associated values, if it fails, we create an error definition and pass that back instead.
  8. Finally, calling dataTask.resume() actually triggers the network request.
class NetworkEngine {
/// Executes the web call and will decode the JSON response into the Codable object provided
/// - Parameters:
/// - endpoint: the endpoint to make the HTTP request against
/// - completion: the JSON response converted to the provided Codable object, if successful, or failure otherwise
//1
class func request < T: Codable > (endpoint: Endpoint, completion: @escaping(Result < T, Error > ) - > ()) {
//2
var components = URLComponents()
components.scheme = endpoint.scheme
components.host = endpoint.baseURL
components.path = endpoint.path
components.queryItems = endpoint.parameters
//3
guard
let url = components.url
else {
return
}
//4
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = endpoint.method
//5
let session = URLSession(configuration: .default)
let dataTask = session.dataTask(with: urlRequest) {
data,
response,
error in
// 6
guard error == nil
else {
completion(.failure(error!))
print(error ? .localizedDescription ? ? "Unknown error")
return
}
guard response != nil,
let data = data
else {
return
}
DispatchQueue.main.async {
if let responseObject =
try ? JSONDecoder().decode(T.self, from: data) {
//7
completion(.success(responseObject))
} else {
let error = NSError(domain: "", code: 200, userInfo: [NSLocalizedDescriptionKey: "Failed to decode response"])
completion(.failure(error))
}
}
}
//8
dataTask.resume()
}
}

Here’s what everything looks like put together:

NetworkEngine.request(endpoint: FlickrEndpoint.getSearchResults(searchText: "iOS", page: 1)) {
(result: Result < FlickrResponse, Error > ) in
switch result {
case.success(let response):
print("Response: ", response)
case.failure(let error):
print(error)
}
}

In the next part, we’ll look at setting up the UI, fetching images asynchronously, and creating our caching layer.

--

--

Aryaman Sharda
Aryaman Sharda

Written by Aryaman Sharda

Staff iOS Engineer @ Turo. Previously, Scoop Technologies & Porsche Digital

No responses yet