Swift - Struct or Dictionary

Swift - Struct or Dictionary

Just to clarify one point when you are worried about memory resources by using struct. A structure is value copied passed type. It's not stored in the memory heap unlike classes (Reference types).

So go and use struct, it's recommended by Apple itself.

Can Swift convert a class / struct data into dictionary?

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

Should a dictionary be converted to a class or struct in Swift?

To access it like this you need to convert your dictionary to Struct as follow:

edit/update: Swift 3.x

struct Job: CustomStringConvertible {
let number: Int
let name, client: String
init(dictionary: [String: Any]) {
self.number = dictionary["jobNumber"] as? Int ?? 0
self.name = dictionary["jobName"] as? String ?? ""
self.client = dictionary["client"] as? String ?? ""
}
var description: String {
return "Job#: " + String(number) + " - name: " + name + " - client: " + client
}
}

let dict: [String: Any] = ["jobNumber": 1234,
"jobName" : "Awards Ceremony",
"client" : "ACME Productions"]

let job = Job(dictionary: dict)
print(job.number) // 1234
print(job.name) // "Awards Ceremony"
print(job.client) // "ACME Productions"
print(job) // "Job#: 1234 - name: Awards Ceremony - client: ACME Productions"""

edit/update:

Swift 4 or later you can use JSON Codable protocol:

struct Job {
let number: Int
let name, client: String
}
extension Job: Codable {
init(dictionary: [String: Any]) throws {
self = try JSONDecoder().decode(Job.self, from: JSONSerialization.data(withJSONObject: dictionary))
}
private enum CodingKeys: String, CodingKey {
case number = "jobNumber", name = "jobName", client
}
}
extension Job: CustomStringConvertible {
var description: String {
return "Job#: " + String(number) + " - name: " + name + " - client: " + client
}
}

let dict: [String: Any] = ["jobNumber": 1234,
"jobName" : "Awards Ceremony",
"client" : "ACME Productions"]
do {
let job = try Job(dictionary: dict)
print(job.number) // 1234
print(job.name) // "Awards Ceremony"
print(job.client) // "ACME Productions"
print(job) // "Job#: 1234 - name: Awards Ceremony - client: ACME Productions\n"
} catch {
print(error)
}

Swift make init Dictionary in struct

Here are two possible solutions. First one is to map from the dictionary elements into the struct. For this we need a failable initializer since we are using compactMap

Note that I took the liberty to change the naming somewhat of the custom type

struct Tool: Codable {
var name: String
var quantity : Int

init?(name: String?, quantity: Int?) {
guard let name = name, let quantity = quantity else {
return nil
}
self.name = name
self.quantity = quantity
}

//...
}

Then it becomes simple to convert toolListDict

let tools = toolListDict.compactMap { 
Tool(name: $0["Name"] as? String, quantity: $0["qty"] as? Int)
}

Another option is to first encode the dictionary using JSONSerialization and then decode the result using JSONDecoder, for this we need to add a CodingKey enum to the custom type

enum CodingKeys: String, CodingKey {
case name = "Name"
case quantity = "qty"
}

and the encoding/decoding is done like this

do {
let data = try JSONSerialization.data(withJSONObject: toolListDict)
let tools = try JSONDecoder().decode([Tool].self, from: data)
//...
} catch {
print(error)
}

Dictionary of structs in a struct

You have a few things going on here. For starters, you are initialising an empty Dict not an empty Array of dictionaries. Your property within your Thingy is a let which is a constant. Like mentioned earlier adding to things needs to be in a function inside a Struct and if you are changing that property you are mutating it.

struct Thing{
let title: String
}

struct Thingy {

var things = [[String:Thing]]()

mutating func createSomeThings() {
let thingo = ["thing1" : Thing(title:"stuff")]
things.append(thingo)
}

mutating func addAnotherThingWith(name: String, title: String) {
let thingo = [name : Thing(title:title)]
things.append(thingo)
}

}

There are to ways you can achieve your goal.

Swift converting Decodable struct to Dictionary

You don't need it to be Decodable. What you need is to be able to encode it (Encodable). So start by declaring your structure as Codable. After encoding it you can convert your data into a dictionary using JSONSerialization jsonObject method:

extension Encodable {
func data(using encoder: JSONEncoder = .init()) throws -> Data { try encoder.encode(self) }
func string(using encoder: JSONEncoder = .init()) throws -> String { try data(using: encoder).string! }
func dictionary(using encoder: JSONEncoder = .init(), options: JSONSerialization.ReadingOptions = []) throws -> [String: Any] {
try JSONSerialization.jsonObject(with: try data(using: encoder), options: options) as? [String: Any] ?? [:]
}
}


extension Data {
func decodedObject<D: Decodable>(using decoder: JSONDecoder = .init()) throws -> D {
try decoder.decode(D.self, from: self)
}
}


extension Sequence where Element == UInt8 {
var string: String? { String(bytes: self, encoding: .utf8) }
}

I would also declare the srtuct properties as constants. If you need to change any value just create a new object:

struct DiscussionMessage: Codable {
let message, userCountryCode, userCountryEmoji, userName, userEmailAddress: String
let messageTimestamp: Double
let fcmToken, question, recordingUrl: String?
}


let message: DiscussionMessage = .init(message: "message", userCountryCode: "BRA", userCountryEmoji: "quot;, userName: "userName", userEmailAddress: "email@address.com", messageTimestamp: 1610557474.227274, fcmToken: "fcmToken", question: "question", recordingUrl: nil)

do {
let string = try message.string()
print(string) // {"fcmToken":"fcmToken","userName":"userName","message":"message","userCountryEmoji":"quot;,"userEmailAddress":"email@address.com","question":"question","messageTimestamp":1610557474.2272739,"userCountryCode":"BRA"}

let dictionary = try message.dictionary()
print(dictionary) // ["userName": userName, "userEmailAddress": email@address.com, "userCountryEmoji": , "messageTimestamp": 1610557474.227274, "question": question, "message": message, "fcmToken": fcmToken, "userCountryCode": BRA]

let data = try message.data() // 218 bytes
let decodedMessages: DiscussionMessage = try data.decodedObject()
print("decodedMessages", decodedMessages) // ecodedMessages DiscussionMessage(message: "message", userCountryCode: "BRA", userCountryEmoji: "quot;, userName: "userName", userEmailAddress: "email@address.com", messageTimestamp: 1610557474.227274, fcmToken: Optional("fcmToken"), question: Optional("question"), recordingUrl: nil)
} catch {
print(error)
}

Swift Dictionary using subset of Array of Struct

The easiest way to do this is to use reduce(into:) so you can map and group in one step

let suggestionsDict = eventRecords.reduce(into: [:]) {
$0[$1.evType, default: []].append($1.evMainTxt)
}

Determing dictionary with nested dictionaries type in Swift

Much much better. Never go back ;) Note that:

  • First letter of structs should be capitalised. ( Linter will shoot if you don't. )
  • You can sometimes embed structs, to enforce scoping. It's up to you and depends of the situation.
  • You don't have to use Array<> and Dictionary<> notations. Prefer brackets.
  • Define a 'string' datatype is not really advised, since it already defines a primitive type. Note that you can use `String` between quotes to redefine primitive types in your scope, but it is a bad practice.
struct Data: Hashable {
struct Text: Hashable {
let translation: String
let pinned: Bool
let order: Int
}

struct Language: Hashable {
let language: String
let target: Bool
let texts: [String: Text]
}

let base: String
let target: String
let languages: [Language]
}

If you need to use Text type outside:

var text: Data.Text

And finally, It's a bit curious to define an array of translations in a Language type. Maybe you should name it Translations and structure a bit differently. for example have a Text struct that contains a Translations dict, with language code as the key. Just a suggestion.. Good luck :)

How to decode a dictionary into struct with key as a value in Swift

Some more thought reminded me I don't need AnyCodingKey in this case because this is such a simple structure. Decode as a [String: String], and then make the mapping:

struct PersonList: Decodable {
let persons: [Person]

init(from decoder: Decoder) throws {
self.persons = try decoder.singleValueContainer()
.decode([String: String].self)
.map(Person.init)
}
}

let list = try JSONDecoder().decode(PersonList.self, from: json).persons

// [Person(name: "Bob", city: "London"), Person(name: "Alice", city: "Berlin")]

Old answer, because sometimes this technique is handy.

The key tool for this is the oft-needed CodingKey (that really should be in stdlib), AnyCodingKey:

struct AnyCodingKey: CodingKey {
var stringValue: String = ""
init?(stringValue: String) { self.stringValue = stringValue }
var intValue: Int?
init?(intValue: Int) { self.intValue = intValue }
}

With that, you just need a container to hang the decoder on. Call it PersonList:

struct PersonList: Decodable {
let persons: [Person]

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
self.persons = try container.allKeys.map { key in
Person(name: key.stringValue,
city: try container.decode(String.self, forKey: key))
}
}
}

let list = try JSONDecoder().decode(PersonList.self, from: json).persons

// [Person(name: "Bob", city: "London"), Person(name: "Alice", city: "Berlin")]

This just maps each key/value to a Person.

Keep in mind that JSON objects are not ordered, so the resulting array may be in a different order than the JSON, as they are in this example. This is not fixable with Foundation (JSONSerialization or JSONDecoder); you'd have to use a different JSON parser if you needed that.



Related Topics



Leave a reply



Submit