0

I'm new to SwiftUI and any help will be appreciated.

I have a Json file:

[
    {
        "id": 1001,
        "key1": "truck",
        "key2": "car1",
        "key3": "motorcycle",
    },
    {
        "id": 1002,
        "key1": "truck",
        "key2": "car2",
        "key3": "motorcycle",    
    },
    {
        "id": 1003,
        "key1": "truck",
        "key2": "car2",
        "key3": "motorcycle",    
    },
    {
        "id": 1004,
        "key1": "truck",
        "key2": "car2",
        "key3": "motorcycle",    
    },
    {
        "id": 1005,
        "key1": "truck",
        "key2": "car3",
        "key3": "motorcycle",    
    },
]

I can reach values like this:

VStack{    
    ForEach(userData.vehicles) { vehicle in
       Text(vehicle.key2)
           .foregroundColor(.white)
    }
}

The output is:

car1
car2
car2
car2
car3

What I want to see is:

car1
car2
car3

Is there any way to group the keys in SwiftUI?

Edit: @MahdiBM requested the following codes in comment section.

Vehicle declaration

final class UserData: ObservableObject {
    @Published var showFavoritesOnly = false
    @Published var vehicles = vehicleInfo
}

Reading Json

let vehicleInfo: [Vehicle] = readJSON("vehicleInfo.json")
func readJSON<T: Codable>(_ named: String) -> T {
    if let resPath = Bundle.main.resourcePath {
        do {
            let dirContents = try FileManager.default.contentsOfDirectory(atPath: resPath)
            let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
            let filteredFiles = dirContents.filter{ $0.contains(".json")}
            for fileName in filteredFiles {
                if let documentsURL = documentsURL {
                    let sourceURL = Bundle.main.bundleURL.appendingPathComponent(fileName)
                    let destURL = documentsURL.appendingPathComponent(fileName)
                    do {
                        try FileManager.default.copyItem(at: sourceURL, to: destURL)
                        print("Save to documents")
                    } catch {
                        print("ERROR FileManager.default.copyItem:: ", error)
                    }
                }
            }
        }catch { print("ERRIR 2", error) }
    }
    
    let data: Data
    do {
        let fileURL = try FileManager.default
            .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
            .appendingPathComponent("vehicleInfo.json")
        
        data = try Data(contentsOf: fileURL)
        let foo = try JSONDecoder().decode(T.self, from: data)
//        print(foo)
        
        return foo
    } catch {
        fatalError("Couldn't find  in main bundle.")
    }
}

Edit 2:

struct Vehicle: Hashable, Codable, Identifiable {
    var id: Int
    var key1: String
    var key2: String
    var key3: String
}

SO says; "It looks like your post is mostly code; please add some more details."

They need to review this requirement I believe, because there is nothing I can say more.

8
  • How does vehicle declaration look like? How do you turn the json into vehicles? Commented Apr 11, 2021 at 13:44
  • I added that to my question. Commented Apr 11, 2021 at 13:58
  • thanks, i need to see Vehicle to be able to give you an appropriate answer. Something like struct Vehicle { var key1: String ...... }. This above code looks good though and at first glance doesn't have any real problems. Commented Apr 11, 2021 at 14:11
  • 1
    Thank you for trying to make my question clear. I also added that. Commented Apr 11, 2021 at 14:27
  • So you only want to access key2 from the struct? If not, in the json, do you consider all elements to be equal that has key2 = "Car2" even though the id is different for each? Commented Apr 11, 2021 at 14:42

2 Answers 2

1

Correct me if im wrong but from what i understand you have some Vehicles that can have a repetitive key2 and you don't like that. You instead want the key2s to not be repetitive.

var vehicles: [Vehicle] = [...]

// to group all `key2`s together, you can use `.map`:
var allKey2: [String] = vehicles.map { $0.key2 } // or `vehicles.map(\.key2)`, both are the same

// now you have all `key2`s grouped together
// to remove repetitive members, you can store the keys in a `Set` and convert them
// back to `Array`. why `Set`? because `Set` automatically removes repetitive members,
// but it also doesnt have any order for the members so we need to order the members back.
var noRepeatUnorderedAllKey2: [String] = Array(Set(allKey2))
// here we sort based on which member appeared sooner/later in the `allKey2` array
var noRepeatOrderedAllKey2: [String] = noRepeatUnorderedAllKey2.sorted {
    (allKey2.firstIndex(of: $0) ?? 0) < (allKey2.firstIndex(of: $1) ?? 0)
}

now you can do this, and you'll have car1, car2, car3 instead of car1, car2, car2, car2, car3:

VStack{    
    ForEach(noRepeatOrderedAllKey2) { vehicleKey2 in
       Text(vehicleKey2)
           .foregroundColor(.white)
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

You are right and thank you so much for your time. I got following error: Cannot use instance member 'vehicles' within property initializer; property initializers run before 'self' is available. I initialized like this: init() { vehicles = [] } What could be the problem?
i need more context, where did you type that init() { vehicles = [] }? the error says you need to first set a value for all things in the struct before using any values of the struct, but i can't see why its saying that here.
My previous comment was wrong, I solved with using id: \.self in ForEach. Your solution is top-notch, thank you so much for your help.
1

I would introduce a new property in your UserData class to hold the unique keys from your Vehicle array. This property is automatically updated when the vehicles property is set.

final class UserData: ObservableObject {
    @Published var showFavoritesOnly = false
    @Published var vehicles: [Vehicle] {
        willSet {
            vehicleKeys = Set(newValue.map(\.key2)).sorted()
        }
    }
    @Published var vehicleKeys: [String] = []
}

And then you can use the vehicleKeys array in your ForEach and you could have a function that returns the Vehicle objects for the selected key.

Another option is to use a dictionary, since then you would have easier access to the corresponding Vehicle objects for a key

    @Published var vehicles: [Vehicle] {
        willSet {
            vehicleKeys = Dictionary(grouping: newValue, by: \.key2)
        }
    }
    @Published var vehicleKeys: [String: [Vehicle]] = [:]

Dictionaries aren't sorted though

7 Comments

Thank you for your time. I got Class 'UserData' has no initializers error. How can I initialize it?
init() { vehicles = [] } is one way.
I did like this: ForEach(userData.vehicleKeys, id: \.self) { vehicleKey in Text(vehicleKey)} No data show up. Do you know what could be the problem?
Is userData declared with property wrapper @ObservedObject?
I use @EnvironmentObject var userData: UserData. I also tried @ObservedObject but some other view crashed with that. I believe @EnvironmentObject should work but no chance, no data show up.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.