41

For example:

class Test {
    var name: String;
    var age: Int;
    var height: Double;
    func convertToDict() -> [String: AnyObject] { ..... }
}

let test = Test();
test.name = "Alex";
test.age = 30;
test.height = 170;

let dict = test.convertToDict();

dict will have content:

{"name": "Alex", "age": 30, height: 170}

Is this possible in Swift?

And can I access a class like a dictionary, for example probably using:

test.value(forKey: "name");

Or something like that?

Cristik
  • 30,989
  • 25
  • 91
  • 127
Chen Li Yong
  • 5,459
  • 8
  • 58
  • 124
  • 3
    Why do you want to? You can use key paths directly on the class/struct. So what's the point here? In what way would a dictionary be "better" than a class/struct? "And can I access a class like a dictionary" Yes, exactly, using key paths, in Swift 4. – matt Oct 06 '17 at 03:11
  • 1
    Possible duplicate of [How can I use Swift’s Codable to encode into a dictionary?](https://stackoverflow.com/questions/45209743/how-can-i-use-swift-s-codable-to-encode-into-a-dictionary) –  Oct 06 '17 at 03:16
  • You want to do conversion for what purpose? If you are going to save your data, you should create a model with a class. Converting a struct into Data will be more troubling. – El Tomato Oct 06 '17 at 03:24
  • I want to do the conversion for JSON encoding and passing values between ViewController purpose. From the accepted answer, I believe the `Encodable` is the key. – Chen Li Yong Oct 06 '17 at 08:47

5 Answers5

92

You can just add a computed property to your struct to return a Dictionary with your values. Note that Swift native dictionary type doesn't have any method called value(forKey:). You would need to cast your Dictionary to NSDictionary:

struct Test {
    let name: String
    let age: Int
    let height: Double
    var dictionary: [String: Any] {
        return ["name": name,
                "age": age,
                "height": height]
    }
    var nsDictionary: NSDictionary {
        return dictionary as NSDictionary
    }
}

You can also extend Encodable protocol as suggested at the linked answer posted by @ColGraff to make it universal to all Encodable structs:

struct JSON {
    static let encoder = JSONEncoder()
}
extension Encodable {
    subscript(key: String) -> Any? {
        return dictionary[key]
    }
    var dictionary: [String: Any] {
        return (try? JSONSerialization.jsonObject(with: JSON.encoder.encode(self))) as? [String: Any] ?? [:]
    }
}

struct Test: Codable {
    let name: String
    let age: Int
    let height: Double
}

let test = Test(name: "Alex", age: 30, height: 170)
test["name"]    // Alex
test["age"]     // 30
test["height"]  // 170
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Actually, I use the example `value(forKey` just to illustrate what I need, which is querying an object to get the value of its properties by using a variable keys instead of hard-coded one. I don't plan to use `NSDictionary`. I plan to use this in conjunction with JSON, so I believe your `Encodable` protocol works best for my problem. I don't know about the keyword to search about this type of problem, but now I do. I know I can create a personalized function to return custom dictionary for each class, I just wondering if there's a universal method to do this. Thanks!! – Chen Li Yong Oct 06 '17 at 08:47
  • 4
    The Encodable extension approach is elegant! Thank you Leo. – Murat Yasar Sep 29 '18 at 14:50
  • Thanks This work for me extension Encodable { var dictionaryObj: [String: Any] { return (try? JSONSerialization.jsonObject(with: JSON.encoder.encode(self))) as? [String: Any] ?? [:] } } – Vicky Mar 02 '23 at 07:25
44

You could use Reflection and Mirror like this to make it more dynamic and ensure you do not forget a property.

struct Person {
  var name:String
  var position:Int
  var good : Bool
  var car : String

  var asDictionary : [String:Any] {
    let mirror = Mirror(reflecting: self)
    let dict = Dictionary(uniqueKeysWithValues: mirror.children.lazy.map({ (label:String?, value:Any) -> (String, Any)? in
      guard let label = label else { return nil }
      return (label, value)
    }).compactMap { $0 })
    return dict
  }
}


let p1 = Person(name: "Ryan", position: 2, good : true, car:"Ford")
print(p1.asDictionary)

["name": "Ryan", "position": 2, "good": true, "car": "Ford"]

Ryan Heitner
  • 13,119
  • 6
  • 77
  • 119
9

A bit late to the party, but I think this is great opportunity for JSONEncoder and JSONSerialization. The accepted answer does touch on this, this solution saves us calling JSONSerialization every time we access a key, but same idea!

extension Encodable {

    /// Encode into JSON and return `Data`
    func jsonData() throws -> Data {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        encoder.dateEncodingStrategy = .iso8601
        return try encoder.encode(self)
    }
}

You can then use JSONSerialization to create a Dictionary if the Encodable should be represented as an object in JSON (e.g. Swift Array would be a JSON array)

Here's an example:

struct Car: Encodable {
    var name: String
    var numberOfDoors: Int
    var cost: Double
    var isCompanyCar: Bool
    var datePurchased: Date
    var ownerName: String? // Optional
}

let car = Car(
    name: "Mazda 2",
    numberOfDoors: 5,
    cost: 1234.56,
    isCompanyCar: true,
    datePurchased: Date(),
    ownerName: nil
)

let jsonData = try car.jsonData()

// To get dictionary from `Data`
let json = try JSONSerialization.jsonObject(with: jsonData, options: [])
guard let dictionary = json as? [String : Any] else {
    return
}

// Use dictionary

guard let jsonString = String(data: jsonData, encoding: .utf8) else {
    return
}

// Print jsonString
print(jsonString)

Output:

{
  "numberOfDoors" : 5,
  "datePurchased" : "2020-03-04T16:04:13Z",
  "name" : "Mazda 2",
  "cost" : 1234.5599999999999,
  "isCompanyCar" : true
}
Bechsh
  • 353
  • 3
  • 7
3

Use protocol, it is an elegant solution.
1. encode struct or class to data
2. decode data and transfer to dictionary.

/// define protocol convert Struct or Class to Dictionary
protocol Convertable: Codable {

}

extension Convertable {

    /// implement convert Struct or Class to Dictionary
    func convertToDict() -> Dictionary<String, Any>? {

        var dict: Dictionary<String, Any>? = nil

        do {
            print("init student")
            let encoder = JSONEncoder()

            let data = try encoder.encode(self)
            print("struct convert to data")

            dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? Dictionary<String, Any>

        } catch {
            print(error)
        }

        return dict
    }
}

struct Student: Convertable {

    var name: String
    var age: Int
    var classRoom: String

    init(_ name: String, age: Int, classRoom: String) {
        self.name = name
        self.age = age
        self.classRoom = classRoom
    }
}

let student = Student("zgpeace", age: 18, classRoom: "class one")

print(student.convertToDict() ?? "nil")

ref: https://a1049145827.github.io/2018/03/02/Swift-%E4%BB%8E%E9%9B%B6%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAStruct%E6%88%96Class%E8%BD%ACDictionary%E7%9A%84%E9%9C%80%E6%B1%82/

Zgpeace
  • 3,927
  • 33
  • 31
2

This answer is like the above which uses Mirror. But consider the nested class/struct case.

extension Encodable {
    func dictionary() -> [String:Any] {
        var dict = [String:Any]()
        let mirror = Mirror(reflecting: self)
        for child in mirror.children {
            guard let key = child.label else { continue }
            let childMirror = Mirror(reflecting: child.value)
            
            switch childMirror.displayStyle {
            case .struct, .class:
                let childDict = (child.value as! Encodable).dictionary()
                dict[key] = childDict
            case .collection:
                let childArray = (child.value as! [Encodable]).map({ $0.dictionary() })
                dict[key] = childArray
            case .set:
                let childArray = (child.value as! Set<AnyHashable>).map({ ($0 as! Encodable).dictionary() })
                dict[key] = childArray
            default:
                dict[key] = child.value
            }
        }
        
        return dict
    }
}