Search code examples
iosswiftswift-protocolsdecodable

How to save a Decodable.Protocol object to a variable?


In my application, several controllers have a very similar code structure. The differences are minimal. So for optimization I decided to create a basis for these controllers and inherit each specific controller from the basis.

I have a function for sending network requests and processing a response. I pass the response structure as a parameter to this function so that the function returns a ready-made response structure to me. Each such structure is Decodable.

An example of such a structure:

struct APIAnswerUserActivity: Decodable {
    let status: String
    let code: Int?
    let data: [UserActivity]?
    let total: Int?
}

Function for network requests, an object (structure) of the Decodable.Protocol type is accepted as a jsonType parameter:

public func networkRequest<T: Decodable> (
    url: String,
    timeout: Double = 30,
    method: URLMethods = .GET,
    data: [String : String]? = nil,
    files: [URL]? = nil,
    jsonType: T.Type,
    success: @escaping (T) -> Void,
    failure: @escaping (APIError) -> Void
) -> URLSessionDataTask { ... }

There are several parameters in the main controller that I override through override in the child controllers. One of these parameters should be an object of type Decodable for the general function to receive data correctly. The JSON structures of the response are very similar, but still slightly different. A common structure for them cannot be created because the data is still a little different.

If in the main controller do this:

public var decodableType: Decodable.Type {
    return APIAnswerUserActivity.self
}

That will work, and it is possible to redefine types, but the network function does not accept this. It needs the Decodable.Protocol object. If the type decodable.Protocol is specified for the variable decodableType, then it is no longer possible to add APIAnswerUserActivity.self, which is quietly accepted when the networkRequest function is called.

How to be in this situation? I hope that I managed to correctly and clearly state the essence of my problem.


Solution

  • @Владислав Артемьев, I'm still not sure that I completely understand the problem because you haven't shared the code that takes the Decodable class. But the issues seems to be about how to pass a Decodable class.

    I hope the following can help clarify how you can impose the right constraint on the generic and how you should declare the variable. You can paste it into a playground and experiment.

    import Foundation
    
    struct FakeToDo: Decodable {
        var userId: Int
        var id: Int
        var title: String
        var completed: Bool
    }
    
    enum URLMethods {
        case GET
        case POST
    }
    
    func networkRequest<T: Decodable> (
        url: String,
        timeout: Double = 30,
        method: URLMethods = .GET,
        data: [String : String]? = nil,
        files: [URL]? = nil,
        jsonType: T.Type,
        success: @escaping (T) -> Void,
        failure: @escaping (Error) -> Void
    ) -> URLSessionDataTask {
    
        let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { (data, response, error) in
            if error != nil {
                failure(error!)
                return
            }
    
            guard let data = data else { return }
    
            let decoder = JSONDecoder()
            guard let value = try? decoder.decode(T.self, from: data) else { return }
    
            // get back on the main queue for UI
            DispatchQueue.main.async {
                success(value)
            }
        })
        return task
    }
    
    class Example<T> where T: Decodable {
        let type: T.Type
    
        init(_ type: T.Type) {
            self.type = type
        }
    
        public var decodableType: T.Type {
            return type
        }
    }
    
    let decodableType = Example(FakeToDo.self).decodableType
    
    let url = "https://jsonplaceholder.typicode.com/todos/1"
    let task = networkRequest(url: url, jsonType: decodableType,
                              success: { value in print(value) },
                              failure: { error in print(error) })
    
    task.resume()