Swift 4 collection of Codable objects - ios

I have a Codable class with a variable that holds a dictionary with String keys and values that may be Strings, Ints or custom structs conforming to Codable. My question is:
How do I define a dictionary with values that are Codable?
I hoped it would be enough to say
var data: [String: Codable] = [:]
but apparently that makes my class no longer conform to Codable. I think the problem here is the same as I had here, where I am passing a protocol rather than an object constrained by the protocol
Using JSON Encoder to encode a variable with Codable as type
So presumably I would need a constrained generic type, something like AnyObject<Codable> but that's not possible.
EDIT: Answer
So since this can't be done as per the accepted answer, I am resorting to a struct with dictionaries for each data type
struct CodableData {
var ints: [String: Int]
var strings: [String: String]
//...
init() {
ints = [:]
strings = [:]
}
}
var data = CodableData()

A Swift collection contains just one type, and for it to be Codable, that type would need to be Codable — not Codable the type, but an adopter of the Codable protocol. You need the type here to be some one specific Codable adopter.
Therefore the only way to do what you're describing would be basically to invent some new type, StringOrIntOrStruct, that wraps a String or an Int or a struct and itself adopts Codable.
But I don't think you're doing to do that, as the effort seems hardly worth it. Basically, you should stop trying to use a heterogeneous Swift collection in the first place; it's completely against the spirit of the language. It's a Bad Smell from the get-go. Rethink your goals.

I think the compiler is simply not smart enough to infer the Codable implementation automatically. So one possible solution is to implement Codable by hand:
class Foo: Codable {
var data: [String:Codable] = [:]
func encode(to encoder: Encoder) throws {
// ...
}
required init(from decoder: Decoder) throws {
// ...
}
}
I don’t know whether you can change the data type to help the compiler do the work for you. At least you can pull the hand-coding to the problematic type:
enum Option: Codable {
case number(Int)
case string(String)
func encode(to encoder: Encoder) throws {
// ...
}
init(from decoder: Decoder) throws {
if let num = try? Int(from: decoder) {
self = .number(num)
} else if let str = try? String(from: decoder) {
self = .string(str)
} else {
throw something
}
}
}
class Foo: Codable {
var data: [String:Option] = [:]
}

Have any of you tried Generics in Swift?
Look:
public struct Response<T> where T : Codable {
let ContentEncoding : String?
let ContentType : String?
let JsonRequestBehavior : Int?
let MaxJsonLength : Int?
let RecursionLimit : Int?
let data : Data?
public struct Data : Codable {
let response : String?
let status : String?
let details : [T]?
}
}

Related

Swift Codable struct with a generic property

Say we've got a cursor based paginated API where multiple endpoints can be paginated. The response of such an endpoint is always as follows:
{
"nextCursor": "someString",
"PAYLOAD_KEY": <generic response>
}
So the payload always returns a cursor and the payload key depends on the actual endpoint we use. For example if we have GET /users it might be users and the value of the key be an array of objects or we could cal a GET /some-large-object and the key being item and the payload be an object.
Bottom line the response is always an object with a cursor and one other key and it's associated value.
Trying to make this generic in Swift I was thinking of this:
public struct Paginable<Body>: Codable where Body: Codable {
public let body: Body
public let cursor: String?
private enum CodingKeys: String, CodingKey {
case body, cursor
}
}
Now the only issue with this code is that it expects the Body to be accessible under the "body" key which isn't the case.
We could have a struct User: Codable and the paginable specialized as Paginable<[Users]> where the API response object would have the key users for the array.
My question is how can I make this generic Paginable struct work so that I can specify the JSON payload key from the Body type?
The simplest solution I can think of is to let the decoded Body to give you the decoding key:
protocol PaginableBody: Codable {
static var decodingKey: String { get }
}
struct RawCodingKey: CodingKey, Equatable {
let stringValue: String
let intValue: Int?
init(stringValue: String) {
self.stringValue = stringValue
intValue = nil
}
init(intValue: Int) {
stringValue = "\(intValue)"
self.intValue = intValue
}
}
struct Paginable<Body: PaginableBody>: Codable {
public let body: Body
public let cursor: String?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RawCodingKey.self)
body = try container.decode(Body.self, forKey: RawCodingKey(stringValue: Body.decodingKey))
cursor = try container.decodeIfPresent(String.self, forKey: RawCodingKey(stringValue: "nextCursor"))
}
}
For example:
let jsonString = """
{
"nextCursor": "someString",
"PAYLOAD_KEY": {}
}
"""
let jsonData = Data(jsonString.utf8)
struct SomeBody: PaginableBody {
static let decodingKey = "PAYLOAD_KEY"
}
let decoder = JSONDecoder()
let decoded = try? decoder.decode(Paginable<SomeBody>.self, from: jsonData)
print(decoded)
Another option is to always take the "other" non-cursor key as the body:
struct Paginable<Body: Codable>: Codable {
public let body: Body
public let cursor: String?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RawCodingKey.self)
let cursorKey = RawCodingKey(stringValue: "nextCursor")
cursor = try container.decodeIfPresent(String.self, forKey: cursorKey)
// ! should be replaced with proper decoding error thrown
let bodyKey = container.allKeys.first { $0 != cursorKey }!
body = try container.decode(Body.self, forKey: bodyKey)
}
}
Another possible option is to pass the decoding key directly to JSONDecoder inside userInfo and then access it inside init(from:). That would give you the biggest flexibility but you would have to specify it always during decoding.
You can use generic model with type erasing, for example
struct GenericInfo: Encodable {
init<T: Encodable>(name: String, params: T) {
valueEncoder = {
var container = $0.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: . name)
try container.encode(params, forKey: .params)
}
}
// MARK: Public
func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
// MARK: Internal
enum CodingKeys: String, CodingKey {
case name
case params
}
let valueEncoder: (Encoder) throws -> Void
}

Generic function which accepts T.Type or [T].Type as parameter and return T

I am trying to write a generic function which accepts Generic T.Type as function argument, but it is not accepting T.Type as array
public protocol Mappable {
}
class User: Mappable {
var name: String?
}
class MyClass {
func request<T: Mappable>(responseType: T.Type) -> T {
let objT = .....
return objT
}
}
let myCls = MyClass()
myCls.request(responseType: User.self) // Works as expected
myCls.request(responseType: [User].self) // Error: Instance method 'request(responseType:)' requires that '[User]' conform to 'Mappable'
Experiment:
If I replace Mappable with Decodable in above code, it works fine. but I want to use my Protocol and not Decodable
I want to achieve similar to
struct Car: Decodable {
var name: String?
}
let data = "{\"name\": \"Lamborghini\"}".data(using: .utf8)
let dataArray = "[{\"name\": \"Lamborghini\"}, {\"name\": \"Ferrari\"}]".data(using: .utf8)
let car = try! JSONDecoder().decode(Car.self, from: data!) // Will return single object of Car
let cars = try! JSONDecoder().decode([Car].self, from: dataArray!) // Will return Array of Car
I am not able to figure out why it works with Decodable and not with my Mappable Protocol
it's because the array is decodable if it's element type is decodable.
extension Array: Decodable where Element: Decodable {
/// Creates a new array by decoding from the given decoder.
///
/// This initializer throws an error if reading from the decoder fails, or
/// if the data read is corrupted or otherwise invalid.
///
/// - Parameter decoder: The decoder to read data from.
public init(from decoder: Decoder) throws {
self.init()
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
let element = try container.decode(Element.self)
self.append(element)
}
}
}
you can refer the implementation from https://github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift
I think your function call myCls.request(responseType: [User].self) is failed because T is [User], it's not a Mappable.

Decodable DocumentReference in Swift

I have a custom model in swift and it has a property of type DocumentReference
I'm getting some data from a cloud function and then I'm trying to decode it back to my model.
DocumentReference doesn't conform to Decodable by itself so I'm trying to write an extension for it.
I can not make it work since I keep getting this error:
Initializer requirement 'init(from:)' can only be satisfied by a 'required' initializer in the definition of non-final class 'DocumentReference'
Any idea how to make this work?
My extension:
import Firebase
extension DocumentReference: Decodable {
public convenience init(from decoder: Decoder) throws {
//
}
}
Existing model:
struct Notification: Identifiable, CollectionProtocol, DocumentProtocol {
var id = UUID()
var documentReference: DocumentReference
var receiverID: String
var type: String
var createdAt: Date
var requestReference: DocumentReference?
var request: Request?
init(document: DocumentSnapshot) {
self.documentReference = document.reference
self.requestReference = document["requestReference"] as? DocumentReference ?? Firestore.firestore().document("")
self.receiverID = document["receiverID"] as? String ?? ""
self.type = document["type"] as? String ?? ""
self.createdAt = (document["createdAt"] as? Timestamp)?.dateValue() ?? Date()
}
}
NS_SWIFT_NAME(DocumentReference)
#interface FIRDocumentReference : NSObject
/** :nodoc: */
- (instancetype)init
__attribute__((unavailable("FIRDocumentReference cannot be created directly.")));
DocumentReference (FIRDocumentReference) cannot be instantiated directly; it has no available initializers. To implement the required init, you'd have to do that in the declaration of the class itself.
If you want to conveniently instantiate custom objects from Firestore data, consider a failable initializer that takes a [String: Any] argument. If you just want to encode/decode the reference itself, consider encoding/decoding the String value of the location itself (the collection and document names) and then using that to reconstruct the DocumentReference.
The class DocumentReference is not final which means that it could possibly be sub-classed but without the sub class conforming to Codable. So this would leave to an unknown state if init(from decoder: Decoder) in your extension was called.
One workaround could be to create a wrapper struct with its own init(from decoder: Decoder) and then do your decoding of DocumentReference there.
Example:
struct Wrapped: Decodable {
let docRef: DocumentReference
public init(from decoder: Decoder) throws {
let container = try decoder....
// add the decoding code here for DocumentReference
}
}

JSON Codable - What to do with [Any]?

I have a JSON with the following structure :
         "properties":[
            {
               "name":"Quality",
               "values":[],
               "displayMode":0,
               "type":6
            },
            {
               "name":"Armour",
               "values":[],
               "displayMode":0,
               "type":16
            },
            {
               "name":"Evasion Rating",
               "values":[],
               "displayMode":0,
               "type":17
            }
         ]
The API is always returning an array for "value" with first element being a String, and second element being an Int.
"values":[
                  [
                     "+26%",
                     1
                  ]
               ],
Here’s how I mapped the JSON so far :
struct Properties: Codable {
var name: String
var values: [Any]
var displayMode: Int
var type: Int
}
At which point Xcode complains because Type 'Properties' does not conform to protocol 'Decodable'
So, I know that Any doesn't conform to codable but the thing is I don't how to convert this [Any] into something Swift can work with...
Can someone share a hint to the solution ?
Thank you very much :)
You need to use an unkeyed container for this when decoding in your custom init(from:).
If you know there is always one string followed by one integer you can define the struct for the values like this
struct Value: Decodable {
let string: String?
let integer: Int?
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
string = try container.decodeIfPresent(String.self)
integer = try container.decodeIfPresent(Int.self)
}
}
If there could be more elements you can use a loop instead
struct Value: Decodable {
let strings: [String]
let integers: [Int]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var strings = [String]()
var integers = [Int]()
while !container.isAtEnd {
do {
let string = try container.decode(String.self)
strings.append(string)
} catch {
let integer = try container.decode(Int.self)
integers.append(integer)
}
}
self.integers = integers
self.strings = strings
}
}

Swift 4 JSONEncoder returns empty JSON array

I have multiple Codeable classes that I want to convert to JSON strings.
class MyCodable: NSObject, Codable {
override init() {
}
func encode(to encoder: Encoder) throws {
}
}
class Category: MyCodable {
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self);
//... assign values and call super.init
}
override func encode(to encoder: Encoder) throws {
}
var categoyId: Int64;
var categoryName: String;
enum CodingKeys: String, CodingKey {
case categoryId
case categoryName
}
}
class Product: MyCodable {
required init(from decoder: Decoder) throws {
//..assign values and call super.init
}
override func encode(to encoder: Encoder) throws {
}
var productId: Int64;
var categoryId: Int64;
var productName: String;
enum CodingKeys: String, CodingKey {
case productId
case categoryId
case productName
}
}
I have a utility class that I am using for the conversion.
class JSONUtil: NSObject {
public static func encode(objects: [MyCodable]) -> String {
var json: String = "";
do {
let encoder = JSONEncoder();
encoder.outputFormatting = .prettyPrinted;
let jsonData = try encoder.encode(objects);
json = String(data: jsonData, encoding: String.Encoding.utf8)!
} catch let convertError { json = "[{error: '" + convertError.localizedDescription + "'}]"; }
return json;
}
public static func toJson(object: MyCodable) -> String {
var objects = [MyCodable]();
objects.append(object);
return encode(objects: objects);
}
}
The return value is always "[{}]" no matter what is inside the array of MyCodable objects. While debugging, I can see the values of the Category object are populated.
Since MyCodable has no properties, is that why the JSONEncoder does not print out the Category properties?.. Or why doesn't it print the Category properties?
I am having the same issue with the Product object.
The goal is to avoid having to customize the JSON conversion for every class I want to print to JSON.
Your reasoning is correct, MyCodable has no properties, so there is nothing to read/write. You have actually stumbled across a bug of the swift JSON Encoder and Decoder. They do not handle inheritance well. So your Product and Category class become anonymous MyCodable class that have nothing to be encoded or decoded.
Looking at your code, I see no reason for the MyCodable class. Simply have Product and Category adhere to the Codable protocol. If you need the polymorphism of being able to refer to Product and Category in the same way, define a protoocl that adheres to Codable and have Product and Category adhere to the protocol you defined. In my eyes this is redundant however there could be use cases where it is a plausible solution.
Your mistake is that you're trying to use inheritance instead of generics. Your JSONUtil class should be look like this.
class JSONUtil: NSObject {
public static func encode<T: Encodable>(objects: [T]) -> String {
var json: String = "";
do {
let encoder = JSONEncoder();
encoder.outputFormatting = .prettyPrinted;
let jsonData = try encoder.encode(objects);
json = String(data: jsonData, encoding: String.Encoding.utf8)!
} catch let convertError { json = "[{error: '" + convertError.localizedDescription + "'}]"; }
return json;
}
public static func toJson<T: Encodable>(object: T) -> String {
return encode(objects: [object]);
}
}
I'm not sure why you're creating classes inheriting from NSObject. You may be stuck in the old Objective-C ways 😜 Prefer to use struct over class, unless you always need the object to be copied by reference.

Resources