I'm working on a practice project where the iOS app prints a list of /posts from jsonplaceholder.typicode.com, and when the user selects one a detailed view controller is loaded and further information about that post is displayed (author and number of comments).
I've made three separate GET requests for the three different endpoints, as each of them require different return types and different parameters (or none at all).
I wanted to take as much code as possible that's in common between the three and put it in a new function to tidy up the class, but I feel as though I could do a lot more.
Is there a way to make the return type of these Structs more generic, with a Switch to determine which to map the JSON response to? Any guidance would be greatly appreciated.
import UIKit
struct Post: Codable {
let userId: Int
let id: Int
let title: String
let body: String
}
struct Author: Codable {
let name: String
}
struct Comment: Codable {
let postId: Int
let id: Int
let name: String
let email: String
let body: String
}
enum Result<Value> {
case success(Value)
case failure(Error)
}
class APIManager {
static let sharedInstance = APIManager()
func getUrl(for path: String) -> URL {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "jsonplaceholder.typicode.com"
urlComponents.path = path
guard let url = urlComponents.url else { fatalError("Could not create URL from components") }
return url
}
func getPosts(completion: ((Result<[Post]>) -> Void)?) {
let url = getUrl(for: "/posts")
var request = URLRequest(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { (responseData, response, responseError) in
DispatchQueue.main.async {
if let error = responseError {
completion?(.failure(error))
} else if let jsonData = responseData {
let decoder = JSONDecoder()
do {
let posts = try decoder.decode([Post].self, from: jsonData)
completion?(.success(posts))
} catch {
completion?(.failure(error))
}
} else {
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(error))
}
}
}
task.resume()
}
func getAuthor(for userId: Int, completion: ((Result<String>) -> Void)?) {
let url = getUrl(for: "/users/\(userId)")
var request = URLRequest(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { (responseData, response, responseError) in
DispatchQueue.main.async {
if let error = responseError {
completion?(.failure(error))
} else if let jsonData = responseData {
let decoder = JSONDecoder()
do {
let author = try decoder.decode(Author.self, from: jsonData)
completion?(.success(author.name))
} catch {
completion?(.failure(error))
}
} else {
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(error))
}
}
}
task.resume()
}
func getComments(for postId: Int, completion: ((Result<[Comment]>) -> Void)?) {
let url = getUrl(for: "/posts/\(postId)/comments")
var request = URLRequest(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { (responseData, response, responseError) in
DispatchQueue.main.async {
if let error = responseError {
completion?(.failure(error))
} else if let jsonData = responseData {
let decoder = JSONDecoder()
do {
let comments = try decoder.decode([Comment].self, from: jsonData)
completion?(.success(comments))
} catch {
completion?(.failure(error))
}
} else {
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(error))
}
}
}
task.resume()
}
}
Just take advantage of the generic Result
type:
class APIManager {
static let sharedInstance = APIManager()
private func getUrl(for path: String) -> URL {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "jsonplaceholder.typicode.com"
urlComponents.path = path
guard let url = urlComponents.url else { fatalError("Could not create URL from components") }
return url
}
private func postsURL() -> URL { return getUrl(for: "/posts") }
private func usersURL(for userId : Int) -> URL { return getUrl(for: "/users/\(userId)") }
private func commentsURL(for postId : Int) -> URL { return getUrl(for: "/posts/\(postId)/comments") }
func getPosts(completion: @escaping (Result<[Post]>) -> Void) {
getInfo(for: postsURL(), completion: completion)
}
func getAuthor(for userId: Int, completion: @escaping (Result<Author>) -> Void) {
getInfo(for: usersURL(for: userId), completion: completion)
}
func getComments(for postId: Int, completion: @escaping (Result<[Comment]>) -> Void) {
getInfo(for: commentsURL(for: postId), completion: completion)
}
private func getInfo<T: Decodable>(for url : URL, completion: @escaping (Result<T>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
} else {
let decoder = JSONDecoder()
do {
let comments = try decoder.decode(T.self, from: data!)
completion(.success(comments))
} catch {
completion(.failure(error))
}
}
}
}
task.resume()
}
}
and use it
let manager = APIManager.sharedInstance
manager.getAuthor(for: 1) { result in
switch result {
case .success(let author) : print(author.name)
case .failure(let error) : print(error)
}
}