Search code examples
iosswiftjsondecoder

Swift - Initialise model object with init(from decoder:)


Below is my model struct

struct MovieResponse: Codable {
    
    var totalResults: Int
    var response: String
    var error: String
    var movies: [Movie]
    
    enum ConfigKeys: String, CodingKey {
        case totalResults
        case response = "Response"
        case error = "Error"
        case movies
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults)!
        self.response = try values.decodeIfPresent(String.self, forKey: .response)!
        self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
        self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies)!
    }
}

extension MovieResponse {
    struct Movie: Codable, Identifiable {
        var id = UUID()
        var title: String
        var year: Int8
        var imdbID: String
        var type: String
        var poster: URL
        
        enum EncodingKeys: String, CodingKey {
            case title = "Title"
            case year = "Year"
            case imdmID
            case type = "Type"
            case poster = "Poster"
        }
    }
}

Now in a ViewModel, I am creating an instance of this model using the below code

@Published var movieObj = MovieResponse()

But there is a compile error saying, call init(from decoder) method. What is the proper way to create a model instance in this case?


Solution

  • As the Swift Language Guide reads:

    Swift provides a default initializer for any structure or class that provides default values for all of its properties and doesn’t provide at least one initializer itself.

    The "and doesn’t provide at least one initializer itself" part is crucial here. Since you are declaring an additional initializer you should either declare your own initialized like so:

    init(
        totalResults: Int,
        response: String,
        error: String,
        movies: [Movie]
    ) {
        self.totalResults = totalResults
        self.response = response
        self.error = error
        self.movies = movies
    }
    

    or move Codable conformance to an extension so Swift can provide you with a default initialiser. This would be a preferred way to do it (my personal opinion, I like to move additional protocol conformances to extensions).

    struct MovieResponse {
        var totalResults: Int
        var response: String
        var error: String
        var movies: [Movie]
    }
    
    extension MovieResponse: Codable {
    
        enum ConfigKeys: String, CodingKey {
            case totalResults
            case response = "Response"
            case error = "Error"
            case movies
        }
    
        init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults)!
            self.response = try values.decodeIfPresent(String.self, forKey: .response)!
            self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
            self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies)!
        }
    }