ForEach on a dictionary - ios

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!
**Update
I was able to get it working with an ordered collection
class Document : Hashable, Equatable{
static func == (lhs: Document, rhs: Document) -> Bool {
lhs.id.description == rhs.id.description
}
let id: String
let value: OrderedDictionary<String,Any?>
init(id: String, value: OrderedDictionary<String,Any?>) {
self.id = id
self.value = value
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
Since it is ordered this allowed me to iterate of the dictionary in the View

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 labels key and value
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 from The 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?
...
}

Related

Loop through Dictionary in SwiftUI

I have var countriesGroupedByRegion: Dictionary<String, [Array<Country>.Element]>
The key (String) is the region and the array are all the countries within that region.
I need to loop through this dictionary so I can create a List with as much sections as regions, and within each section, I want to show all the countries... but I don't know how to loop through this dictionary in SwiftUI using ForEach. My current code is the following:
NavigationView {
List {
ForEach(countriesGroupedByRegion, id: \.self) { region in
}
}
}
where countriesGroupedByRegion is the mentioned dictionary. I get the following error: Cannot convert value of type 'Dictionary<String, [Array<Country>.Element]>'
Could someone help me figure out how to loop through this dict and create the sections in SwiftUI?
I recommend to map the (keys of the) dictionary to an array of a region struct
struct Region {
let name : String
let countries : [Country]
}
This can be used in a ForEach statement with id: \.name. Further consider that a dictionary is unordered.
This is a simple example with a string array representing the countries
let dictionary = ["Asia": ["Japan", "India"], "Europe": ["Italy", "Norway"]]
struct Region {
let name : String
let countries : [String]
}
let regions = dictionary
.keys
.sorted()
.map{Region(name: $0, countries: dictionary[$0]!)}
NavigationView {
List {
ForEach(regions, id: \.name) { region in
}
}
}

List is not showing data from API in SwiftUI?

I am trying to get data from API and want to show that in List.
List {
ForEach(0 ..< (declarationViewModel.items?.count)!) { index in
HStack {
......
but as its taking some time to fetch data from API, initially data is 0 hence can not able to show data in list as it's not reloading after getting data.
public class DeclarationViewModel: ObservableObject {
#Published var items: [Item]?
#Published var title: String?
init() {
self.items = [Item]()
self.title = ""
}
init(items: [Item]?, title: String) {
self.items = items
self.title = title
}
}
Error log
forEach<Range<Int>, Int, HStack<TupleView<(HStack<TupleView<(ModifiedContent<ModifiedContent<Image, _EnvironmentKeyWritingModifier<Optional<Color>>>, _FrameLayout>, Text, Spacer)>>, Spacer, _ConditionalContent<ModifiedContent<ModifiedContent<Image, _EnvironmentKeyWritingModifier<Optional<Color>>>, _FrameLayout>, ModifiedContent<ModifiedContent<Image, _EnvironmentKeyWritingModifier<Optional<Color>>>, _FrameLayout>>, _ConditionalContent<ModifiedContent<ModifiedContent<Image, _EnvironmentKeyWritingModifier<Optional<Color>>>, _FrameLayout>, ModifiedContent<ModifiedContent<Image, _EnvironmentKeyWritingModifier<Optional<Color>>>, _FrameLayout>>)>>> count (14) != its initial count (0). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!
you could try something simple like this:
EDIT: and if your items are Identifiable, then remove the "id:.\self",
nothing to it.
List {
if let items = declarationViewModel.items {
ForEach(items, id: \.self) { item in
....
}
}
}
Check out your console, you'll see following warning:
ForEach<Range, Int, ModifiedContent<Text, AddGestureModifier<_EndedGesture>>> count (2) != its initial count (1). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
Newer use 0..<shoppingList.count inside ForEach or List. At least use declarationViewModel.items.indices instead.
Make items non optional by specifying a default value of an empty array instead of initializing it in the init. You can do same with title:
#Published var items: [Item] = []
#Published var title = ""
But perfectly instead of using indices you need to confirm your Item to Identifiable and make sure each item has a unique id, so your animations will work good and SwiftUI reuse cells to increase performance. You can use UUID:
struct Item: Identifiable {
let id = UUID()
}
Then in ForEach you get item instead of index:
ForEach(declarationViewModel.items) { item in
If you also need index in ForEach, check out my ForEachIndexed in this answer

same value in dictionary in swift

I m mapping data that come from service with using dictionary [String: String]. I collect them dictionary array. For example, if their parent ids are the same, I want to add their values by array value.
["ParentId": "1","Value": "["Giyim","Aksesuar","Ayakkabı"]"]
It is also the reason I don't know parent id sometimes on the left sometimes on the right in photo
Here is my code and its output.
struct Categories {
let parentId: String
let values: [String]
}
for result in results {
if result?.parentCategoryId != "" {
for docId in self.docIds {
if result?.parentCategoryId == docId {
//print(result?.name)
var values = [String]()
values.append(result?.name ?? "")
self.newCat.append(Categories(parentId: docId, values: values))
}
}
}
}
Problem
As far as I understand from the description you want to map some service data structure to a dictionary where key is parentId and value is an array of some items referred to parentId.
I believe your problem comes from a misunderstanding of the concept of dictionary as a data structure.
[String: String] is dictionary where keys and their associated values are of String type. For example:
["firstKey": "firsthValue", "secondKey": "secondValue", ...]
That means you cannot store associated values of String and Array types in the same dictionary, as you already told the compiler you would like to store only strings.
It is also the reason I don't know parent id sometimes on the left sometimes on the right in photo
This is because key-value pairs are stored in the dictionary without order. This is how dictionaries work :) I'd strongly recommend reading some short official materials to get used to them.
New Swift 5.4 version has a new OrderedDictionary data structure, where keys are ordered, but there is absolutely 100500% no reason to use it for your problem*
Possible solutions
In your case i would suggest either use some struct:
struct SomeData {
let parentID: Int
var values: [String]
}
var storage: [SomeData] // alternative to emptyDic
// Filling storage with data from service
for result in inputData {
// search for SomeData with required id, add value
// OR create SomeData if there is no such id in array yet
}
OR [personally this appeals to me more]
Store data in [String: [String]] dictionary, where the key is parentID and the associated value is an array of values.
The algorithm of filling this dictionary is pretty the same:
You add new key-value pair for every new parentID
You append new values for parentIDs that are already in the dictionary.
Using the struct approach, you could do something like this (you'll need to adapt it to your code, but that should be straightforward):
struct Categories {
let parentId: String
var values: [String] //notice this needs to be a var, not a let
}
func addItem(categories : inout [Categories], docId: String, name: String) {
if let index = categories.firstIndex(where: { $0.parentId == docId }) {
categories[index].values.append(name)
} else {
categories.append(Categories(parentId: docId, values: [name]))
}
}
func addValues() {
var categories = [Categories]()
addItem(categories: &categories, docId: "4", name: "Test1")
addItem(categories: &categories, docId: "1", name: "Test")
addItem(categories: &categories, docId: "4", name: "Test2")
addItem(categories: &categories, docId: "4", name: "Test3")
print(categories)
//in your code, it'll look more like:
// addItem(categories: &self.newCat, docId: docId, name: result?.name ?? "")
}
Which yields this:
[
StackOverflowPlayground.Categories(parentId: "4", values: ["Test1", "Test2", "Test3"]),
StackOverflowPlayground.Categories(parentId: "1", values: ["Test"])
]
I still wonder whether you maybe just want a Dictionary that is keyed by the parentId, but not knowing your use case, it's hard to say.

trying to decode value in swift 4.2 using its position in the json structure

How can I decode the following using swift Decodable? I'm only interested in the extract value
{
"batchcomplete":"",
"query":{
"normalized":[ ],
"pages":{
"434325":{ //can be any number!
"pageid":434325,
"ns":0,
"title":"asdfasdfsa",
"extract":""
I'm attempting the following:
struct Entry: Decodable {
let batchcomplete: String
let query: Query
struct Query: Decodable {
let normalized: [String]
let pages: Page
struct Page: Decodable {
let pageid: Entry // I think this is going to be an issue
struct Entry: Decodable {
let title: String
let extract: String
}
}
}
}
I'm trying to retrieve the extract like this:
print(entry.query.pages.first.extract)
Is there a way to use .first for this ?
I'm only every going to get maximum one page so ideally I would just grab the first element.
Your decoding code is ok up until `Query type.
So what should be used instead:
struct Query: Decodable {
let normalized: [String]
let pages: [String: Page]
struct Page: Decodable {
let pageid: Int
let title: String
let extract: String
}
}
So, key points:
If you don't know what keys would be there, use [String: <SomeDecodableType>] because any JSON Object can be mapped to the [String: <Decodable>].
You don't need to declare Page.Entry, just put all the fields to the Page
To get extract instead of entry.query.pages.first.extract you will have to use entry.query.pages.first?.value.extract (extract of first random page, because [String: Page] is unsorted) or entry.query.pages.sorted(by: sorter).first?.value.extract (where sorter is some function, that takes two key-value pairs and returns true if first one should go before second one in order) to get first using some order.

Proper way to get swift dictionary item index

I have dictionary in my app written in Swift
var cities: [String: String] = ["":"not specified", "ny":"New York", "la":"Los Angeles", "sf":"San Francisco"]
I use this dictionary to build pickerView. When user selects one of this items - city code is saving to device memory.
When user opens app next time I want pickerView to show saved city. How should i do it?
You can find the index of a key in a dictionary with higher-order functions:
let desiredKey = "ny"
let pos = x.enumerate().filter { (pair: (index: Int, element: (String, String))) -> Bool in
return pair.element.0 == desiredKey
}.map { (pair: (index: Int, element: (String, String))) -> Int in
return pair.index
}
pos is an Optional(Int) containing the position of "ny" in the current iteration order of the dictionary.
Store the value of the key somewhere (say, in user defaults), then retrieve it, get the index of the key-value pair using the code above, and pass to selectedRowInComponent: of your picker view.
Note: The problem with using a dictionary alone as a backing for your picker is that the order is not specified explicitly. Adding or removing keys may change the order of existing keys. In addition, existing keys may not end up in places that you want them to be - for example, your "" - "not specified" pair may end up in the middle of the picker.
To fix this problem, keep a separate array of city keys in the proper order that you wish to follow. Use this array to decide the picker row in which a city is to be displayed, and look up the actual city using the dictionary.
You can use NSUserDefaults to insure the persistency of your cityCode.
Copy an paste this sample in a playground.
var cities: [String: String] = ["":"not specified", "ny":"New York", "la":"Los Angeles", "sf":"San Francisco"]
let cityCodeIndexKey="cityCodeIndexKey"
/**
Selects and saves the cityCode
- parameter cityCode: a cityCode String
- returns: true if the code exists.
*/
func selectCity(cityCode:String)->Bool{
if let _ = cities.indexForKey(cityCode){
NSUserDefaults.standardUserDefaults().setObject(cityCode, forKey:cityCodeIndexKey)
NSUserDefaults.standardUserDefaults().synchronize()
return true
}else{
return false
}
}
selectCity("ny") // Returns true
selectCity("pr") // Returns false
NSUserDefaults.standardUserDefaults().valueForKey(cityCodeIndexKey) // returns "ny"
Get key,
var cityLongName <- user selected one
var key = dict.first((key,value) In{
If (value == cityLongName ){
return true
}else{
return false
}
}).key!
Get values ,
var value = dict[“city sort name”]

Resources