Search code examples
swiftswift-playground

Returning generic type from a URLRequest dataTask


I'm trying to build a generic function to return an array of a given type once a URLSession's dataTask has completed.

I have this Decodable:

struct Todo: Hashable, Codable, Identifiable {
    var id: Int
    var title: String
    var completed: Bool
}

And this function:

func loadFrom<T: Decodable>(url: String, memberType: T, completionHandler: (T?) -> Void) {
    guard let url = URL(string: url) else {
        completionHandler(nil)
    }

    URLSession.shared.dataTask(with: url) {data, response, error in
        guard let data = data else {
            fatalError("No data returned")
        }

        do {
            let decoder = JSONDecoder()
            let results = try decoder.decode(T.self, from: data)

            completionHandler(results)
        }catch {
            fatalError("Couldn't parse data")
        }
    }.resume()
}

loadFrom(url: "https://jsonplaceholder.typicode.com/todos?completed=true", memberType: Todo) {response in
    ...
}

error: Swift Scratchpad.playground:61:88: error: argument type 'Todo.Type' does not conform to expected type 'Decodable'

I've seen similar issues which point to the compiler being unable to synthesise the methods that the Decodable protocol requires for conformance, but building a similar method which is assigned to the Decodable type rather than having it specified as a parameter works:

func loadFile<T: Decodable>(file: String) -> T {
...
}

var todos: [Todo] = loadFile(file: "todos.json")
print(todos[0].title) => "The todo title"

I assume the fact that my loadFrom doesn't have its return type specified is the cause, but I don't understand why. Would it be possible to provide enough context for this code to compile?


Solution

  • You have to declare

    func loadFrom<T: Decodable>(url: String, memberType: T.Type, completionHandler: @escaping (T?) -> Void) {
    

    and call it

    loadFrom(url: "https://jsonplaceholder.typicode.com/todos?completed=true", memberType: Todo.self) {response in
        ...
    }
    

    And rather than the unhandy fatal errors add a better result type to be able to return also the errors and catch the bad URL before calling the method

    func loadFrom<T: Decodable>(url: URL, memberType: T.Type, completionHandler: @escaping (Result<T,Error>) -> Void) {
        URLSession.shared.dataTask(with: url) {data, _, error in
            if let error = error {
                completionHandler(.failure(error))
            } else {
               completionHandler( Result{ try JSONDecoder().decode(T.self, from: data!)})
            }
        }.resume()
    }