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
Swift, Error Exc_Breakpoint (Code=1, Subcode=0X100695474)
How to Open Your App's Settings (Inside the Settings App) with Swift (iOS 11)
Why Use an Extra Let Statement Here
How to Switch to Swift 4.0 in Xcode 9.3
Codable Enum with Multiple Keys and Associated Values
Unresponsive Uibutton in Subview Added to Uistackview
Counting Coloured Pixels on the Gpu - Theory
How to Set Priority on Constraints in Swift
Background Upload with Share Extension
How to Test Required Init(Coder:)
Is There an Kotlin Equivalent 'With' Function in Swift
Attrackingmanager Stopped Working in iOS 15