I have an array of dictionaries I would like to populate in a list view with SwiftUI.
I used for loops in the past but since that's not possible within a View I'm stuck as to what to do. I'm able to achieve partial results with the code below.
struct Test : View {
let dict = csvArray[0]
var body: some View {
let keys = dict.map{$0.key}
let values = dict.map {$0.value}
return List {
ForEach(keys.indices) {index in
HStack {
Text(keys[index])
Text("\(values[index])")
}
}
}
}
}
I'm looking to index through the entire Array of dictionaries and append them to the list, not just csvArray[0].
This is like Sections of key values, right?
So do it like:
This is an example of csvArray, This could be anything but you should update the rest of the code duo to the original data type
let csvArray = [
[ "section0-key0": "section0-value0",
"section0-key1": "section0-value1"],
[ "section1-key0": "section1-value0",
"section1-key1": "section1-value1"],
[ "section2-key0": "section2-value0",
"section2-key1": "section2-value1"]
]
This is your code for a single dictionary. but this will take that dictionary instead of hardcoded one:
struct SectionView : View {
#State var dict = [String: String]()
var body: some View {
let keys = dict.map{$0.key}
let values = dict.map {$0.value}
return ForEach(keys.indices) {index in
HStack {
Text(keys[index])
Text("\(values[index])")
}
}
}
}
And this is the list builder connected to the original array. I used sections for the meaningfulness of the data structure.
struct ContentView: View {
var body: some View {
List {
ForEach(csvArray, id:\.self) { dict in
Section {
SectionView(dict: dict)
}
}
}
}
}
Note that you can't relay on the order of the key value in a dictionary. So I suggest you to do some sorting before populating the list or use another data structure like class or struct instead of a plain dictionary.
Related
This question already has answers here:
SwiftUI iterating through dictionary with ForEach
(10 answers)
Closed 1 year ago.
I am trying to make a list with a dictionary. My dictionary is located in a model. My dictionary is [String : String]. I tried to sort it hopefully it is sorted by alphabetical. I couldn't figure it out why it does not work
var fw: Deck
var body: some View {
let sortedDict = fw.dictItems.sorted(by: < )
let keys = sortedDict.map {$0.key}
let values = sortedDict.map {$0.value}
return List{
ForEach(keys.indices) { index in
HStack {
Text(keys[index])
Text("\(values[index])")
}
}
}
}
}
Here is a way for you:
struct ContentView: View {
#State var entries: [String: String] = ["key1":"val1", "key2":"val2", "key3":"val3", "key4":"val4"]
var body: some View {
List {
ForEach(entries.keys.sorted(by: <), id: \.self) { key in
HStack {
Text(key)
Text(entries[key]!)
}
}
}
}
}
I have data in a dictionary which is [String: String]. What I want to provide is an interface to the user to edit the values in the dictionary, while the keys remain fixed. I can see how to display the values, but putting them into a TextField is what I want, and haven't been able to find how to do.
Here is the code:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.sorted(by: <), id: \.key) { key, value in
HStack {
Text(key)
TextField("", text: $entries[key])
}
}
}
}
}
This doesn't compile, with no fewer than three errors on the TextField line:
Cannot convert value of type 'Slice<Binding<[String : String]>>' to expected argument type 'Binding'
Cannot convert value of type 'String' to expected argument type 'Range<Binding<[String : String]>.Index>'
Referencing subscript 'subscript(_:)' on 'Binding' requires that '[String : String]' conform to 'MutableCollection'
So obviously I am doing things incorrectly, but I am lost trying to find what the correct way would be, and haven't been able to find an answer in an internet search. Can anyone point me in the right direction?
you could try this simple approach:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.keys.sorted(by: <), id: \.self) { key in
HStack {
Text(key)
TextField("", text: Binding(
get: { entries[key]! },
set: { entries[key] = $0 }
))
}
}
}
}
}
struct ContentView: View {
#State var entries: [String: String] = ["key1":"val1", "key2":"val2", "key3":"val3", "key4":"val4"]
var body: some View {
dictionaryEditor(entries: $entries)
Button(action: { print("----> entries: \(entries)") }) {
Text("print entries")
}
}
}
The problem is that entries[key] returns an optional String value while the text parameter of TextField expects a Binding of non optional String.
You can create an optional binding extension and then you can use it safely:
extension Binding where Value == String? {
var optionalBind: Binding<String> {
.init(
get: {
wrappedValue ?? ""
}, set: {
wrappedValue = $0
}
)
}
}
Then you can just add the optionalBind to your code:
struct dictionaryEditor: View {
#Binding var entries: [String: String]
var body: some View {
List {
ForEach(entries.sorted(by: <), id: \.key) { key, value in
HStack {
Text(key)
TextField("", text: $entries[key].optionalBind) // <--
}
}
}
}
}
Ok, I'll preface this with the fact that I'm new to SwiftUI and programming. I'm a UX Designer. However, I'm trying to run a simple Firestore query and return the results to a list.
I've been able to write a function that writes the results to the console successfully, but I have no idea how to access the information that's within the function so that I can use it within the main view of the page.
I've started a simple view so that I can just focus on displaying Firestore data in a list. Here's my barebones code currently.
import SwiftUI
import FirebaseFirestore
struct FestivalListFB: View {
let db = Firestore.firestore()
func getVenues() {
let db = Firestore.firestore()
db.collectionGroup("Venues").getDocuments() {(querySnapshot, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
for document in querySnapshot!.documents {
guard let venueEntry = document.get("venueTitle") as? String else {
continue
}
print(venueEntry)
}
}
}
}
var body: some View {
VStack {
List(0 ..<5) { item in
Text("Hello, World!")
}
}.onAppear(perform: getVenues)
}
}
Console displays:
"Refreshment Outpost
Holiday Sweets & Treats
Fife & Drum Tavern
L'Artisan des Glaces
Shi Wasu
...etc"
And of course, the body only displays "Hello World" 5 times within a list. How would I go about accessing the values in "venueEntry" so that I can display it within the list element?
I've included a image of my Firestore data structure as well. Ideally, I'd like to display the venues grouped by the "venueArea" they are located in.
For easier use, i created a model for you venue. See the below snippet how you can show your data in your View.
Your model:
class VenueObject: ObservableObject {
#Published var venueID: String
#Published var venueTitle: String
#Published var venueArea: String
init(id: String, title: String, area: String) {
venueID = id
venueTitle = title
venueArea = area
}
}
Your View:
struct FestivalListFB: View {
#State var data: [VenueObject] = []
let db = Firestore.firestore()
var body: some View {
VStack {
ForEach((self.data), id: \.self.venueID) { item in
Text("\(item.venueTitle)")
}
}.onAppear {
self.getVenues()
}
}
func getVenues() {
// Remove previously data to prevent duplicate data
self.data.removeAll()
self.db.collectionGroup("Venues").getDocuments() {(querySnapshot, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
for document in querySnapshot!.documents {
let id = document.documentID
let title = document.get("venueTitle") as! String
let area = document.get("venueArea") as! String
self.data.append(VenueObject(id: id, title: title, area: area))
}
}
}
}
}
I'm new to flow, any trying to cover some of my functions, however often I have these snippets where I extract fields form an object based on some condition. But I'm struggling to cover them with flow.
const _join = function ( that: Array<Object>, by: string, index: number) {
that.forEach((thatOBJ: {[string]: any}, i: number)=>{
let obj: {[string]: any} = {};
for (let field: string in thatOBJ) {
if (field !== by) {
obj[`${index.toString()}_${field}`] = thatOBJ[field]; // NOT COVERED
} else {
obj[field] = thatOBJ[field]; // NOT COVERED
}
that[i] = obj;
}
});
}
The array that in this code is a data array so can really be in any format of mongodb data.
Any ideas on what to add to make the two lines which are not covered by flow covered?
Thanks.
A few notes...
This function has a "side effect" since you're mutating that rather than using a transformation and returning a new object.
Array<Object> is an Array of any, bounded by {}. There are no other guarantees.
If you care about modeling this functionality and statically typing them, you need to use unions (or |) to enumerate all the value possibilities.
It's not currently possible to model computed map keys in flow.
This is how I'd re-write your join function:
// #flow
function createIndexObject<T>(obj: { [string]: T }, by: string, index: number): { [string]: T } {
return Object.keys(obj).reduce((newObj, key) => {
if (key !== by) {
newObj[`${index}_${key}`] = newObj[key]
} else {
newObj[key] = obj[key]
}
return newObj
}, {})
}
// NO ERROR
const test1: { [string]: string | number } = createIndexObject({ foo: '', bar: 3 }, 'foo', 1)
// ERROR
const test2: { [string]: string | boolean } = createIndexObject({ foo: '', bar: 3 }, 'foo', 1)
In my Meteor app I have Lists which have Items. The Items can belong to multiple Lists, so the Lists contain a field with an array of Item IDs. When I go to a single List view I want a data object with the Items queried from this array of IDs. I think the query would look like this:
Items.find({ _id: { $in: theArrayOfIds } });
However how/where do I make this query when I load my single List view? At the moment this is my route declaration:
this.route('list', {
path: '/list/:_id',
data: function() {
return {
list: Lists.findOne(this.params._id)
}
}
});
Can I point at the future result of the list object somehow? Or do I make this query somewhere else?
In Iron Router, the data value of your route can be an object as well as a function. See here in the documentation. Therefore you can return both the items and the list in the data call as follows:
this.route('list', {
path: '/list/:_id',
data: {
list: function(){
var id = Router._currentController.params._id;
return Lists.findOne(id);
},
items: function(){
var id = Router._currentController.params._id;
var list = Lists.findOne(id);
if (!!list){
/* assuming your array of item ids is a field on list named items */
var theArrayOfIds = list.items;
}
return !!list && Items.find({ _id: {$in: theArrayOfIds}});
}
});