Search code examples
iosswiftstructcodable

Refining Swift API GET Functions


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()
  }
}

Solution

  • 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)
        }
    }