List is not showing data from API in SwiftUI? - ios

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

Related

ForEach on a dictionary

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?
...
}

Using a Picker with CoreData - Saving and fetching data

I am trying to save a string selection from a picker into CoreData, but it is stating
"Picker: the selection "" is invalid and does not have an associated tag, this will give undefined results." I can see all the options, but it won't let me click any of them and its throwing that error down below in the console.
I have the following:
'feeling' is defined in my CoreData Entity as a String & defined in my SleepModel type as a String
PickerValues.swift
class PickerValues {
enum Feeling: String, CaseIterable, Codable {
case great = "Great"
case ok = "OK"
case bad = "Bad"
}
}
In the AddEntryView.swift:
#State var feelingSelect = String()
Picker ("Feeling", selection: $feelingSelect, content: {
ForEach(PickerValues.Feeling.allCases, id: \.self){ feeling in
Text(feeling.rawValue).tag(feeling)
}
}).foregroundColor(.black)
//inside the function called when clicking the add entry button
var sleepModel = SleepModel(feeling: feelingSelect)

SwiftUI ForEach with array or sub-array

I want to iterate an array using ForEach and, depending on the use-case, its sub-version with fewer elements.
ForEach(isExpanded ? $items : $items[..<4])
The problem is if I am using a subscript, I am getting an error Result values in '? :' expression have mismatching types 'Binding<[RowItem]>' and 'Binding<[RowItem]>.SubSequence' (aka 'Slice<Binding<Array<RowItem>>>'). I can not use it like this, because it is not an array but a Slice. Just casting to Array does not work either.
This is a sample code which responds to the poster's second question.
Use binding to store/pass/receive data from another struct:
struct Test: View {
#State var bindWithChild: String = ""
var body: some View {
Text("\(bindWithChild)")
//when you edit data in the child view, it will updates back to the parent's variable
TestChild(receiveAndUpdate: $bindWithChild)
}
}
struct TestChild: View {
#Binding var receiveAndUpdate: String
var body: some View {
TextField("Enter here", text: $receiveAndUpdate)
}
}
However, If your data only works within one struct, and you don't need to pass the data back and forth within another struct, use #State:
struct Test: View {
#State var binding: String = ""
var body: some View {
Text("\(binding)")
//when you edit data in the TextField below, it will update this Text too.
TextField("Enter data: ", text: $binding)
}
}
You don’t have to use $ sign in front of your arrays.
ForEach(isExpanded ? items[..<items.count] : items[..<4], id: \.self) { sub in
}

swiftUI using picker to show different values in the Text field than in the selector

I've got an array of strings that I want to present using swiftUI Picker widget. Each string is composed of multiple words delimited by spaces.
I'd like to get the Picker showing the full string of each item in the list, while the selection variable should only get the first word (the selected item is stored in arg)
This was my attempt to do so. notice that the object that hold the items called myHandler, and it's shared to the swiftUI view, and can be modified by external swift closure:
class myHandler: ObservableObject {
#Published var items = [String]()
}
struct ContentView: View {
#State var arg: String = ""
#ObservedObject var handler : myHandler
...
VStack {
Picker("items", selection: $arg) {
Text("AAA").tag("xxx")
Text("BBB").tag("yyy")
Text("CCC").tag("zzz")
ForEach(handler.items , id: \.self, content: {
Text($0).tag($0.components(separatedBy: " ")[0])
})
}
}
.frame()
TextField("firstword", text: $arg).frame()
For the options outside the ForEach statement, I can see that arg get the value written in the tag. However, for all options that derived from the ForEach, I see that arg equals to the iterable item ($0) which is the multi work string, and not to the first word as expected.
Any idea how to fix those items that are generated from the ForEach, so that selection of such item, will set the arg to the string value of the first word in the iterator ?
Picker("items", selection: $arg) {
ForEach(handler.items, id: \.self) {
Text($0)
.tag($0.components(separatedBy: " ").first ?? "")
}
}

SwiftUI List not recognizing object type

So I'm trying to make a List in SwiftUI like this:
struct DetailsView: View {
var piis = [IDPiece]()
var body: some View {
List(piis, id: \.identifier) { pii in
Text( pii.label )
}
}
}
where IDPiece looks like:
struct IDPiece: Equatable {
init() {}
init(claim: Claim) {
self.document = claim.document
self.identifier = claim.identifier
self.claimUID = claim.claimUID
self.label = claim.label
}
var document: DocumentType = .na
var identifier: String = ""
var claimUID: String = ""
var label: String?
}
But I keep receiving the following error on the line where I initialize the list:
Type '_' has no member 'identifier'
It doesn't seem to be parsing the type of object contains in my piis list. Anyone know why that may be?
SwiftUI compiler errors are generally useless (this will improve over time, but today they're useless). Your problem has nothing to do with \.identifier. The problem is you have an optional .label, but you don't handle the case where it's nil. Almost certainly you should just make label non-optional. But if it needs to be optional (if you treat nil differently than empty in some place), then you need to do something about that, such as:
Text(pii.label ?? "N/A")

Resources