Mark Kang
Mark Kang

Reputation: 191

Swift dictionary with mix types (optional and non-optional)

I have a struct that has a method to return a dictionary representation. The member variables were a combination of different types (String and Double?)

With the following code example, there would be a warning from Xcode (Expression implicitly coerced from 'Double?' to Any)

struct Record {
  let name: String
  let frequency: Double?

  init(name: String, frequency: Double?) {
    self.name = name
    self.frequency = frequency
  }

  func toDictionary() -> [String: Any] {
    return [
      "name": name,
      "frequency": frequency
    ]
  }
}

However if it was returning a type [String: Any?], the warning goes away:

struct Record {
  let name: String
  let frequency: Double?

  init(name: String, frequency: Double?) {
    self.name = name
    self.frequency = frequency
  }

  func toDictionary() -> [String: Any?] {
    return [
      "name": name,
      "frequency": frequency
    ]
  }
}

My question is: Is this correct? And if it is, can you point me to some Swift documentation that explains this?

If it isn't, what should it be?

== EDIT ==

The following works too:

struct Record {
  let name: String
  let frequency: Double?

  init(name: String, frequency: Double?) {
    self.name = name
    self.frequency = frequency
  }

  func toDictionary() -> [String: Any] {
    return [
      "name": name,
      "frequency": frequency as Any
    ]
  }
}

Upvotes: 0

Views: 281

Answers (2)

ielyamani
ielyamani

Reputation: 18591

You can cast frequency to Any since the latter can hold any type. It is like casting instances of specific Swift type to the Objective-C id type. Eventually, you'll have to downcast objects of the type Any to a specific class to be able to call methods and access properties.

I would not recommend structuring data in your code using Any, or if you want to be specific Any? (when the object may or may not hold some value). That would be a sign of bad data-modeling.

From the documentation:

Any can represent an instance of any type at all, including function types.[...] Use Any and AnyObject only when you explicitly need the behavior and capabilities they provide. It is always better to be specific about the types you expect to work within your code.

(emphasis is mine)

Instead, use the Data type. And you would be able to decode Record or encode it from and into Data:

struct Record : Codable {
    let name: String
    let frequency: Double?

    init(name: String, frequency: Double?) {
        self.name = name
        self.frequency = frequency
    }

    init(data: Data) throws { 
        self = try JSONDecoder().decode(Record.self, from: data) 
    }

    func toData() -> Data {
        guard let data = try? JSONEncoder().encode(self) else {
            fatalError("Could not encode Record into Data")
        }
        return data
    }
}

And use it like so:

let record = Record(name: "Hello", frequency: 13.0)
let data = record.toData()

let decodedRecord = try Record(data: data)
print(decodedRecord.name)
print(decodedRecord.frequency ?? "No frequency")

Upvotes: 1

Cristik
Cristik

Reputation: 32813

I'd recommend adding Codable conformance, and letting JSONEncoder do all the heavy lifting. If however you are constrained to the toDictionary approach, then I would advise against [String:Any?], since that might result in undefined behaviour (try to print the dictionary for more details).

A possible solution for toDictionary is to use an array of tuples that gets converted to a dictionary:

func toDictionary() -> [String: Any] {
    let propsMap: [(String, Any?)] = [
        ("name", name),
        ("frequency", frequency)
    ]
    return propsMap.reduce(into: [String:Any]()) { $0[$1.0] = $1.1 }
}

This way the nil properties simply don't receive entries in the output dictionary.

Upvotes: 0

Related Questions