ForEach not working with Identifiable & id = UUID() - foreach

import SwiftUI
struct TestStudentView: View {
#StateObject var students = Students()
#State private var name = ""
#State private var numberOfSubjects = ""
#State private var subjects = [Subjects](repeating: Subjects(name: "", grade: ""), count: 10)
var body: some View {
NavigationView {
Group {
Form {
Section(header: Text("Student details")) {
TextField("Name", text: $name)
TextField("Number of subjects", text: $numberOfSubjects)
}
let count = Int(numberOfSubjects) ?? 0
Text("Count: \(count)")
Section(header: Text("Subject grades")) {
if count>0 && count<10 {
ForEach(0 ..< count, id: \.self) { number in
TextField("Subjects", text: $subjects[number].name)
TextField("Grade", text: $subjects[number].grade)
}
}
}
}
VStack {
ForEach(students.details) { student in
Text(student.name)
ForEach(student.subjects) { subject in //Does not work as expected
//ForEach(student.subjects, id:\.id) { subject in //Does not work as expected
//ForEach(student.subjects, id:\.self) { subject in //works fine with this
HStack {
Text("Subject: \(subject.name)")
Text("Grade: \(subject.grade)")
}
}
}
}
}
.navigationTitle("Student grades")
.navigationBarItems(trailing:
Button(action: {
let details = Details(name: name, subjects: subjects)
students.details.append(details)
}, label: {
Text("Save")
})
)
}
}
}
struct TestStudentView_Previews: PreviewProvider {
static var previews: some View {
TestStudentView()
}
}
class Students: ObservableObject {
#Published var details = [Details]()
}
struct Details: Identifiable {
let id = UUID()
var name: String
var subjects: [Subjects]
}
struct Subjects: Identifiable, Hashable {
let id = UUID()
var name: String
var grade: String
}
When I use - "ForEach(student.subjects, id:.id) { subject in" under normal circumstances it is supposed to work as id = UUID and the incorrect output is as follows:
then as the class conforms to Identifiable I tried - "ForEach(student.subjects) { subject in" it still does not work correctly. However, when I do - "ForEach(student.subjects, id:.self) { subject in" except I had to have the class conform to hashable and gives me the correct expected output. The correct output which is shown:

You need to use a map instead of repeating.
By using Array.init(repeating:) will invoke the Subjects to initialize only one time, and then insert that object into the array multiple times.
So all, in this case, all id is same.
You can check by just print all id in by this .onAppear() { print(subjects.map({ (sub) in print(sub.id) }))
struct TestStudentView: View {
#StateObject var students = Students()
#State private var name = ""
#State private var numberOfSubjects = ""
#State private var subjects: [Subjects] = (0...10).map { _ in
Subjects(name: "", grade: "")
} //<-- Here

Related

How to loop HashMap style in the View in SWIFTUI

var someProtocol = [SurveyItems : [Surveys]]()
sectionLabels.forEach{ a in
var finalSurveys = [Surveys]()
surveys.forEach{ b in
if a.groupHeader == b.group_survey {
finalSurveys.append(b)
}
someProtocol[a] = finalSurveys
}
}
I wanted to use that someProtocol to dynamically display the label section and the surveys under that section.
for (Surveys, SurveyItems) in someProtocol {
Text(Surveys.sectionTitle)
for survey in SurveyItems {
Text(survey.label)
}
}
I tried ViewBuider but getting some error.
To loop and display your someProtocol dictionary in a View, try this example code:
Adjust the code for your own purpose. Note that in a SwiftUI View you need to use a ForEach not the "normal" swift for x in ... to loop over a sequence.
struct ContentView: View {
#State var someProtocol = [SurveyItems : [Surveys]]()
var body: some View {
List(Array(someProtocol.keys), id: \.self) { key in
VStack {
if let surveys = someProtocol[key] {
Text(key.title).foregroundColor(.red)
ForEach(surveys, id: \.self) { survey in
Text("survey \(survey.label)")
}
}
}
}
.onAppear {
// for testing
someProtocol[SurveyItems(id: "1", number: 1, title: "title-1")] = [Surveys(id: "s1", label: "label-1"), Surveys(id: "s2", label: "label-2")]
someProtocol[SurveyItems(id: "2", number: 2, title: "title-2")] = [Surveys(id: "s3", label: "label-3")]
}
}
}
struct SurveyItems: Identifiable, Hashable {
let id: String
let number: Int
var title: String
}
struct Surveys: Identifiable, Hashable {
let id: String
let label: String
}

How do I create a dynamic input list?

I have some accounts that I want to get a user to input a number to provide an initial balance.
Not sure how to do the binding. Here's my model:
class DefaultBalancesViewModel: ObservableObject {
#Published var accountBalances: [Account: String] = [:]
#Published var accounts: [Account] = []
private let dbCore: DBCore
init(dbCore: DBCore = DBCore.shared) {
self.dbCore = dbCore
self.accounts = dbCore.accounts
self.accountBalances = dbCore.accounts.reduce(into: [Account: String]()) { $0[$1] = "" }
}
}
and my basic SwiftUI:
struct DefaultBalancesView: View {
#StateObject var viewModel = DefaultBalancesViewModel()
var body: some View {
ForEach(viewModel.accounts) { account in
HStack {
Text(account.name)
Spacer()
TextField("", text: $viewModel.accountBalances[account]) // <-- error here: Cannot convert value of type 'Binding<[Account : String]>.SubSequence' (aka 'Slice<Binding<Dictionary<Account, String>>>') to expected argument type 'Binding<String>' (and a couple of other errors)
}
}
Button("Save") {
//
}
}
}
How should I structure this so I can dynamically enter a number for each account?
At the moment you have multiple copies of Account arrays, and a dictionary with String! There should be only one source of truth, eg #Published var accounts: [Account], and the Account should be able to hold (or calculate) its balance.
Try this approach (works well for me), where you have only one array of [Account] (one source of truth), that you can edit
using a TextField:
Customise the code to suit your needs, e.g the currency,
or simply a "normal" decimal formatter.
class DefaultBalancesViewModel: ObservableObject {
// for testing
#Published var accounts: [Account] = [Account(name: "account-1", balance: 1.1),
Account(name: "account-2", balance: 2.2),
Account(name: "account-3", balance: 3.3)]
private let dbCore: DBCore
init(dbCore: DBCore = DBCore.shared) {
self.dbCore = dbCore
self.accounts = dbCore.accounts
}
}
struct DefaultBalancesView: View {
#StateObject var viewModel = DefaultBalancesViewModel()
var body: some View {
ForEach($viewModel.accounts) { $account in // <-- here
HStack {
Text(account.name).foregroundColor(.red)
Spacer()
TextField("", value: $account.balance, format: .currency(code: "USD")) // <-- here
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Button("Save") {
// for testing
viewModel.accounts.forEach { print("--> name: \($0.name) balance: \($0.balance)")}
}
}
}
// for testing
struct Account: Identifiable {
let id = UUID()
var name: String
var balance: Double // <-- here
}

How to create new instance of object and pass it into array SwiftUI

I want to create simple program for edit this JSON : https://pastebin.com/7jXyvi6Y
I created Smoothie struct and read smoothies into array.
Now I want create new Smoothie instance which I should pass as parameter into SmoothieForm. In Smoothie form I should complete fields with values and then this smoothie should be added to array and array should be saved in json.
How to create new instance of this Smoothie struct ? And how append into array ?
I have struct with my smoothies
import Foundation
import SwiftUI
struct Smoothie : Hashable, Codable, Identifiable {
var id: Int
var name: String
var category: Category
var wasDone: Bool
var isFavorite: Bool
var time: String
var ingedients: [Ingedients]
var steps: [Steps]
var image : Image {
Image(imageName)
}
enum Category: String, CaseIterable, Codable {
case forest = "Forest fruit"
case garden = "Garden fruit"
case egzotic = "Exotic"
case vegatble = "Vegetables"
}
private var imageName: String
struct Steps: Hashable, Codable {
var id: Int
var description: String
}
struct Ingedients: Hashable, Codable {
var id: Int
var name: String
var quantity: Double
var unit: String
}
}
And now I builded form view with first few fields:
struct SmoothieForm: View {
var body: some View {
VStack {
Text("Add smooth")
HStack {
Text("Name")
TextField("Placeholder", text: .constant(""))
}
HStack {
Text("Category")
TextField("Placeholder", text: .constant(""))
}
HStack {
Text("Time")
TextField("Placeholder", text: .constant(""))
}
Divider()
}
.padding(.all)
}
}
struct SmoothieForm_Previews: PreviewProvider {
static var previews: some View {
SmoothieForm()
}
}
Class for load data from json :
import Foundation
final class ModelData:ObservableObject{
#Published var smoothies: [Smoothie] = load("smoothieData.json")
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename,withExtension: nil) else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
I work with c # on a daily basis
import SwiftUI
//You need default values so you can initialize an empyty item
struct Smoothie : Hashable, Codable, Identifiable {
//Find a way to make this unique maybe switch to UUID
var id: Int = 999999
var name: String = ""
var category: Category = Category.unknown
var wasDone: Bool = false
var isFavorite: Bool = false
var time: String = ""
var ingedients: [Ingedients] = []
var steps: [Steps] = []
var image : Image {
if !imageName.isEmpty{
return Image(imageName)
}else{
return Image(systemName: "photo")
}
}
enum Category: String, CaseIterable, Codable {
case forest = "Forest fruit"
case garden = "Garden fruit"
case egzotic = "Exotic"
case vegatble = "Vegetables"
case unknown
}
private var imageName: String = ""
struct Steps: Hashable, Codable {
var id: Int
var description: String
}
struct Ingedients: Hashable, Codable {
var id: Int
var name: String
var quantity: Double
var unit: String
}
}
struct SmothieForm: View {
//Give the View access to the Array
#StateObject var vm: ModelData = ModelData()
//Your new smoothie will be an empty item
#State var newSmoothie: Smoothie = Smoothie()
var body: some View {
VStack {
Text("Add smooth")
HStack {
Text("Name")
//reference the new smoothie .constant should only be used in Preview Mode
TextField("Placeholder", text: $newSmoothie.name)
}
VStack {
Text("Category")
//reference the new smoothie .constant should only be used in Preview Mode
Picker(selection: $newSmoothie.category, label: Text("Category"), content: {
ForEach(Smoothie.Category.allCases, id: \.self){ category in
Text(category.rawValue).tag(category)
}
})
}
HStack {
Text("Time")
//reference the new smoothie .constant should only be used in Preview Mode
TextField("Placeholder", text: $newSmoothie.time)
}
Divider()
//Append to array when the user Saves
Button("Save - \(vm.smoothies.count)", action: {
vm.smoothies.append(newSmoothie)
})
}
.padding(.all)
}
}

SwiftUI #Binding property not updating nested views - Xcode 12

I have problems updating my nested SwiftUI views with #Binding property.
I declared a DataModel with ObservableObject protocol:
class DataModel: ObservableObject {
#Published var subjects: [Subject] = []
...
}
I added it to my main app:
#main
struct LessonToTextApp: App {
#ObservedObject private var data = DataModel()
var body: some Scene {
WindowGroup {
NavigationView {
SubjectsView(subjects: $data.subjects) {
data.save()
}
}
.onAppear {
data.load()
}
}
}
}
I passed the Subjects array to the first view
struct SubjectsView: View {
#Binding var subjects: [Subject]
var body: some View {
List {
if subjects.isEmpty {
Text("subjects.empty")
} else {
ForEach(subjects) { subject in
NavigationLink(destination: DetailView(subject: binding(for: subject), saveAction: saveAction)) {
CardView(subject: subject)
}
.listRowBackground(subject.color)
.cornerRadius(10)
}
}
}
private func binding(for subject: Subject) -> Binding<Subject> {
guard let subIndex = subjects.firstIndex(where: { $0.id == subject.id }) else {
fatalError("Can't find subject in array")
}
return $subjects[subIndex]
}
And then i passed the single subject to the Second view using the function binding declared above:
struct DetailView: View {
#Binding var subject: Subject
var body: some View {
ForEach(subject.lessons) { lesson in
NavigationLink(destination: LessonView(lesson: lesson)) {
Text(lesson.date, style: .date)
}
}
.onDelete { indexSet in
self.subject.lessons.remove(atOffsets: indexSet)
}
}
In the DetailView, when i delete an item in ForEach the item still appear, the view doesn't update.
I'm using SwiftUI 2.0 on Xcode 12.3 (12C33)
EDIT
This is the Model:
struct Subject: Identifiable, Codable {
let id: UUID
var name: String
var teacher: String
var color: Color
var lessons: [Lesson]
}
struct Lesson: Identifiable, Codable {
let id: UUID
let date: Date
var lenghtInMinutes: Int
var transcript: String
}
Your SubjectsView should take in the entire DataModel as an #EnvironmentObject
struct LessonToTextApp: App {
#StateObject private var data = DataModel()
var body: some Scene {
WindowGroup {
NavigationView {
SubjectsView().environmentObject(data) {
data.save()
}
...
struct SubjectsView: View {
#EnvironmentObject var data = DataModel
var body: some View {
List {
if data.subjects.isEmpty {
Text("subjects.empty")
...
Also, struct are immutable
class Subject: Identifiable, Codable, ObservableObject {
let id: UUID
#Published var name: String
#Published var teacher: String
#Published var color: Color
...
}
struct DetailView: View {
#ObservedObject var subject: Subject
var body: some View {
...
That way you can get to the DetailView with
DetailView(subject: subject)

If a property of my model object is an array of a custom object, how do I let users append objects to it?

I'm making an app where the user can create groups, and fill these groups with people. Groups are showed in a list and link to GroupViews, where there is a list of people part of that particular group.
The list of and creation of groups work as expected; the user can create groups and clicking any group takes the user to that specific groups own view.
The list of and creation of people don't work; when the user attempts to append a person to the people list it falls between the AddPersonView and GroupView and does not show up in the list.
Below is my current attempt at a solution:
Models.swift
import Foundation
struct Group: Identifiable {
var id: UUID
var name: String
var people: [Person]
init(name: String) {
self.id = UUID()
self.name = name
self.people = [Person]()
}
}
struct Person: Identifiable {
var id: UUID
var firstName: String
var lastName: String
}
ModelView.swift
import Foundation
class GroupList: ObservableObject {
#Published var groups = [Group]()
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject var groupList: GroupList
#State private var showingAddGroupView = false
var body: some View {
NavigationView {
List(groupList.groups) { group in
NavigationLink(destination: GroupView(group: group.people)) {
Text(group.name)
}
}
.navigationBarItems(trailing:
Button(action: {
self.showingAddGroupView.toggle()
}) {
Text("Add group")
})
}
.sheet(isPresented: $showingAddGroupView) {
AddGroupView(groupList: self.groupList)
}
}
}
AddGroupView.swift
import SwiftUI
struct AddGroupView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var groupList: GroupList
#State private var name = ""
var body: some View {
VStack {
TextField("Name", text: self.$name)
Button(action: {
self.groupList.groups.append(Group(name: self.name))
self.presentationMode.wrappedValue.dismiss()
}) {
Text("OK")
}
}
}
}
GroupView.swift
import SwiftUI
struct GroupView: View {
var group: [Person]
#State private var showingAddPersonView = false
var body: some View {
NavigationView {
VStack {
List(group) { person in
NavigationLink(destination: Text(person.firstName)) {
Text("\(person.firstName) \(person.lastName)")
}
}
.sheet(isPresented: $showingAddPersonView) {
AddPersonView(group: self.group)
}
}
.navigationBarItems(trailing:
Button(action: {
self.showingAddPersonView.toggle()
}) {
Text("Add person")
})
}
}
}
AddPersonView.swift
import SwiftUI
struct AddPersonView: View {
#Environment(\.presentationMode) var presentationMode
#State var group: [Person]
#State private var firstName = ""
#State private var lastName = ""
var body: some View {
VStack {
TextField("First name", text: self.$firstName)
TextField("Last name", text: self.$lastName)
Button(action: {
self.group.append(Person(id: UUID(), firstName: self.firstName, lastName: self.lastName))
self.presentationMode.wrappedValue.dismiss()
}) {
Text("OK")
}
}
}
}
check this out:
the problem is exactly there what Lou said - structs will be copied. you must change and work on your observable object - not on copies.
import SwiftUI
import Foundation
struct Group: Identifiable {
var id: UUID
var name: String
var people: [Person]
init(name: String) {
self.id = UUID()
self.name = name
self.people = [Person]()
}
}
struct Person: Identifiable {
var id: UUID
var firstName: String
var lastName: String
}
class GroupList: ObservableObject {
#Published var groups = [Group]()
func getGroupBy(id: UUID) -> Group? {
let result = groups.filter { $0.id == id }
if result.count == 1 {
return result[0]
}
return nil
}
func getGroupIndex(id: UUID) -> Int? {
return groups.firstIndex { $0.id == id }
}
}
struct ContentView: View {
#EnvironmentObject var groupList: GroupList
#State private var showingAddGroupView = false
var body: some View {
NavigationView {
List(self.groupList.groups) { group in
NavigationLink(destination: GroupView(group: group).environmentObject(self.groupList)) {
Text(group.name)
}
}
.navigationBarItems(trailing:
Button(action: {
self.showingAddGroupView.toggle()
}) {
Text("Add group")
})
}
.sheet(isPresented: $showingAddGroupView) {
AddGroupView().environmentObject(self.groupList)
}
}
}
struct AddGroupView: View {
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var groupList: GroupList
#State private var name = ""
var body: some View {
VStack {
TextField("Name", text: self.$name)
Button(action: {
self.groupList.groups.append(Group(name: self.name))
self.presentationMode.wrappedValue.dismiss()
}) {
Text("OK")
}
}
}
}
struct GroupView: View {
#EnvironmentObject var groupList: GroupList
var group: Group
#State private var showingAddPersonView = false
var body: some View {
NavigationView {
VStack {
List(self.group.people) { person in
NavigationLink(destination: Text(person.firstName)) {
Text("\(person.firstName) \(person.lastName)")
}
}
.sheet(isPresented: $showingAddPersonView) {
AddPersonView(group: self.group).environmentObject(self.groupList)
}
}
.navigationBarItems(trailing:
Button(action: {
self.showingAddPersonView.toggle()
}) {
Text("Add person")
})
}
}
}
struct AddPersonView: View {
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var groupList : GroupList
#State var group: Group
#State private var firstName = ""
#State private var lastName = ""
var body: some View {
VStack {
TextField("First name", text: self.$firstName)
TextField("Last name", text: self.$lastName)
Button(action: {
if let index = self.groupList.getGroupIndex(id: self.group.id) {
self.groupList.groups[index].people.append(Person(id: UUID(), firstName: self.firstName, lastName: self.lastName))
self.group = self.groupList.groups[index]
}
self.presentationMode.wrappedValue.dismiss()
}) {
Text("OK")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(GroupList())
}
}
The issue is in GroupView, with this line
var group: [Person]
When you call AddPersonView, you are sending a copy of the array and the view appends a person to that copy and then it is lost when the view dismisses.
You must pass something that is shared. Probably this should all be in an ObservableObject and not local view variables.
Pass down the GroupList object or bindings to its internals

Resources