I need my app to display a table of data. The data looks like ["body": Optional("Go Shopping"), "isDeleted": Optional(false), "_id": Optional("63333b1600ce507b0097e3b3"), "isCompleted": Optional(false)]
The column headers for the table would be the keys body, isDeleted, isCompleted, _id
. I will have multiple instances of this data that will have the same keys, but different values. I will need to display the values for each data instance under the respective header and each row will belong to one data instance.
Example:
I'm struggling because the only way I can think of doing this is with a dictionary, but run into a lot of problems when using a dictionary in the View.
*** Important Note: The app allows a user to select a certain collection and then the app will load all the data for that collection. Each collection has different keys in its data, so I cannot create a specific struct since I won't actually know the keys/values in the data. The model will have to be dynamic in the sense that I don't know what key/value types will be used in each collection and the table will need to redraw when a different collection is selected.
What I Tried
A document class that would hold a 'value: [String: Any?]` the string would be the key and the Any is the value from the data instance
class Document{
let value: [String:Any?]
init(value:[String:Any?]) {
self.value = value
}
}
in my ViewModel I have a call to a database that uses the selected collection name to return an array of all the documents from that collection. I loop through the array and create a Document obj with the value of the Document looking like ["body": Optional("Go Shopping"), "isDeleted": Optional(false), "_id": Optional("63333b1600ce507b0097e3b3"), "isCompleted": Optional(false)]
and I add each Document to an array of Document's
class DocumentsViewModel : ObservableObject {
@Published var docKeys: [String]?
@Published var docsList: [Document]?
func getDocs() {
... //Database call to get docs from collection
for doc in docs {
// add doc keys to array (used for table header)
self.docKeys = doc.value.keys.map{$0}
self.docsList?.append(Document(value: doc.value))
}
Then in my View I tried to first display a header from the docKeys and then use that key to loop through the array of [Document] and access the value var and use the key to get the correct value to display under the header for that document
var body: some View {
Text(viewModel.collectionName)
HStack {
ForEach(viewModel.docKeys ?? [], id: \.description) {key in
Text(key.name)
VStack {
ForEach(viewModel.docsList ?? [], id: \.value) { doc in
Text(doc.value[property.name])
}
}
}
}
}
After doing research I understand why I can't ForEach over an unsorted dictionary.
I will accept any help/guidance on how I can display this table. Also, if there is any other suggestions besides using a dictionary? THANK YOU!
CodePudding user response:
Just a a few hints:
- If you need "dictionary with order" you can try to use a
Key-ValuePairs
object which is essentially an array of tuples with labelskey
andvalue
let values: KeyValuePairs = ["key1": "value1", "key2": "value2"]
when you print this to the console you'll realize that this is just a tuple!print(values[0])
will display(key: "key1", value: "value1")
- please take a look at
OrderedDictionary
fromThe Swift Collections
https://www.kodeco.com/24803770-getting-started-with-the-swift-collections-package - have you consider to use an array of simple structs instead?
struct Document {
let body: String?
let isDeleted: Bool?
let id: String?
let isCompleted: Bool?
...
}
CodePudding user response:
Here is some example code that shows how to deal with different collections data,
each having different keys. And still put all the results into an array of [Document]
,
that then gets displayed in a view.
struct ContentView: View {
@StateObject var viewModel = DocumentsViewModel()
// display one collection
func listCollection(_ name: String) -> some View {
VStack {
if let docs = viewModel.docCollections[name] {
Text(name).foregroundColor(.blue)
ScrollView {
ForEach(docs) { doc in
HStack {
ForEach(Array(doc.data.keys), id: \.self) { key in
HStack {
Text(key).foregroundColor(.red)
if let val = doc.data[key] as? NSObject {
Text("\(val)")
}
}
}
}
Divider()
}
}
} else {
Text("no data for collection \(name)")
}
}
}
var body: some View {
ScrollView {
listCollection("first collection")
Divider()
listCollection("second collection")
}
.onAppear {
viewModel.getDocsFor("first collection")
viewModel.getDocsFor("second collection")
}
}
}
class DocumentsViewModel : ObservableObject {
// the key as the name of the collection, and [Document] as the values
@Published var docCollections: [String : [Document]] = [:]
// data for testing, "first collection"
let jsonData1 = """
[
{
"body": "Go Shopping",
"isDeleted": false,
"_id": "6300b3",
"isCompleted": false
},
{
"body": "Go xxxx",
"isDeleted": false,
"_id": "1234",
"isCompleted": true
},
{
"body": "Go yyyy",
"isDeleted": true,
"_id": "7523",
"isCompleted": false
}
]
"""
// data for "second collection"
let jsonData2 = """
[
{
"xbody": "Go X",
"xbool": false,
"_id": "1600b3",
"xopt": "TTTTT"
},
{
"xbody": "XXXXX",
"xbool": true,
"_id": "6468",
"xopt": null
},
{
"xbody": "UUUUUU",
"xbool": false,
"_id": "864",
"xopt": "ertyuu"
}
]
"""
// simulated database fetching of any collection data
func getDocsFor(_ collectionName: String) {
// for testing
var jsonData = jsonData1 // "first collection"
if collectionName == "second collection" { jsonData = jsonData2 }
if let data = jsonData.data(using: .utf8) {
do {
docCollections[collectionName] = try JSONDecoder().decode([Document].self, from: data)
} catch {
print("decoding error: \(error)")
}
}
}
// var docsList: [Document] = []
// for doc in docs {
// docsList.append(Document(data: doc.value))
// }
// docCollections[collectionName] = docsList
// or alternatively
// docCollections[collectionName] = docs.map{Document(data: $0.value)}
}
struct Document: Identifiable, Decodable {
let id = UUID()
var data: [String: Any?] = [:]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DynamicKey.self)
container.allKeys.forEach { key in
if let theString = try? container.decode(String.self, forKey: key) {
self.data[key.stringValue] = theString
}
if let theInt = try? container.decode(Int.self, forKey: key) {
self.data[key.stringValue] = theInt
}
if let theDouble = try? container.decode(Double.self, forKey: key) {
self.data[key.stringValue] = theDouble
}
if let theBool = try? container.decode(Bool.self, forKey: key) {
self.data[key.stringValue] = theBool
}
}
}
}
struct DynamicKey: CodingKey {
var intValue: Int?
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = ""
}
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
}