Swift Dictionary order does not match JSON file

I'm working on parsing a JSON file into a dictionary:


  // JSON Payload File
  {
  "orderPlaced": "done",
  "prep": "pending",
  "shipped" : "pending"
  }


Here is my code to make a dictionary from that file:


      
if let path = Bundle.main.path(forResource: "JSONPayload", ofType: "json") {
         
         do {

         // Data from JSON file path
         let data = try Data(contentsOf: URL(fileURLWithPath: path), options: [])
        
         // JSON dicrtionary being made
         let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
           
            print(json)
         
            
         } catch let jsonErr {
            
            print("err", jsonErr)
            
         }


Expected output: ["orderPlaced": done, "prep": pending, "shipped": pending]

The actual output is: ["shipped": pending, "orderPlaced": done, "prep": pending]


Is there a way I can re-order my dictionary or have it ordered from the start. I'm assuming the latter request might not be doable since dictionaires from my understanding are un-ordered. Maybe the only thing I can do is re-order after the dictionary is made, but how would i do that? Do I have to modify my JSON file and the Swift code or just one or ther other?

Replies

Is there a particular reason you want the keys to be sorted? Like you said, dictionaries are not supposed to be sorted by key.


However, if you still want the dictionary to be sorted, get all the keys as an array and sort the array. Example:


let dictionary = ["c": "one", "b": "two", "a": "three"]
let keys = dictionary.keys.sorted()
for key in keys {
    print("\(key) == \(dictionary[key] ?? "")")
}


Output:


a == three
b == two
c == one

I would like to have the Swift dictionary be in order of the JSON file... additionally, my reasoning behind the ordering is as follows:


1. orderedPlaced, prep, and shipped are strings that represent image files in the iOS App. These images files are used to construct a FedEx style progress indicator.


2. The order of images is important since that is the order they need to be displayed in the progress tracker UI. Here is what I am am trying to do (I'm new to JSON parsing... so maybe there's a cleaner, effecuent solution, but this is what i have so far:



// All the possible images that a progress tracker UI can show depending upon the response from
// JSON file from the internet

var orderPlaced =  imageLiteral(resourceName: "OrderPlaced")
var prep =  imageLiteral(resourceName: "Prep")
var check =  imageLiteral(resourceName: "Check")
var shipped =  imageLiteral(resourceName: "Ship")
var attention =  imageLiteral(resourceName: "Attention")
var emptyImage =  imageLiteral(resourceName: "Placeholder")



// Images are placed in an image array
var images = [orderPlaced, plan, check, rocket, attention]


// Each image has a corresponding string name
var names = ["orderPlaced", "plan", "check", "rocket", "attention"]

// Images to String Dictionary
var imagesString: [UIImage: String] = [:]


if let path = Bundle.main.path(forResource: "JSONPayload2", ofType: "json") {
         
         do {
         let data = try Data(contentsOf: URL(fileURLWithPath: path), options: [])
         let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
            
            // for-in loop
            // for an image name and status inside the JSON dictionary...
            for (imageName, status) in json {
               
               // for an image and its correpsonding name
               for (image, name) in imagesString {
                  
                    // if imagename == name
                    // i.e only the images that the JSON file passed, based on the status
                    // provided, tint the image a specific color. My custom tint(image:forGivenResponse) method
                    // returns the tinted image (red, green, gray, etc)
                    // Then images are stored in an array for display to the progress tracker
                  if imageName == name {
                     
                     let tintedImage = tint(image: image, forGivenResponse: status as! String)

                    // My custom progress tracker element properties
                     progressTracker.progressElementImages.append(tintedImage)
                     progressTracker.numberOfProgressElements = progressTracker.progressElementImages.count
                     
                  }
                  
               }
               
               
               
            }
          
         
            print(json)
         
            
         } catch let jsonErr {
            
            print("err", jsonErr)
            
         }
         
         
         
      }

Order of JSON object is not defined in JSON specification. Which means you should not use JSON object if the order is important.


Use JSON array instead of JSON object.

For example:

// JSON Payload File
[
    {"name": "orderPlaced", "status": "done"},
    {"name": "prep", "status": "pending"},
    {"name": "shipped", "status": "pending"}
]


Or else, give up using JSONSerialization or JSONDecoder and write all things by yourself. To implement something like that you may need to implement an OrderedDictionary. In my experience, you can achieve it with just a thousand lines of code.


I strongly recommend to go with the first way.


(By the way your code is far from efficient and I do not think UIImage is not suitable for Key type of a Dictionary. Why don't you use `var stringImages: [String: UIImage]` instead of `imagesString`?)

  • Another issue I have for maintaining order is for debug ease. In particular, comparing of log files during debug, of slightly different runs (e.g. suppose I change X, how does it handle that case.) If the generation of log files has indeterminate order (e.g. in printing options, or eval execution ordering), the log files can get way out of sync. A flag in the debug config which says "use invariant order" would float my boat. I just haven't found that yet.

Add a Comment

Okay... I like the idea of using a JSON Array and thanks for the guidance on not using UIImage as a key type for a dictionary.. going to fix that as well. So on the Swift side... would the structure be an array instead of a dictionary. If so, how would the mechanism look for coloring only the images that are in the JSON array and then presenting those in the UI?

It would not be far from your code (assuming you use my For Example):

        // String to Images Dictionary
        let stringImages: [String: UIImage] = [
            "orderPlaced": orderPlaced,
            "plan": plan,
            "check": check,
            "rocket": rocket,
            "attention": attention
        ]
        
        
        if let url = Bundle.main.url(forResource: "JSONPayload2", withExtension: "json") {
            do {
                let data = try Data(contentsOf: url)
                let json = try JSONSerialization.jsonObject(with: data) as! [[String: Any]]
                
                // for-in loop
                // for an image name and status inside the JSON array...
                for imageNameAndStatus in json {
                    let imageName = imageNameAndStatus["name"] as! String
                    let status = imageNameAndStatus["status"] as! String
                    
                    // for an image with correpsonding name
                    if let image = stringImages[imageName] {
                        let tintedImage = tint(image: image, forGivenResponse: status)
                        
                        // My custom progress tracker element properties
                        progressTracker.progressElementImages.append(tintedImage)
                        progressTracker.numberOfProgressElements = progressTracker.progressElementImages.count
                    }
                }
                print(json)
            } catch let jsonErr {
                print("err", jsonErr)
            }
        }


There's many `as!` in the code above, as you say the JSON is placed in a file. If you go on some sort of API outputs, you should better make it safer.

OOPer. Thanks that did the trick! 🙂. My question is I'm somewhat confused on this block.... particularly line 1 here:

From what I understand it that we have a variable called image and we are assigning it the string value from the stringImage dictionary...


1. How can we pass "image" as a UIImage into the tint(image:forGivenResponse) method?

2. I'm kinda shaky on this if let concept still 😕.. could you explain that in this specific context?


if let image = stringImages[imageName] {
                  
                  let tintedImage = tint(image: image, forGivenResponse: status)
                  progressTracker.progressElementImages.append(tintedImage)
                  progressTracker.numberOfProgressElements = progressTracker.progressElementImages.count
                  
               }


Thanks!

Oh wait... so does the following mean:

if you can let the new variable "image" equal the value for the key "imageName"?

if let image = stringImages[imageName] {

look for details for if let in Swift handbook.


But, the concept is simple.


Before unwrapping an optional, you need to test it is not nil.

So you could write

var image: UIImage
if stringImages[imageName] != nil {
  image = stringImages[imageName]!
}

2 problems: it is long. And you need a var


if let allows to do it in a line

if let image = stringImages[imageName] {


It tests if nil ; if not, it unwraps and assigns to image


Note that image is then defined only inside the scope of the if statement.

If you need to use outsode, use guard let instead:


func forTest() {
     guard let image = stringImages[imageName] else { return }
      let newImage = image
}

if you can let the new variable "image" equal the value for the key "imageName"?

Not actually.

You should not try to find any meanings of `=`, in if-let syntax. It's not a simple assignment nor equal testing. It's just a separator.

if {let/var variable-declaration} = {tested-value}

This means if {tested-value} is not nil, declare the local variable {let/var variable-declaration} with the non-nil value of {tested-value} and execute the true-block.


In other words, it's equivalent to:

if {tested-value} != nil {
   {let/var variable-declaration} = {tested-value}!
    ...
}


With my code below:

if let image = stringImages[imageName] {
    let tintedImage = tint(image: image, forGivenResponse: status)
    progressTracker.progressElementImages.append(tintedImage)
    progressTracker.numberOfProgressElements = progressTracker.progressElementImages.count
}

It is equivalent to:

if stringImages[imageName] != nil {
    let image = stringImages[imageName]!
    let tintedImage = tint(image: image, forGivenResponse: status)
    progressTracker.progressElementImages.append(tintedImage)
    progressTracker.numberOfProgressElements = progressTracker.progressElementImages.count
}

You know Swift Dictionaries may return nil, when the given key cannot be found. (For example, if the `imageName` == "missing", the value of `stringImages[imageName]` == nil.) And you also know postfix `!` is called as a "crash me" operator as it often causes fatal error: Unexpectedly found nil.


The if-let syntax unwraps the Optional value only when it is not nil and introduces a new local variable of non-Optional. It's one of some famous ways you should know when working with Optionals in Swift.

Thanks OOPer and Claude31 for the explanations... Going to look into if-let even more.