Learn Courses My Dashboard

JSON Parsing Help

I want to decode the following JSON into a list of Swift structs. However, I’m running into an issue, where I don’t know how to properly decode the items. For each move, many people might have many different items to move. There is no set amount of items. I want to use a boolean to keep track of whether/ not each item was moved, but am not sure how to decode it. How do I properly decode this?

Here’s my JSON:

[
  {
    "id": 0
    "client": "George",
    "moverName": "Fred",
    "items": 
      {
        "redStand": false,
        "blueTable": false 
      }
  },
  {
    id: 1
    "client": "Mary",
    "moverName": "Fred",
    "items": 
      {
        "redStand": false,
        "glassTable": false,
        "espressoMachine": false,
        "grayCouch": false,
        "brownStand": false
      }
  }
]

Here’s my current MoveDetails model:

struct MoveDetails: Identifiable, Decodable {
  var id: Int
  var client: String 
  var moverName: String
  var items: [String: Bool]
}

If You have control over the json being generated then I would suggest turning your items into their own objects with a name and a boolean property

{
    "id": 0
    "client": "George",
    "moverName": "Fred",
    "items":[
        {
            "name": "Red Stand",
            "hasMoved": false
        },
        {
            "name": "Blue Table",
            "hasMoved": false
        }
    ]
}

And Your model would look like this:

var items: [Item]

struct Item {
    var name: String
    var hasMoved: Bool
}

What you have would work perfectly fine. What @Abdul_Wahid proposes would work perfectly fine too. It really depends on how the data will be used once it’s decoded.

Some questions:

  1. Are the items truly freeform or do they only come from a predetermined list?
  2. Does the order of the items matter? (I imagine not or you’d be using an array.)
  3. Is the name of the item as displayed to the user redStand or would it be something like Red Stand?
  4. Do you only list items that haven’t been moved or could there be an item with a true value?
1 Like

Hey Patrick,

Thanks for taking a stab at this.

Ah, well this is slightly embarrassing! The code in my project had the items as a list in key/ value pairs, like this:

"items": 
    [
      "redStand": false,
      "blueTable": false 
    ]

In essence, it wasn’t valid JSON like what I wrote here. I wish Xcode could flag invalid JSON data, before executing it, and running into a problem.

Per your questions:

The items will come from 12 different configurations, which will fill out my View code, where a person can tap whether/ not the item was moved.

Order does not matter.

It would be the second, Red Stand

Only items not moved yet. However, in the end, I would want to keep track of multiple different clients with multiple move jobs. So I plan to load a base JSON configuration into the View code, then save the changed list of items for that move job into Firebase down the road. So some clients could have certain items moved, while others would not.

Hey Abdul,

Thanks for taking a shot at this as well! I considered that structure, but didn’t like it. I didn’t like having a list of dictionaries, but preferred to have a list of key/ value pairs, or in this case a set of keys, where each key name represented the name of the item, whereas the value represented, whether/ not the item was moved.

Thanks,
Andrew

@RedFox1 Based on your answers to my questions, have you considered just using an array for items since it sounds like you will only be listing the items that have not moved? I would even consider using a Set instead of Array in your Swift struct not because of the possibility of duplicates but because lookups are faster in a Set. Although with only a dozen possible values, it probably doesn’t matter all that much.

Otherwise, I think using a [String: Bool] is your best bet. It should be possible to use an enum as the key, like [MovableItem: Bool] (or whatever you call your enum of items) but it might be more trouble than it’s worth to implement.

Hey @roosterboy appreciate all the help with this! Looks like my current setup introduces some weirdness. I was going off of the Learning App’s example, but in my app, it runs into problems…

The end goal with this short demo app is to come up with a way to create Views dynamically based on JSON configurations. So I can have a list of questions, then update those questions via a JSON file without the need to re-publish the app/ get users to update it. The app will download the list of questions, then convert that into Swift objects, and be able to save the responses in Firebase.

Hence, I came up with this little challenge with keeping track of items for a moving company. However, I’m running into an index out of range, when I update one of the client’s jobs, and go back to it:

ForEach<Range<Int>, Int, _ConditionalContent<ModifiedContent<Button<Text>, _EnvironmentKeyWritingModifier<Optional<Color>>>, ModifiedContent<Button<Text>, _EnvironmentKeyWritingModifier<Optional<Color>>>>> count (2) != its initial count (5). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!
Swift/ContiguousArrayBuffer.swift:580: Fatal error: Index out of range

Here’s the link to my GitHub repo:

The View code to display all the clients:

//
//  ContentView.swift
//  MovingApp
//
//  Created by Leone on 3/14/22.
//

import SwiftUI

struct ContentView: View {
    
    @EnvironmentObject var model: MoveViewModel
    
    var body: some View {
        NavigationView {
            ScrollView {
                VStack {
                    // MARK: - Display Systems
                    // Loop through all of the moving jobs
                    ForEach(0..<model.movingJobsList.count) { index in
                        
                        NavigationLink(
                            destination:
                                JobDetail()
                                    .onAppear(perform: {
                                        model.beginJob(index)
                                    }),
                            label: {
                                ZStack {
                                    RoundedRectangle(cornerRadius: 10)
                                    Text(model.movingJobsList[index].client)
                                        .foregroundColor(.white)
                                }
                            })
                        
                        // Create a NavigationLink button to jump to another View from here
                       
                        
                    }
                    
                }
            }
            .navigationBarTitle("Current Jobs")
            .padding()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(MoveViewModel())

    }
}

The code to display the job in detail:

//
//  JobDetail.swift
//  MovingApp
//
//  Created by Leone on 3/14/22.
//

import SwiftUI

struct JobDetail: View {
    @EnvironmentObject var model: MoveViewModel
    
    @State var itemMoved: Bool = false
    
    var body: some View {
        ScrollView {
            LazyVStack {
                if model.currentJob != nil {
                    let keys = model.currentJob!.items.map{$0.key}
                    let values = model.currentJob!.items.map {$0.value}
                    
                    ForEach(keys.indices) { index in
                        if model.currentJob!.items[keys[index]] ?? true {
                            Button("\(keys[index])") {
                                // Change the status of the item moved
                                model.currentJob!.items[keys[index]] = false
                                
                            }
                            .foregroundColor(.green)
                        }
                        // Else the item has not been moved yet
                        else {
                            Button("\(keys[index])") {
                                // Change the status of the item moved to false
                                model.currentJob!.items[keys[index]] = true
                                
                            }
                            .foregroundColor(.red)
                        }
                        
                    }
                }
                else {
                    Text("Has not loaded current job")
                }
            }
        }
    }
}

struct JobDetail_Previews: PreviewProvider {
    static var previews: some View {
        JobDetail()
    }
}

In JobDetail, the issue is this line:

ForEach(keys.indices) { index in

The initializer on ForEach that is being used here is this one:

init(_ data: Range<Int>, content: @escaping (Int) -> Content)

That’s becaise keys.indices returns a Range<Int>. Unfortunately, as the docs for this init tell us:

Creates an instance that computes views on demand over a given constant range.

With those last two words being the most important. You’ll notice if you look at George’s details first, then Mary’s, there will be no crash but you will only see 2 of Mary’s items. That’s because George only has 2 items and since the ForEach is constant, it believes that whatever it’s looping through will only have 2 items.

However, keys and values have been recalculated now and keys.indices.count == 5 so when you try to display George—who only has 2 items—again…crash!

Same thing happens if you look at Mary first then try to look at George.

There are two things you could do to fix this issue:

  1. Change the ForEach line to this:
ForEach(keys.indices, id: \.self) { index in

This will cause SwiftUI to use a different initializer on ForEach and the range that it loops through will be dynamic each time rather than constant.

Personally, I would not recommend this solution. It’s easiest, for sure, but it also ties each item’s id to its index in the keys array, which doesn’t work well if the array can have items added or removed. You could end up with a situation where you replace the item at, say, index 1 but because index 1 is the same as index 1, SwiftUI won’t recognize that the item stored at that index has changed and won’t re-render your new content to the screen.

  1. Add a method in your view model to return the items to loop through in a way that conforms to Identifiable and loop through them like that. This is where perhaps using a Set to store the items (which would be an array in the corresponding JSON) might be a good idea. If you are storing items that are only true or only false, you don’t really need a Dictionary since all the keys would have the same value.
1 Like

@roosterboy thanks so much for this detailed response! I wouldn’t understand those docs myself without your explanations either. Amazing you even tracked down why it crashed between the two! I wanted to implement your recommended solution, but couldn’t figure out how to get it to work. How would I structure a Set differently to store the values?

@RedFox1 I can certainly use your github project to demonstrate what I mean, but it might be tomorrow before I can finish and get back to you.

1 Like

Thanks @roosterboy! That would be great.

Otherwise, I got your method one working. It seems to work well, except if I have a long list of items, SwiftUI will not change the color of the button tapped. I will have to go back to the main page, then to that page again, in order to see the colors of the buttons changed (indicating in green it was moved; or red it was not). It works fine for two items or a few items, but more than that, it doesn’t work out. So I’m hoping to learn your method two.

You’re the best! Appreciate all this help :slight_smile:

Andrew

Sorry, took me longer than I expected due to external interferences.

So I’ve forked your sample project and made some changes to it. You can find it here. I think the code should be commented enough to make it clear what I’ve done and why. If not, you know what to do! :wink:

1 Like

Awesome, thanks so much Patrick!!! Appreciate you taking the time to do all this. You’re very generous, and I love comments! I find many developers comment too little, so appreciate that.

Hey @roosterboy thanks again for taking the time to fork that!

You’ve taught me already a few things:

  1. How to check someone else’s branch via a fork: git fetch git@github.com:<otherUserName>/<otherRepoName>.git <otherBranchName>:<newLocalBranchName>, which I learned from here.
  2. How to better structure a project/ Views e.g. the way you passed in the job as a binding, versus having a separate property to track that job. I often get confused with bindings/ get suspicious that the View underneath won’t change the binded value, however, this is not the case.
  3. I loved the way, you transformed the un-ordered set into an ordered array as a property! This seems super useful, and something I’ll be able to use any many different projects.
  4. You got me to read official docs/ how the language of Swift itself changes. Although, stuff like SE-0320 still flies over my head, it was good to get used to reading it. Also, I got a more meaningful breakdown of the changes via one of the other resources you recommended me, Paul Hudson, here.
  5. I LOVED the way you used an extension to create sample data, versus reading it from JSON. That’s something I keep using in a few of my other projects as well in order to get the SwiftUI Previews to load. However, I like the simpler way you used it here.

However, I don’t like how the Views re-draw after every press. Also, I’d like people to be able to change it back to red, if they accidentally pressed it, so will work on that.

Thanks so much for this!

Andrew

Hmm, it should toggle back and forth between the two every time you click. I may have messed something up. Let me figure it out and get back to you.

Okay, it’s because I tried to get fancy and use a custom ButtonStyle instead of applying the styling directly to the Buttons using modifiers. Fixed it now. The buttons toggle back and forth as you click on them.

1 Like

Amazing! You’re the best - can’t wait to try this out…

Ugh that’s too bad. I was impressed by that section of code; I liked the use of the modifier with .modifierName (I can’t remember the exact name, but hope that gets across the point)