13

I am working on a native iOS application that receives data in JSON format from a web-service which we are also in control of. The plan is to change out the backend database in a bout 18 months in favor of a different platform.

With that in mind, we want to be sure that that iOS app is going to be relatively easy to adapt to the new datasource, particularly as we may change the keys used in the associative array received from the server via JSON.

There are two goals:

  1. Create a single location for each PHP request where the keys can be modified if needed. This would avoid digging through code to find things like job["jobNumber"].

  2. Clean up our existing code to also eliminate references like job["jobNumber"].

We are both very new to Swift with no Objective-C experience, but I am was thinking a Struct or Class would be appropriate to create references like job.jobNumber.

Should a dictionary be converted into a class or struct? Sample code representing a reusable method of taking a Dictionary<String, String> as shown below and converting it to the recommended type would be extremely helpful.

Example Dictionary:

job = {
    "jobNumber" : "1234",
    "jobName" : "Awards Ceremony",
    "client" : "ACME Productions"
}

Desired result:

println("job name is \(job.name)")
// prints: job name is Awards Ceremony
Michael Voccola
  • 1,827
  • 6
  • 20
  • 46
  • 3
    If you have a choice between a class and a struct, always choose struct unless you have a reason to choose class. A struct is the Swift object type _par excellence_. – matt Apr 10 '15 at 03:58
  • see [Why Choose Struct Over Class?](https://stackoverflow.com/questions/24232799/why-choose-struct-over-class/38024319#38024319) – mfaani Jun 21 '17 at 19:34

5 Answers5

28

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)
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
3

Definitely a job for a struct.
1. Structs are thread-safe and don't need to be managed by ARC.
2. Some studies have found them to be about 30,000x faster to work with than classes in general.
3. Structs also provide default initializers so your code will be cleaner.
4. In this case, you don't have to worry about inheritance/subclassing.
5. The Protocol Oriented Programming paradigm recommends using structs over classes if you're able.

struct Job {
    let number: Int
    let name: String
    let client: String
}

Initializer for free:

let newJob = Job(number: 2, name: "Honey", client: "Jeff")

Or you can create a custom initializer that takes the dictionary:

struct Job {
    let number: Int
    let name: String
    let client: String

    init(json: [String: Any]) {
        self.number = Int(dictionary["jobNumber"] as? String) ?? 0
        self.name = dictionary["jobName"] as? String ?? ""
        self.client = dictionary["client"] as? String ?? ""
    }
}

usage:

let newJob = Job(json: yourDictionary)
print(newJob.number) 

// outputs: 1234
ScottyBlades
  • 12,189
  • 5
  • 77
  • 85
3

You can add an extension to Dictionary like this to get generic objects:

extension Dictionary where Key == String, Value: Any {

    func object<T: Decodable>() -> T? {
        if let data = try? JSONSerialization.data(withJSONObject: self, options: []) {
            return try? JSONDecoder().decode(T.self, from: data)
        } else {
            return nil
        }
    }
}

and use like this on any [String: Any] dictionaries:

let object: MyDecodableStruct? = dictionary.object()
Behrad Kazemi
  • 302
  • 1
  • 10
1

I generally make use of value classes to achieve what you want to do. In my project I do something like following:

protocol Request : class {
    func toDictionary() -> [String : String]
}

protocol Response : class {
    init(dictionary: [String : String])
}

class MyRequest : Request {
    var var1: Int
    var var2: String

    //Other stuff in class...

    func toDictionary() -> [String : String] {
        //Convert the value to dictionary and return
    }
}

class MyResponse : Response {
    var var1: String
    var var2: Int

    //You could have nested object as members 
    var innerObject: MyInnerResponseClass

    //Other stuff in class...

    var someCalculatedProperty: String {
        return //Calculate property value
    }

    required init(dictionary: [String : String]) {
        //Initialize class from dictionary
    }
}

class MyInnerResponseClass: Response {
    //Class definition here...
}

For objects that could be used as request and response you could implement both of the protocols.

You need to write code for translation once, but then it could be easy to use everywhere. Also by using calculated properties you may get extra ease.

I am not sure if you could just do it out of the box in Swift. I will require reflection which is not yet very well supported by Swift. Also even if there is reflection and you come up with clever way to use to achieve what you need, it could be quite slow if the data is quite large.

Abdullah
  • 7,143
  • 6
  • 25
  • 41
0

My two cents about "logic". )all correct about using structs and so on...)

Do NOT keep (as many from web do..) data in dict or JSON, convert it to struct always.

a lot of efficiently, think about for example about sorting in a tableview..

ingconti
  • 10,876
  • 3
  • 61
  • 48