0

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
    ]
  }
}
Mark Kang
  • 161
  • 11
  • It might help to make the `struct` Codable, and encode it directly to `Data`. – Cristik Sep 25 '18 at 18:37
  • Related: https://stackoverflow.com/questions/46630244/difference-between-any-and-any – rmaddy Sep 25 '18 at 18:43
  • You can declare your dictionary type as Any and add optionals and non optionals anyway. You just need to explicitly coerce your `Double?` as `Any` -> `var dictionary: [String: Any] { return ["name": name,"frequency": frequency as Any] }` – Leo Dabus Sep 25 '18 at 18:52
  • Note that there is no need to declare it as a method when you don't need to pass any parameter to it, just declare it as a computed property. `init(name: String, frequency: Double?) { self.name = name self.frequency = frequency }`it is redundant when declaring a struct – Leo Dabus Sep 25 '18 at 18:58
  • `struct Record { let name: String let frequency: Double? var dictionary: [String: Any] { return ["name": name,"frequency": frequency as Any] } }` – Leo Dabus Sep 25 '18 at 19:00
  • possible duplicate of https://stackoverflow.com/a/46597941/2303865 – Leo Dabus Sep 25 '18 at 19:04

2 Answers2

1

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")
ielyamani
  • 17,807
  • 10
  • 55
  • 90
  • Why not simply `self = record` ? Btw you should make your initialiser non fallible and make it propagate the error making it throws `init(_ data: Data) throws { self = try JSONDecoder().decode(Record.self, from: data) }` – Leo Dabus Sep 25 '18 at 20:10
  • Why are you adding “from” it is pointless, the only case that would make sense would be in a method like `stride(from:to:)`. Just use IMO and to make it consistent with apple guidelines `init(data: Data)` – Leo Dabus Sep 25 '18 at 20:23
  • Sure, and you're more than welcome to edit the answer. Your valuable comments are very helpful to the community – ielyamani Sep 25 '18 at 20:27
0

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.

Cristik
  • 30,989
  • 25
  • 91
  • 127