Search code examples
swiftrx-swiftmoya

Extending a class model for generic type Swift


I am passing API response with Moya and getting this value. I am able to get the object but I extented a base response to handle extra parameters but the extended value does not seem to work. The data expected could be an array of objects and it could just be a regular object. After passing this values, It stopped working and data is not got but every other parameter like status , message are passed except data. Here is my Base response and how I used it

class MaxResponseBase: Codable {
    var status: String?
    var message: String?
    var pagination: Pagination?

    var isSucessful: Bool {
        return status == "success"
    }

    struct ErrorMessage {
        static let passwordInvalid = " Current password is invalid."
        static let loginErrorIncorrectInfo = " Incorrect username/password."
        static let loginErrorAccountNotExist = " Invalid request"
    }
}

class MaxResponse<T: Codable>: MaxResponseBase {
    var data: T?
}

class MaxArrayResponse<T: Codable>: MaxResponseBase {
    var data = [T]()
}

Here is my API call for signin for example

func signin(email: String, password: String) -> Observable<MaxResponse<AuthResponse>> {
        return provider.rx.request(.signin(username: email, password: password))
            .filterSuccess()
            .mapObject(MaxResponse<AuthResponse>.self)
            .asObservable()
    }

how can I tweak this to get data object also

JSON

{
  "status" : "success",
  "data" : {
    "is_locked" : false,
    "__v" : 0,
    "created_at" : "2019-04-15T11:57:12.551Z"
  }
}

It could also be an array of data


Solution

  • (Note: all the code below can be put in a Playground to show that it works.)

    In order to solve this, you have to manually write all your initializers. I posted the code that does most of it below but I strongly recommend you use structs instead of classes. It is better in every way if you use structs and containment instead of classes and inheritance.

    struct Pagination: Codable { }
    
    struct AuthResponse: Codable {
        let isLocked: Bool
        let __v: Int
        let createdAt: Date
    }
    
    class MaxResponseBase: Codable {
        let status: String?
        let message: String?
        let pagination: Pagination?
    
        var isSucessful: Bool {
            return status == "success"
        }
    
        struct ErrorMessage {
            static let passwordInvalid = " Current password is invalid."
            static let loginErrorIncorrectInfo = " Incorrect username/password."
            static let loginErrorAccountNotExist = " Invalid request"
        }
    
        enum CodingKeys: String, CodingKey {
            case status, message, pagination
        }
    
        required init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            status = try container.decode(String?.self, forKey: .status)
            message = try? container.decode(String?.self, forKey: .message) ?? nil
            pagination = try? container.decode(Pagination?.self, forKey: .pagination) ?? nil
        }
    }
    
    class MaxResponse<T: Codable>: MaxResponseBase {
        let data: T?
    
        enum DataCodingKeys: String, CodingKey {
            case data
        }
    
        required init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: DataCodingKeys.self)
            data = try container.decode(T?.self, forKey: .data)
            try super.init(from: decoder)
        }
    }
    
    let json = """
    {
    "status" : "success",
    "data" : {
    "is_locked" : false,
    "__v" : 0,
    "created_at" : "2019-04-15T11:57:12.551Z"
    }
    }
    """
    
    let data = json.data(using: .utf8)!
    
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "YYYY-MM-dd'T'HH:mm:ss.SSZ"
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    decoder.dateDecodingStrategy = .formatted(dateFormatter)
    let response = try decoder.decode(MaxResponse<AuthResponse>.self, from: data)
    
    print(response)
    

    It is far simpler and less code to just use a struct:

    struct AuthResponse: Codable {
        struct ResponseData: Codable {
            let isLocked: Bool
            let __v: Int
            let createdAt: Date
        }
        let status: String?
        let data: ResponseData
    }
    
    let json = """
    {
    "status" : "success",
    "data" : {
    "is_locked" : false,
    "__v" : 0,
    "created_at" : "2019-04-15T11:57:12.551Z"
    }
    }
    """
    
    let data = json.data(using: .utf8)!
    
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "YYYY-MM-dd'T'HH:mm:ss.SSZ"
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    decoder.dateDecodingStrategy = .formatted(dateFormatter)
    let response = try decoder.decode(AuthResponse.self, from: data)
    
    print(response)
    

    And if you really need the MaxResponse type, then make it a protocol and have your other types conform to it. I'm almost willing to bet that you don't need it though.


    In response to your comments, here is a generic solution using structs:

    struct LoginResponseData: Codable {
        let isLocked: Bool
        let __v: Int
        let createdAt: Date
    }
    
    struct BlogResponseData: Codable {
        let xxx: Bool
        let yyy: Int
        let createdAt: Date
    }
    
    struct BaseRresponse<ResponseData: Codable>: Codable {
        let status: String?
        let data: ResponseData
    }