SwiftUI - deletion of elements in Binding Array causes errors - arrays

I'm developing an App (using Xcode 11.3.1, target device: iPad) for our company's engineers to report on work they do. Part of the app needs to be an editable list of parts they've used.
I've replicated the mechanisms I'm trying to implement (Observed Object/#Binding etc) in a simple 'Person List' test project (full project code below).
I'm still trying to learn SWiftUI so I've probably done something stupid in my code.
The objective here is to create a dynamic list with editable fields. When the code is previewed it seems to work perfectly, however, things start to go wrong after elements are deleted. (Deleting the last element causes "Fatal error: Index out of range".
If you add new elements after deleting some, the new elements have blank textFields and are un-editable.
I would very much appreciate any help anyone can offer.
import SwiftUI
struct EditView: View {
#Binding var person:Person
var body: some View {
HStack{
Group{
TextField("name1", text: $person.name1)
TextField("name2", text: $person.name2)
}.frame(width:150)
.font(.headline)
.padding(.all, 3)
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.blue, lineWidth: 1))
}
}
}
struct Person:Identifiable, Equatable{
var id:UUID
var name1:String
var name2:String
}
class PersonList: ObservableObject {
#Published var individuals = [Person]()// Array of Person structs
}
struct ContentView: View {
#ObservedObject var people = PersonList()// people.individuals = [Person] array
#State private var edName1:String = "" //temporary storage for adding new member
#State private var edName2:String = "" //temporary storage for adding new member
var elementCount:Int{
let c = people.individuals.count
return c
}
// arrays for testing - adds random names from these (if input field '1st name' is empty)...
var firstNames = ["Nick","Hermes","John","Hattie","Nicola","Alan", "Dwight", "Richard"]
var surnames = ["Fury","Smith","Jones","Hargreaves","Bennylinch", "Davidson","Lucas","Partridge"]
var body: some View {
NavigationView{
VStack{
HStack{
Text("Add person:")
.padding(.all, 5)
.frame(alignment: .leading)
TextField("1st name", text: $edName1)
.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.blue, lineWidth: 2))
TextField("2nd name", text: $edName2)
.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.blue, lineWidth: 2))
// 🆗 Button...
Image(systemName: "plus.square")
.font(.title)
.foregroundColor(.orange)
.onTapGesture {
if self.edName1 == ""{
self.edName1 = self.firstNames.randomElement() ?? "⁉️"
self.edName2 = self.surnames.randomElement() ?? "⁉️"
}
self.people.individuals.append(Person(id: UUID(), name1: self.edName1, name2: self.edName2))
self.edName1 = ""
self.edName2 = ""
print("Element count: \(self.elementCount)")
}
Spacer()
// 🆗 Button...sort
Image(systemName: "arrow.up.arrow.down.square")
.font(.title)
.padding(.all,4)
.foregroundColor(.blue)
.onTapGesture {
self.people.individuals.sort{ // sort list alphabetically by name2
$0.name2 < $1.name2
}
}
// 🆗 Button...reverse order
Image(systemName: "arrow.uturn.up.square")
.font(.title)
.padding(.all,8)
.foregroundColor(.blue)
.onTapGesture {
self.people.individuals.reverse()
}
}.padding(.all,8)
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.orange, lineWidth: 2))
List{
ForEach(people.individuals){individual in
HStack{
EditView(person: self.$people.individuals[self.people.individuals.firstIndex(of:individual)!])
Text("\(individual.name1) \(individual.name2)")
.frame(width: 200, alignment: .leading)
.padding(.all, 3)
Text("\(self.people.individuals.firstIndex(of: individual)!)")
Spacer()
Image(systemName: "xmark.circle.fill").font(.title).foregroundColor(.red)//❌
.onTapGesture {
let deletedElement = self.people.individuals.remove(at: self.people.individuals.firstIndex(of: individual)!)
print("Deleted element:\(deletedElement) Element count: \(self.elementCount)")
}
}
}//.onDelete(perform: deleteRow)// an alternative to the red xmark circle
}
}.navigationBarTitle("People List (\(elementCount))")
}.navigationViewStyle(StackNavigationViewStyle())
}
func deleteRow(at offsets: IndexSet){
self.people.individuals.remove(atOffsets: offsets)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.colorScheme, .dark)
}
}

Thanks to KRJW and Natalia Panferova for help with different aspects of this code. There are no 'index out of range' errors now and also it is possible to delete rows without causing problems with adding items.
I'm sharing this answer because I believe it's a very useful mechanism to create editable lists.
import SwiftUI
class Person: ObservableObject, Identifiable, Equatable {
static func == (lhs: Person, rhs: Person) -> Bool {
return lhs.id == rhs.id
}
#Published var id:UUID
#Published var name1:String
#Published var name2:String
init(id: UUID, name1: String, name2: String, isEditable: Bool){
self.id = id
self.name1 = name1
self.name2 = name2
}
}
struct EditView: View {
#ObservedObject var person: Person
var body: some View {
VStack{
HStack{
Group{
TextField("name1", text: $person.name1)
TextField("name2", text: $person.name2)
}//.frame(width:200)
.font(.headline)
.padding(.all, 3)
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.blue, lineWidth: 1))
}.id(person.id)
}
}
}
struct ContentView: View {
#State var people = [Person]()//try! ObservableArray<Person>(array: []).observeChildrenChanges(Person.self)// people.individuals = [Person] array
#State private var edName1:String = "" //temporary storage for adding new member
#State private var edName2:String = "" //temporary storage for adding new member
// arrays for testing - adds random names from these (if input field '1st name' is empty)...
var firstNames = ["Nick","Hermes","John","Hattie","Nicola","Alan", "Dwight", "Richard","Turanga", "Don","Joey"]
var surnames = ["Farnsworth","Fry","Wong","Zoidberg","Conrad","McDougal","Power","Clampazzo","Brannigan","Kroker","Leela"]
var body: some View {
NavigationView{
VStack{
HStack{
Text("Add person:")
.padding(.all, 5)
.frame(alignment: .leading)
TextField("1st name", text: $edName1)
//.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.blue, lineWidth: 2))
TextField("2nd name", text: $edName2)
//.frame(width:150)
.padding(.all, 5)
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.blue, lineWidth: 2))
// 🆗 Button...
Image(systemName: "plus.circle")
.font(.largeTitle)
.foregroundColor(.orange)
.onTapGesture {
if self.edName1 == ""{
self.edName1 = self.firstNames.randomElement() ?? "⁉️"
self.edName2 = self.surnames.randomElement() ?? "⁉️"
}
self.people.append(Person(id: UUID(), name1: self.edName1, name2: self.edName2, isEditable: false))
self.edName1 = ""
self.edName2 = ""
print("Element count: \(self.people.count)")
}
Spacer()
// 🆗 Button...sort
Image(systemName: "arrow.up.arrow.down.square")
.font(.title)
.padding(.all,4)
.foregroundColor(.blue)
.onTapGesture {
self.people.sort{ // sort list alphabetically by name2
$0.name2 < $1.name2
}
}
// 🆗 Button...reverse order
Image(systemName: "arrow.uturn.up.square")
.font(.title)
.padding(.all,8)
.foregroundColor(.blue)
.onTapGesture {
self.people.reverse()
}
}.padding(.all,8)
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.orange, lineWidth: 2))
List {
ForEach(self.people) { person in
EditView(person: person)
}.onDelete(perform: deleteRow)
}
}.navigationBarTitle("People List (\(self.people.count))")
}.navigationViewStyle(StackNavigationViewStyle())
}
func deleteRow(at offsets: IndexSet){
self.people.remove(atOffsets: offsets)
print(self.people.count)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
// .environment(\.colorScheme, .dark)
}
}
The code is all bundled together for convenience. Feel free to copy and paste and play around.

Related

Problem with method. Cannot use mutating member on immutable value: 'book' is a 'let' constant

I am very new to SwiftUI (few days of studying), and when learning basics, I've encountered a problem that I can't seem to figure out.
I would like to have a method (changeBookRead()) that will toggle book variable "read" when button is clicked.
The problem is, I think, that the book is structs instance inside of an array, but I am not sure if that's correct.
I will appreciate if you can help me with solution.
Book struct with changeBookRead() method
struct ContentView: View {
#State private var title = String()
#State private var author = String()
#State private var isRead = false;
#State private var books = [Book]()
#State private var showingAlert = false
func toggleRead () {
isRead.toggle()
}
struct Book {
let ID = UUID()
let bookTitle: String
let bookAuthor: String
#State var read: Bool
// Function that changes read status of specific book instance
mutating func changeBookRead () {
read.toggle()
print(read)
}
}
func addToBooks () {
let book = Book(bookTitle: title, bookAuthor: author, read: isRead)
books.append(book)
}
Body section - last button in the ForEach loop is the one I want to use.
var body: some View {
NavigationView{
VStack{
TextField("Book title", text: $title)
.padding()
TextField("Book author", text: $author)
.padding()
Group{
Toggle(isOn: $isRead) {
Text("Did I read it...?")
}
.toggleStyle(SwitchToggleStyle(tint: .orange))
.padding()
.frame(minWidth: 100)
.animation(.default, value:isRead)
Button("Add book") {
addToBooks()
}
.buttonStyle(.bordered)
.tint(.orange)
}
.navigationTitle("My Books")
ForEach(books, id: \.ID) { book in
VStack {
Text("Book Title: \(book.bookTitle)")
Text("Book Author: \(book.bookAuthor)")
Button("Remove book", role: .destructive) {
showingAlert.toggle()
}
.alert("Do you want to remove this book?", isPresented: $showingAlert) {
Button("Remove", role: .destructive) {
books.removeAll(where: { $0.ID == book.ID})
}
}
.buttonStyle(.bordered)
Button(book.read ? "Is Read" : "Want to read") {
book.changeBookRead()
}
Divider()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}

DetailView only calls on first item in array when using LazyVGrid

I had a list that I could add items to and when I click on them it opened up the correct detail view. I recently swapped the list out for LazyVGrid but when I click on a grid item, the detail view only calls on the first item in the array.
This is the list view:
import SwiftUI
struct ListView: View {
#EnvironmentObject var listViewModel: ListViewModel
#State var addItemView = false
#State var detailView = false
let columns = [GridItem(.flexible()), GridItem(.flexible())]
var body: some View {
NavigationView {
ZStack {
ZStack {
Color(.white)
}
VStack {
Button(action: {
self.addItemView = true
}, label: {
Text("Add")
}).sheet(isPresented: $addItemView, content: {
AddItemView()
})
LazyVGrid(columns: columns, spacing: 20) { // <-- the only line i comment when using list
// List {
ForEach(listViewModel.item, id:\.id){ item in
Button(action: {
self.detailView = true
}, label: {
ListRowView(item: item)
}).sheet(isPresented: $detailView, content: {
DetailView(item: item)
})
}
// .onDelete(perform: listViewModel.deleteItem)
// .onMove(perform: listViewModel.moveItem)
// .listRowBackground(Color.blue)
}
// .listStyle(PlainListStyle())
Spacer()
}
}
}
}
}
struct ListRowView: View { // <-- using this as grid item
#State var item:Item
var body: some View{
VStack {
Text(item.name).foregroundColor(.white)
}.frame(width: 150, height: 150, alignment: .center)
.background(Color.blue)
.cornerRadius(10)
}
}
The add item and detail view:
struct AddItemView: View {
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var listViewModel: ListViewModel
#State var id = UUID()
#State var name = ""
var body: some View {
TextField("Enter Name", text: $name)
Button(action: {
addItem()
}, label: {
Text("Done")
})
}
func addItem() {
listViewModel.addItem(id: id.uuidString, name: name)
presentationMode.wrappedValue.dismiss()
}
}
struct DetailView: View {
#State var item:Item
var body: some View {
Text(item.name)
}
}
And this is how im adding each item:
import Foundation
struct Item: Hashable, Codable, Equatable {
var id:String
var name: String
}
class ListViewModel: ObservableObject {
#Published var item: [Item] = [] {
didSet {
saveItem()
}
}
let itemsKey: String = "items_key"
init() {
getItems()
}
func getItems() {
guard
let data = UserDefaults.standard.data(forKey: itemsKey),
let savedItems = try? JSONDecoder().decode([Item].self, from: data)
else { return }
self.item = savedItems
}
func deleteItem(indexSet: IndexSet){
item.remove(atOffsets: indexSet)
}
func moveItem(from: IndexSet, to: Int){
item.move(fromOffsets: from, toOffset: to)
}
func addItem(id: String, name: String){
let newItem = Item(id: id, name: name)
item.append(newItem)
print(newItem)
}
func saveItem() {
if let encodedData = try? JSONEncoder().encode(item) {
UserDefaults.standard.set(encodedData, forKey: itemsKey)
}
}
}
I'm not sure why the LazyVGrid is only calling on the first item, any help would be appreciated
The problem is here:
#State var detailView = false /// will only work for 1 sheet, not multiple...
...
ForEach(listViewModel.item, id:\.id){ item in
Button(action: {
self.detailView = true
}, label: {
ListRowView(item: item)
}).sheet(isPresented: $detailView, content: { /// not good to have `sheet` inside ForEach
DetailView(item: item)
})
}
In each iteration of the ForEach, you have a sheet. That's a lot of sheets... and when detailView is set to true, all of them will try to present. By chance, the first once gets presented.
Instead, you'll need to use sheet(item:onDismiss:content:). This alternate version of sheet is specifically made for your purpose — when you have an array of Items and want to present a sheet for that specific Item.
First, you'll need to make Item conform to Identifiable.
/// here!
struct Item: Hashable, Codable, Equatable, Identifiable {
Then, replace the old trigger, #State var detailView = false, with #State var selectedDetailItem: Item?. Also, make sure to move the sheet outside of the ForEach, so it doesn't repeat. Now, the sheet will only present when selectedDetailItem is not nil.
struct ListView: View {
#EnvironmentObject var listViewModel: ListViewModel
#State var addItemView = false
#State var selectedDetailItem: Item? /// here!
let columns = [GridItem(.flexible()), GridItem(.flexible())]
var body: some View {
NavigationView {
ZStack {
ZStack {
Color(.white)
}
VStack {
Button(action: {
self.addItemView = true
}, label: {
Text("Add")
}).sheet(isPresented: $addItemView, content: {
AddItemView()
})
LazyVGrid(columns: columns, spacing: 20) {
ForEach(listViewModel.item, id:\.id){ item in
Button(action: {
self.selectedDetailItem = item /// set the selected item
}, label: {
ListRowView(item: item)
})
}
}
Spacer()
}
}
} /// present the sheet here
.sheet(item: $selectedDetailItem) { selectedItem in
DetailView(item: selectedItem)
}
}
}
Result:

Swiftui append to a struct array doesn't work

Im learning swiftui at this moment. But now i have come across a problem.
Im trying to append data to an array that is an struct.
struct outfit:Identifiable {
var id = UUID()
var user: String
var amount: Double
var rating: Double
}
and the other file
import SwiftUI
import Firebase
import FirebaseStorage
import FirebaseFirestore
struct rate: View {
private var db = Firestore.firestore()
#State var user = Auth.auth().currentUser
#State private var outfitcards = [outfit]()
#State private var cards = [1, 2, 3]
#State private var offset = [CGSize.zero, CGSize.zero]
init () {
loadcards()
}
var body: some View {
GeometryReader { frame in
ZStack{
VStack {
Text("outfit")
.font(.largeTitle)
.padding()
ZStack {
Text("No cards to show")
.frame(width: frame.size.width * 0.6, height: frame.size.width * 1.6)
HStack {
Image(systemName: "record.circle.fill")
.foregroundColor(.red)
Spacer()
Image(systemName: "record.circle.fill")
.foregroundColor(.green)
}
ForEach(outfitcards) { index in
Text(index.user)
}
}
}
}
}
}
func loadcards () {
db.collection("rating").whereField("user", isNotEqualTo: user?.uid ?? "Error")
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err.localizedDescription)")
} else {
for document in querySnapshot!.documents {
let cuser = document.get("user") as! String
let camount = document.get("amount") as! Double
let crating = document.get("rating") as! Double
print("user=\(cuser) amount=\(camount) crating=\(crating)")
outfitcards.append(outfit(user: cuser, amount: camount, rating: crating))
}
print(outfitcards)
}
}
}
}
It does print the username, the amount and the rating but then when i print the array itself it is giving me a []. So it doens't append. Also the for each loop is empty so that also means that the array is empty
and nothing is appended
Does anyone know what I do wrong?
Loading data in a View like that is a dangerous practice and it'll get reset/reloaded any time that view is re-rendered (or even re-inited in this case).
Instead, move your code that loads the data to an ObservableObject:
struct Outfit:Identifiable {
var id = UUID()
var user: String
var amount: Double
var rating: Double
}
class Loader : ObservableObject {
private var db = Firestore.firestore()
private var user = Auth.auth().currentUser
#Published var outfitcards = [Outfit]()
func loadcards () {
db.collection("rating").whereField("user", isNotEqualTo: user?.uid ?? "Error")
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err.localizedDescription)")
} else {
for document in querySnapshot!.documents {
let cuser = document.get("user") as! String
let camount = document.get("amount") as! Double
let crating = document.get("rating") as! Double
print("user=\(cuser) amount=\(camount) crating=\(crating)")
self.outfitcards.append(Outfit(user: cuser, amount: camount, rating: crating))
}
print(self.outfitcards)
}
}
}
}
struct Rate: View {
#StateObject var loader = Loader()
#State private var cards = [1, 2, 3]
#State private var offset = [CGSize.zero, CGSize.zero]
var body: some View {
GeometryReader { frame in
ZStack{
VStack {
Text("outfit")
.font(.largeTitle)
.padding()
ZStack {
Text("No cards to show")
.frame(width: frame.size.width * 0.6, height: frame.size.width * 1.6)
HStack {
Image(systemName: "record.circle.fill")
.foregroundColor(.red)
Spacer()
Image(systemName: "record.circle.fill")
.foregroundColor(.green)
}
ForEach(loader.outfitcards) { index in
Text(index.user)
}
}
}
}
}.onAppear {
loader.loadcards()
}
}
}
Now, the loader is responsible for making the database call. It updates a #Published property, which the View observes.
Note that I changed the capitalization of a few things (Rate, Outfit). In Swift, the common practice is to capitalize types (classes/structs/enums) and start variable names with lowercase letters.
Also probably worth noting that the way that you're casting the data from firebase (with as!) is dangerous and can crash your program if the data isn't in the format you expect. Better to use optional binding (let user = document.get("user") as? String)
Your issue is there you are updating #State value in initializing View, which you are doing in Wrong way, do like this onAppear:
struct rate: View {
private var db = Firestore.firestore()
#State var user = Auth.auth().currentUser
#State private var outfitcards = [outfit]()
#State private var cards = [1, 2, 3]
#State private var offset = [CGSize.zero, CGSize.zero]
var body: some View {
GeometryReader { frame in
ZStack{
VStack {
Text("outfit")
.font(.largeTitle)
.padding()
ZStack {
Text("No cards to show")
.frame(width: frame.size.width * 0.6, height: frame.size.width * 1.6)
HStack {
Image(systemName: "record.circle.fill")
.foregroundColor(.red)
Spacer()
Image(systemName: "record.circle.fill")
.foregroundColor(.green)
}
ForEach(outfitcards) { index in
Text(index.user)
}
}
}
}
}
.onAppear() { loadcards() } // <<: Here
}
func loadcards () {
db.collection("rating").whereField("user", isNotEqualTo: user?.uid ?? "Error")
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err.localizedDescription)")
} else {
for document in querySnapshot!.documents {
let cuser = document.get("user") as! String
let camount = document.get("amount") as! Double
let crating = document.get("rating") as! Double
print("user=\(cuser) amount=\(camount) crating=\(crating)")
outfitcards.append(outfit(user: cuser, amount: camount, rating: crating))
}
print(outfitcards)
}
}
}
}

SwiftUI how to update a view when the object in a published array is updated and not the array itself

The following is a contrived example of the problem I am facing.
Situation:
I have a class PersonStore that stores class Person in an array and is Published.
The class Person creates a LegalAge model for each Person and the model is Published
Problem:
When an update to the persons age in LegalAge is performed the view for PersonStore is not updated.
I'm guessing that the reason there is no change is because the change occurs to the object within the array and not to the array itself. How do I overcome this?
import SwiftUI
enum LegalAge: Int, CaseIterable {
case UK = 18
case USA = 21
var name: String { "\(self)" }
}
struct ContentView: View {
var body: some View {
MainView()
}
}
/// Displays a list of people and their ages
struct MainView: View {
#ObservedObject var personStore = PersonStore()
#State private var addPerson = false
var body: some View {
NavigationView {
List {
ForEach(personStore.store) { person in
NavigationLink(destination: ShowAge(person: person)) {
HStack {
Text("\(person.name)")
Text("\(person.model.personsAge)")
Text("\(person.model.country)")
Image(systemName: (person.model.personsAge >= person.model.drinkingAge) ? "checkmark.shield.fill" : "xmark.shield.fill").foregroundColor((person.model.personsAge >= person.model.drinkingAge) ? Color.green : Color.red)
}
}
}
}
.navigationBarTitle(Text("Can I Drink?"))
.navigationBarItems(leading: Button(action: { addPerson = true }) { Image(systemName: "plus") }
)
}
/// Open a sheet to create a new person.
.sheet(isPresented: $addPerson) { AddPerson().environmentObject(personStore) }
}
}
/// A Person store to hold all the people.
class PersonStore: ObservableObject {
#Published var store = [Person]()
func addPerson(person: Person) {
store.append(person)
}
func getPerson(name: String) -> Person? {
if let nameAtIndex = store.firstIndex(where: {$0.name == name}) {
return store[nameAtIndex]
}
return nil
}
func insertPerson(person: Person) {
store.append(person)
}
}
/// Form to allow adding people.
struct AddPerson: View {
#EnvironmentObject var personStore: PersonStore
#State private var name: String = ""
#State private var age: String = ""
#State private var country: LegalAge = LegalAge.UK
var body: some View {
NavigationView {
Form {
TextField("Name", text: $name)
TextField("Age", text: $age)
Picker(selection: $country, label: Text("Country")) {
ForEach(LegalAge.allCases, id: \.self) { c in
Text("\(c.name)").tag(c)
}
}
Section {
Button(action: { personStore.addPerson(person: Person(name: name, age: Int(age) ?? 18, country: country))}, label: { Text("Add person" )})
}
}
}
}
}
/// View to show peoples details and allow some of these details to be edited.
struct ShowAge: View {
#ObservedObject var person: Person
#State private var showAgeEditor = false
var body: some View {
VStack {
/// Show the current age.
Text("\(self.person.name)")
Text("\(self.person.model.personsAge)")
Text("\(self.person.model.country)")
Image(systemName: "keyboard")
.onTapGesture {
self.showAgeEditor = true
}
/// Present the sheet to update the age.
.sheet(isPresented: $showAgeEditor) {
SheetView(showAgeEditor: self.$showAgeEditor)
.environmentObject(self.person)
.frame(minWidth: 300, minHeight: 400)
}
}
}
}
/// Sheet to allow editing of persons details.
struct SheetView: View {
#EnvironmentObject var person: Person
#Binding var showAgeEditor: Bool
let maxAge = 21
let minAge = 16
var body: some View {
return VStack(alignment: .leading) {
Text("Name: \(person.name)")
Stepper(value: $person.model.personsAge, in: minAge...maxAge, step: 1) {
Text("Age: \(person.model.personsAge)")
}
Text("Country: \(person.model.country)")
}
}
}
/// ViewModel that creates the Person Object to stored.
class Person: ObservableObject, Identifiable {
#Published var model: LegalDrinkingAge
var name: String
var id = UUID()
init(name: String, age:Int, country: LegalAge) {
self.name = name
self.model = Person.createLegalAge(drinkingAge: country.rawValue, country: "\(country)", personsAge: age)
}
private static func createLegalAge(drinkingAge: Int, country: String, personsAge: Int) -> LegalDrinkingAge {
LegalDrinkingAge(drinkingAge: drinkingAge, country: country, personsAge: personsAge)
}
func updateAge(_ age: Int) {
model.personsAge = age
}
}
struct LegalDrinkingAge {
var drinkingAge: Int = 0
var country: String = ""
var personsAge: Int = 0
mutating func setDrinkingAge(age: Int, country: String, personsAge: Int) {
drinkingAge = age
self.country = country
self.personsAge = personsAge
}
}
Separate view and make it observed for person
class StorePersonRowView: View {
#ObservedObject person: Person
var body: some View {
HStack {
Text("\(person.name)")
Text("\(person.model.personsAge)")
Text("\(person.model.country)")
Image(systemName: (person.model.personsAge >= person.model.drinkingAge) ? "checkmark.shield.fill" : "xmark.shield.fill").foregroundColor((person.model.personsAge >= person.model.drinkingAge) ? Color.green : Color.red)
}
}
}
and use it in link
ForEach(personStore.store) { person in
NavigationLink(destination: ShowAge(person: person)) {
StorePersonRowView(person: person)
}
}

Unable to reflect my added items in a list

My code runs and compiles correctly, but I am unable to see any items that have been added to the list. I know that they are part of the vitallist array as I can print them and see that they have been added, but I cannot work out why they aren't showing in the ListView. This might be related to ObservableObject Protocol.
struct Vital: Identifiable {
let id = UUID()
var name: String
}
class VitalList:ObservableObject {
#Published var vitallist = [Vital]()
}
struct ItemAdd: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var items1: VitalList
#State private var item = ""
#State var itemvalue: Int = 0
var body: some View {
NavigationView{
HStack{
TextField("Item", text: $item)
.padding()
Button(action:{
if self.itemvalue == 1{
let things1 = Vital(name: self.item)
self.items1.vitallist.append(things1)
self.presentationMode.wrappedValue.dismiss()
}
else{
let things4 = Delegate(name: self.item)
self.items4.delegatelist.append(things4)
self.presentationMode.wrappedValue.dismiss()
}
}){
Image(systemName:"paperplane")
.font(.headline)
.padding()
}
}
}
}
struct Row: View {
var vital: Vital
#State var completed:Bool = false
var body: some View {
HStack{
Image(systemName: completed ? "checkmark.circle.fill" : "circle").onTapGesture {
self.completed.toggle()
}
Text(vital.name)
}
}
}
struct Lists: View {
#ObservedObject var vitallist = VitalList()
var body: some View {
NavigationView{
List{
Section(header: Text("Vital")){
ForEach(vitallist.vitallist){ item in
Row(vital: item)
}
}
}.navigationBarTitle("ToDo")
}
}
}
struct ContentView: View {
#ObservedObject var vital = VitalList()
#State private var showingAdditem: Bool = false
var body: some View {
ZStack{
Lists()
VStack{
Spacer()
HStack{
Spacer()
Button(action:{
self.showingAdditem = true
}){
Image(systemName: "plus")
.font(Font.system(size: 25,weight: .regular))
.font(.largeTitle)
.frame( width: 50, height: 50)
.foregroundColor(.white)
}.sheet(isPresented: $showingAdditem)
{
ItemAdd(items1: self.vital, items2: self.urgent, items3: self.important, items4: self.delegate)
}.background(Color.blue)
.cornerRadius(25)
.shadow(color: Color.black.opacity(0.3), radius: 3,
x: 3,
y: 3)
.padding()
.offset(x: -5,
y: 15)
}
}
}
}
}

Resources