So many ways to skin a networking layer
There are myriads of ways to organize & structure your networking layer code in an iOS app but at the end of the day the best approach is to have a clear separation of concerns between objects at play
There are myriads of ways to organize and structure your networking service layer code in an iOS application, but at the end of the day the best approach is to have a clear separation of concerns between objects that make requests, objects that process and map data from requests, and objects that initiate requests.
In the 2nd edition of The iOS Interview Guide I’m currently working on, I covered this concept in the interview question “How do you typically implement networking on iOS?”.
The main idea is that you want a separation such as where you have an object, such as an APIClient (that makes HTTP requests), and a Service object (that uses it to make specific requests to specific endpoints).
The APIClient object knows how to compose a generic GET/POST/etc. request and how to set up default headers and other parameters and configuration. The service object on the other hand knows what URL to send requests to, what input format it takes, what response schema it is, and how map response to domain model objects.
Here’s an excerpt of a code sample from the book:
import Foundation
protocol APIClientInterface {
func GET(url: URL,
completion: @escaping (_ data: Data) -> Void) -> URLSessionTask
func GET(url: URL) async throws -> Data
}
final class APIClient: APIClientInterface {
private let urlSession: URLSession
init(urlSession: URLSession) {
self.urlSession = urlSession
}
func GET(url: URL,
completion: @escaping (_ data: Data) -> Void) -> URLSessionTask {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "GET"
let dataTask = urlSession.dataTask(with: urlRequest) { data, _, error in
if let data {
completion(data)
}
}
return dataTask
}
func GET(url: URL) async throws -> Data {
let (data, _) = try await urlSession.data(from: url)
return data
}
}
struct Post: Codable {
let title: String
let body: String
}
struct AllPostsResponse: Codable {
let posts: [Post]
}
protocol PostsServicable {
func fetchUserPosts(userId: String,
completion: @escaping (_ posts: [Post]) -> Void)
}
final class PostsService: PostsServicable {
private let apiClient: APIClientInterface
private let decoder = JSONDecoder()
init(apiClient: APIClientInterface) {
self.apiClient = apiClient
}
func fetchUserPosts(userId: String,
completion: @escaping (_ posts: [Post]) -> Void) {
guard let url = buildGetAllPostsUrl(withUserId: userId) else { return }
let task = apiClient.GET(url: url) { data in
if let response = self.mapDataToAllPostsResponse(data) {
completion(response.posts)
}
}
task.resume()
}
private func buildGetAllPostsUrl(withUserId userId: String) -> URL? {
URL(string: "https://newsletter.mobileengineer.io/user/\(userId)/posts")
}
private func mapDataToAllPostsResponse(_ data: Data) -> AllPostsResponse? {
try? decoder.decode(AllPostsResponse.self, from: data)
}
}
There is a lot more nuance and details in the book in regards to this code sample such as usage of completion blocks vs async/await and more.
Subscribe to hear more updates on the book’s progress like this one. Or sign up for the waitlist on https://iosinterviewguide.com/ to only hear about big “official” book updates such as when pre order starts or when each chapter is updated and released (I’ll also post those updates here on The Mobile Engineer substack too).