Difficulty Updating String With a TextField in SwiftUI - swift

I am currently having trouble modifying a String value using a TextField. Here is my (simplified) code so far:
class GradeItem: ObservableObject {
#Published var name: String
#Published var scoredPoints: Double
#Published var totalPoints: Double
let isUserCreated: Bool
init(name: String, scoredPoints: Double, totalPoints: Double, isUserCreated: Bool) {
self.name = name
self.scoredPoints = scoredPoints
self.totalPoints = totalPoints
self.isUserCreated = isUserCreated
}
}
var courses: [Course] {
// initialization code...
}
struct GradeCalculatorView: View {
#State var selectedCourseIndex: Int = 0
var body: some View {
VStack {
// allows user to select a course:
ForEach(0 ..< courses.count) { i in
Button(action: {
self.selectedCourseIndex = i
}, label: {
Text(courses[i].name)
})
}
CourseView(course: courses[selectedCourseIndex])
}
}
}
struct CourseView: View {
#ObservedObject var course: Course // passed in from GradeCalculatorView
var body: some View {
VStack(alignment: .leading) {
Text(course.name)
ForEach(course.categories, id: \.name) { category in
GradeCategoryView(category: category)
}
}.padding(.leading).frame(alignment: .leading)
}
}
struct GradeCategoryView: View {
#ObservedObject var category: GradeCategory // passed in from CourseView
var body: some View {
VStack(alignment: HorizontalAlignment.leading) {
HStack {
Text(category.name)
Spacer()
}
ForEach(category.items, id:\.name) { item in
GradeItemRow(item: item)
}
}
}
}
struct GradeItemRow: View {
#ObservedObject var item: GradeItem // passed in from GradeCategoryView
var body: some View {
TextField("Item Name", text: $item.name)
}
}
I cannot seem to modify the GradeItem object's name using the TextField. When the TextField is edited, its text changes temporarily. However, when the GradeItemRow View is reloaded, it displays the GradeItem object's original name, rather than its updated name.
Would somebody please be able to help?
Thanks in advance
UPDATE: As per your requests, I have added more context to this sample code.
I know that this does not work, as when I attempt to modify a GradeItem's name with a TextField, it changes temporarily. However, when I select a different course and then the course I was initially on, the TextField displays the unmodified name value.

The following test works.
class GradeItem: ObservableObject {
#Published var name: String
#Published var scoredPoints: Double
#Published var totalPoints: Double
let isUserCreated: Bool
init(name: String, scoredPoints: Double, totalPoints: Double, isUserCreated: Bool) {
self.name = name
self.scoredPoints = scoredPoints
self.totalPoints = totalPoints
self.isUserCreated = isUserCreated
}
init() {
self.name = "gradeItem" + UUID().uuidString
self.scoredPoints = 0.0
self.totalPoints = 0.0
self.isUserCreated = false
}
}
class Course: ObservableObject {
#Published var name: String
#Published var categories: [GradeCategory]
init(name: String, categories: [GradeCategory]) {
self.name = name
self.categories = categories
}
init() {
self.name = "course_" + UUID().uuidString
self.categories = [GradeCategory]()
self.categories.append(GradeCategory())
}
}
class GradeCategory: ObservableObject {
#Published var name: String
#Published var items: [GradeItem]
init(name: String, items: [GradeItem]) {
self.name = name
self.items = items
}
init() {
self.name = "category_" + UUID().uuidString
self.items = [GradeItem]()
self.items.append(GradeItem())
}
}
struct GradeItemRow: View {
#ObservedObject var item: GradeItem // passed in from GradeCategoryView
var body: some View {
TextField("Item Name", text: $item.name).textFieldStyle(RoundedBorderTextFieldStyle())
}
}
struct GradeCategoryView: View {
#ObservedObject var category: GradeCategory // passed in from CourseView
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(category.name)
Spacer()
}
ForEach(category.items, id: \.name) { item in
GradeItemRow(item: item)
}
}
}
}
struct CourseView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#ObservedObject var course: Course // passed in from ContentView
var body: some View {
VStack(alignment: .leading) {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("done")
})
Spacer()
Text(course.name)
ForEach(course.categories, id: \.name) { category in
GradeCategoryView(category: category)
}
Spacer()
}.padding(.leading).frame(alignment: .leading)
}
}
struct ContentView: View {
#State var courses: [Course] = [Course(), Course()]
#State var selectedCourseIndex: Int = 0
#State var showCourse = false
var body: some View {
VStack {
ForEach(0 ..< courses.count) { i in
Button(action: {
self.selectedCourseIndex = i
self.showCourse = true
}, label: {
Text(self.courses[i].name)
})
}
}.sheet(isPresented: self.$showCourse) {
CourseView(course: self.courses[self.selectedCourseIndex])
}
}
}

Related

Create list with NavigationLink items in SwiftUI where each NavigationLink again contains a list of items which is individual

[Pic 1 AS IS]
[Pic 2 TO BE]
Hi there,
I am just starting to learn Swift an I would like my app users to build their own list of items (first level) where each item again contains a list of items (second level). Important is that each of the individually created lists in the second level is like no other of the individually created lists. (see picture)
Is anyone aware of which approach I need to take to solve this?
I am myself able to build the list within the list within the NavigationView, but how can I make each list individual?
Here is my code:
struct ItemModel: Hashable {
let name: String
}
struct ProductModel: Hashable {
let productname: String
}
class ListViewModel: ObservableObject {
#Published var items: [ItemModel] = []
}
class ProductlistViewModel: ObservableObject {
#Published var products: [ProductModel] = []
}
struct ContentView: View {
#StateObject private var vm = ListViewModel()
#StateObject private var pvm = ProductlistViewModel()
#State var firstPlusButtonPressed: Bool = false
#State var secondPlusButtonPressed: Bool = false
var body: some View {
NavigationView {
List {
ForEach(vm.items, id: \.self) { item in
NavigationLink {
DetailView() //The DetailView below
.navigationTitle(item.name)
.navigationBarItems(
trailing:
Button(action: {
secondPlusButtonPressed.toggle()
}, label: {
NavigationLink {
AddProduct() //AddProduct below
} label: {
Image(systemName: "plus")
}
})
)
} label: {
Text(item.name)
}
}
}
.navigationBarItems(
trailing:
Button(action: { firstPlusButtonPressed.toggle()
}, label: {
NavigationLink {
AddItem() //AddItem below
} label: { Image(systemName: "plus")
}
})
)
}
.environmentObject(vm)
.environmentObject(pvm)
}
}
struct AddItem: View {
#State var textFieldText: String = ""
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var vm: ListViewModel
var body: some View {
NavigationView {
VStack {
TextField("Add an item...", text: $textFieldText)
Button(action: {
vm.addItem(text: textFieldText)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("SAVE")
})
}
}
}
}
struct DetailView: View {
#StateObject private var pvm = ProductlistViewModel()
#Environment(\.editMode) var editMode
var body: some View {
NavigationView {
List {
ForEach(pvm.products, id: \.self) { product in
Text(product.productname)
}
}
}
.environmentObject(pvm)
}
}
struct AddProduct: View {
#State var textFieldText: String = ""
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var pvm: ProductlistViewModel
var body: some View {
NavigationView {
VStack {
TextField("Add a product", text: $textFieldText)
Button(action: {
pvm.addProduct(text: textFieldText)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("SAVE")
})
}
}
}
}
This is going to be long but here it goes. The issue is the whole ViewModel setup. You detail view now is only using the product view model, you need to rethink your approach.
But what makes the whole thing "complicated" is the 2 different types, Item and Product which you seem to want to combine into one list and use the same subviews for them both.
In swift you have protocol that allows this, protocols require struct and class "conformance".
//Protocols are needed so you can use reuse views
protocol ObjectModelProtocol: Hashable, Identifiable{
var id: UUID {get}
var name: String {get set}
init(name: String)
}
//Protocols are needed so you can use reuse views
protocol ListModelProtocol: Hashable, Identifiable{
associatedtype O : ObjectModelProtocol
var id: UUID {get}
var name: String {get set}
//Keep the individual items with the list
var items: [O] {get set}
init(name: String, items: [O])
}
extension ListModelProtocol{
mutating func addItem(name: String) {
items.append(O(name: name))
}
}
Then your models start looking something like this. Notice the conformance to the protocols.
//Replaces the ListViewModel
struct ListItemModel: ListModelProtocol{
let id: UUID
var name: String
var items: [ItemModel]
init(name: String, items: [ItemModel]){
self.id = .init()
self.name = name
self.items = items
}
}
//Replaces the ProductlistViewModel
struct ListProductModel: ListModelProtocol{
let id: UUID
var name: String
var items: [ProductModel]
init(name: String, items: [ProductModel]){
self.id = .init()
self.name = name
self.items = items
}
}
//Uniform objects, can be specialized but start uniform
struct ItemModel: ObjectModelProtocol {
let id: UUID
var name: String
init(name: String){
self.id = .init()
self.name = name
}
}
//Uniform objects, can be specialized but start uniform
struct ProductModel: ObjectModelProtocol {
let id: UUID
var name: String
init(name: String){
self.id = .init()
self.name = name
}
}
class ModelStore: ObservableObject{
#Published var items: [ListItemModel] = [ListItemModel(name: "fruits", items: [.init(name: "peach"), .init(name: "banana"), .init(name: "apple")])]
#Published var products: [ListProductModel] = [ListProductModel(name: "vegetable", items: [.init(name: "tomatoes"), .init(name: "paprika"), .init(name: "zucchini")])]
}
Now your views can look something like this
struct ComboView: View {
#StateObject private var store = ModelStore()
#State var firstPlusButtonPressed: Bool = false
#State var secondPlusButtonPressed: Bool = false
var body: some View {
NavigationView {
List {
//The next part will address this
ItemLoop(list: $store.items)
ItemLoop(list: $store.products)
}
.toolbar(content: {
ToolbarItem {
AddList(store: store)
}
})
}
}
}
struct ItemLoop<LM: ListModelProtocol>: View {
#Binding var list: [LM]
var body: some View{
ForEach($list, id: \.id) { $item in
NavigationLink {
DetailView<LM>(itemList: $item)
.navigationTitle(item.name)
.toolbar {
NavigationLink {
AddItem<LM>( item: $item)
} label: {
Image(systemName: "plus")
}
}
} label: {
Text(item.name)
}
}
}
}
struct AddList: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var store: ModelStore
var body: some View {
Menu {
Button("add item"){
store.items.append(ListItemModel(name: "new item", items: []))
}
Button("add product"){
store.products.append(ListProductModel(name: "new product", items: []))
}
} label: {
Image(systemName: "plus")
}
}
}
struct AddItem<LM>: View where LM : ListModelProtocol {
#State var textFieldText: String = ""
#Environment(\.presentationMode) var presentationMode
#Binding var item: LM
var body: some View {
VStack {
TextField("Add an item...", text: $textFieldText)
Button(action: {
item.addItem(name: textFieldText)
presentationMode.wrappedValue.dismiss()
}, label: {
Text("SAVE")
})
}
}
}
struct DetailView<LM>: View where LM : ListModelProtocol{
#Environment(\.editMode) var editMode
#Binding var itemList: LM
var body: some View {
VStack{
TextField("name", text: $itemList.name)
.textFieldStyle(.roundedBorder)
List (itemList.items, id:\.id) { item in
Text(item.name)
}
}
.navigationTitle(itemList.name)
.toolbar {
NavigationLink {
AddItem(item: $itemList)
} label: {
Image(systemName: "plus")
}
}
}
}
If you notice the List in the ComboView you will notice that the items and products are separated into 2 loop. That is because SwiftUI requires concrete types for most views, view modifiers and wrappers.
You can have a list of [any ListModelProtocol] but at some point you will have to convert from an existential to a concrete type. In your case the ForEach in de DetailView requires a concrete type.
class ModelStore: ObservableObject{
#Published var both: [any ListModelProtocol] = [
ListProductModel(name: "vegetable", items: [.init(name: "tomatoes"), .init(name: "paprika"), .init(name: "zucchini")]),
ListItemModel(name: "fruits", items: [.init(name: "peach"), .init(name: "banana"), .init(name: "apple")])
]
}
struct ComboView: View {
#StateObject private var store = ModelStore()
#State var firstPlusButtonPressed: Bool = false
#State var secondPlusButtonPressed: Bool = false
var body: some View {
NavigationView {
List {
ConcreteItemLoop(list: $store.both)
}
.toolbar(content: {
ToolbarItem {
AddList(store: store)
}
})
}
}
}
struct ConcreteItemLoop: View {
#Binding var list: [any ListModelProtocol]
var body: some View{
ForEach($list, id: \.id) { $item in
NavigationLink {
if let concrete: Binding<ListItemModel> = getConcrete(existential: $item){
DetailView(itemList: concrete)
} else if let concrete: Binding<ListProductModel> = getConcrete(existential: $item){
DetailView(itemList: concrete)
}else{
Text("unknown type")
}
} label: {
Text(item.name)
}
}
}
func getConcrete<T>(existential: Binding<any ListModelProtocol>) -> Binding<T>? where T : ListModelProtocol{
if existential.wrappedValue is T{
return Binding {
existential.wrappedValue as! T
} set: { newValue in
existential.wrappedValue = newValue
}
}else{
return nil
}
}
}
struct AddList: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var store: ModelStore
var body: some View {
Menu {
Button("add item"){
store.both.append(ListItemModel(name: "new item", items: []))
}
Button("add product"){
store.both.append(ListProductModel(name: "new product", items: []))
}
} label: {
Image(systemName: "plus")
}
}
}
I know its long but this all compiles so you should be able to put it in a project and disect it.
Also, at the end of all of this you can create specific views for the model type.
struct DetailView<LM>: View where LM : ListModelProtocol{
#Environment(\.editMode) var editMode
#Binding var itemList: LM
var body: some View {
VStack{
TextField("name", text: $itemList.name)
.textFieldStyle(.roundedBorder)
List (itemList.items, id:\.id) { item in
VStack{
switch item{
case let i as ItemModel:
ItemModelView(item: i)
case let p as ProductModel:
Text("\(p.name) is product")
default:
Text("\(item.name) is unknown")
}
}
}
}
.navigationTitle(itemList.name)
.toolbar {
NavigationLink {
AddItem(item: $itemList)
} label: {
Image(systemName: "plus")
}
}
}
}
struct ItemModelView: View{
let item: ItemModel
var body: some View{
VStack{
Text("\(item.name) is item")
Image(systemName: "person")
}
}
}

SWIFTUI: Why adding #State to property doesn't update data model

having a problem with updating data in ObservedObject
here's the data MRE:
struct Property: Identifiable, Codable, Hashable {
var id = UUID()
var name : String = ""
var meters : [Meter] = [Meter(name: "electricity"), Meter(name: "water")]
}
struct Meter: Identifiable, Hashable, Codable {
var id = UUID()
var name : String = ""
}
class PropertyData: ObservableObject {
#Published var properties: [Property]
func save() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(properties) {
UserDefaults.standard.set(encoded, forKey: "PropertyData")
}
}
init() {
if let properties = UserDefaults.standard.data(forKey: "PropertyData") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([Property].self, from: properties) {
self.properties = decoded
return
}
}
self.properties = [
Property(name: "Saks /1", meters: [Meter(name: "electricity")]),
Property(name: "Saks /2", meters: [Meter(name: "electricity"), Meter(name: "water")]),
]
}
}
import SwiftUI
struct ContentView: View {
#ObservedObject var data = PropertyData()
var body: some View {
NavigationView {
List {
ForEach(data.properties, id:\.self) {property in
NavigationLink(destination: NavigationLinkView(data: data, property: property)) {
Text(property.name)
}
}
}
}
}
}
struct NavigationLinkView: View {
#ObservedObject var data : PropertyData
var property:Property
var body: some View {
TabView {
MetersView(data: data, property: property)
.tabItem{
VStack {
Image(systemName: "scroll")
Text("Utitity")
}
}
}
}
}
struct MetersView: View {
#ObservedObject var data: PropertyData
#State private var metersPage = 0
#State var property : Property
#State private var addMeters = false
var body: some View {
VStack {
HStack {
Picker("Meters", selection: $metersPage) {
ForEach (0 ..< property.meters.count, id:\.self) {index in
Text(property.meters[index].name)
}
}
.pickerStyle(SegmentedPickerStyle())
Button{
print(property.meters)
addMeters.toggle()
} label: {
Image(systemName: "gear")
}
}.padding()
Spacer()
}.sheet(isPresented: $addMeters){AddMetersView(data: data, property: $property)}
}
}
struct AddMetersView: View {
#ObservedObject var data : PropertyData
#Binding var property : Property
#State var newMeter: String = ""
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Form{
Section {
TextField("Add another meter", text: $newMeter)
.autocapitalization(.none)
Button{
if newMeter != "" {
property.meters.append(Meter(name: newMeter))
data.save()
print(property.meters.count)
} } label: {
Text("Add a meter")
}
}
ForEach(0..<property.meters.count, id:\ .self) {index in
Text(property.meters[index].name)
}
Section() {
Button("That's enough"){
print(property.meters)
presentationMode.wrappedValue.dismiss()}
}
}
}
}
}
I cannot understand, why the meters, that I add on AddMetersView, do update the Meters page, but then just goes away as soon as I go to the ContentView.
Plus, in my app, if I add a property, it does stay, and it persists, but not the Meters
This is similar to your previous question. Change #State var property : Property to
It is all about the connections. You break them in a few places along the way I made some changes and put comments along the line.
import SwiftUI
struct MetersParentView: View {
//Change to StateObject
#StateObject var data = PropertyData()
var body: some View {
NavigationView {
List {
ForEach($data.properties, id:\.id) {$property in
NavigationLink(destination: NavigationLinkView(data: data, property: $property)) {
Text(property.name)
}
}
}
}
}
}
struct NavigationLinkView: View {
#ObservedObject var data : PropertyData
//Make Binding there is no connection without it
#Binding var property : Property
var body: some View {
TabView {
MetersView(data: data, property: $property)
.tabItem{
VStack {
Image(systemName: "scroll")
Text("Utitity")
}
}
}
}
}
struct MetersView: View {
#ObservedObject var data: PropertyData
#State var selectedMeter: Meter = .init()
//Change to Binding
//State is a source of truth, it breaks the connection
#Binding var property : Property
#State private var addMeters = false
var body: some View {
VStack {
HStack {
Picker("Meters", selection: $selectedMeter) {
//Use object not index
ForEach(property.meters, id:\.id) {meter in
//Tag adjustment
Text(meter.name).tag(meter as Meter)
}
}.onAppear(perform: {
selectedMeter = property.meters.first ?? Meter()
})
.pickerStyle(SegmentedPickerStyle())
Button{
print(property.meters)
addMeters.toggle()
} label: {
Image(systemName: "gear")
}
}.padding()
Spacer()
}.sheet(isPresented: $addMeters){
AddMetersView(data: data, property: $property)
}
}
}
struct AddMetersView: View {
#ObservedObject var data : PropertyData
#Binding var property : Property
#State var newMeter: String = ""
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Form{
Section {
TextField("Add another meter", text: $newMeter)
.autocapitalization(.none)
Button{
if newMeter != "" {
print(property.meters.count)
property.meters.append(Meter(name: newMeter))
print(property.meters.count)
data.save()
print(property.meters.count)
}
} label: {
Text("Add a meter")
}
}
//Dont use index
ForEach(property.meters, id:\ .id) {meter in
Text(meter.name)
}
Section() {
Button("That's enough"){
print(property.meters)
presentationMode.wrappedValue.dismiss()}
}
}
}
}
}
struct MetersParentView_Previews: PreviewProvider {
static var previews: some View {
MetersParentView()
}
}

SwiftUI .onTapGesture issue in TableView with Sections and Rows

I have two models:
struct Category: Identifiable {
var id = UUID()
var title: String
var number: Int
var items: [ChecklistItem]
}
and:
struct ChecklistItem: Identifiable {
let id = UUID()
var name: String
var isChecked = false
}
with:
class Checklist: ObservableObject {
#Published var items = [Category]()
func deleteListItem(whichElement: IndexSet) {
items.remove(atOffsets: whichElement)
}
func moveListItem(whichElement: IndexSet, destination: Int) {
items.move(fromOffsets: whichElement, toOffset: destination)
}
}
I try to implement tap on row to check and uncheck cheklist item in tableView with sections and rows, but I cannot get how this can be released. My code:
struct ChecklistView: View {
#EnvironmentObject var checklist: Checklist
#State var newChecklistItemViewIsVisible = false
var body: some View {
NavigationView {
List {
ForEach(checklist.items) { category in
Section(header: Text(category.title)) {
ForEach(category.items) { item in
HStack {
Text(item.name)
Spacer()
Text(item.isChecked ? "✅" : "🔲")
}
.background(Color.white)
.onTapGesture {
if let matchingIndex =
category.items.firstIndex(where: { $0.id == item.id }) {
category.items[matchingIndex].isChecked.toggle()
}
}
}
}
}
.onDelete(perform: checklist.deleteListItem)
.onMove(perform: checklist.moveListItem)
}
.navigationBarItems(
leading: Button(action: { self.newChecklistItemViewIsVisible = true }) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add")
}
},
trailing: EditButton()
)
.navigationBarTitle("List")
}
.sheet(isPresented: $newChecklistItemViewIsVisible) {
NewChecklistItemView(checklist: self.checklist)
}
}
}
I get error with this code on line with category.items[matchingIndex].isChecked.toggle():
Cannot use mutating member on immutable value: 'category' is a 'let' constant
How I can get to ChecklistItem and make it check and uncheck on tap.
import SwiftUI
//Change to class and add NSObject structs are immutable
class Category: NSObject, Identifiable {
let id = UUID()
var title: String
var number: Int
var items: [ChecklistItem]
//Now you need an init
init(title: String , number: Int, items: [ChecklistItem]) {
self.title = title
self.number = number
self.items = items
}
}
//Change to class and add NSObject structs are immutable
class ChecklistItem: Identifiable {
let id = UUID()
var name: String
var isChecked: Bool = false
//Now you need an init
init(name: String) {
self.name = name
}
}
class Checklist: ObservableObject {
#Published var items = [Category]()
}
struct ChecklistView: View {
//Can be an #EnvironmentObject if the #ObservedObject comes from a higher View
#ObservedObject var checklist: Checklist = Checklist()
#State var newChecklistItemViewIsVisible = false
var body: some View {
NavigationView {
List {
ForEach(checklist.items) { category in
Section(header: Text(category.title)) {
ForEach(category.items) { item in
Button(action: {
print(item.isChecked.description)
item.isChecked.toggle()
//Something to trigger the view to refresh will not be necessary if using something like #FetchRequest or after you somehow notify `checklist.items` that there is a change
checklist.objectWillChange.send()
}) {
HStack {
Text(item.name)
Spacer()
Text(item.isChecked ? "✅" : "🔲")
}//HStack
//White is incompatible with Text Color in Dark Mode
.background(Color.gray)
}//Button
}//ForEach
}//Section
}//ForEach
//Methods not provided
//.onDelete(perform: checklist.deleteListItem)
//.onMove(perform: checklist.moveListItem)
}
.navigationBarItems(
leading: Button(action: {
self.newChecklistItemViewIsVisible = true
//Code to Add Samples
checklist.items.append(Category(title: "Test", number: Int.random(in: 0...100), items: [ChecklistItem(name: "Test")]))
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add")
}
},
trailing: EditButton()
)
.navigationBarTitle("List")
}
.sheet(isPresented: $newChecklistItemViewIsVisible) {
//Pass as an #EnvironmentObject
NewChecklistItemView().environmentObject(checklist)
}
}
}
struct NewChecklistItemView: View {
#EnvironmentObject var checklist: Checklist
var body: some View {
Text(checklist.items.count.description)
}
}
struct ChecklistView_Previews: PreviewProvider {
static var previews: some View {
//When the #ObservedObject comes from a higher View remove comment below
ChecklistView()//.environmentObject(Checklist())
}
}
The reason you are getting that error is because structs are immutable. You should use method marked with "mutating" inside desired struct. Something like
if let matchingIndex = category.items.firstIndex(where: { $0.id == item.id }) {
category.items[matchingIndex].toggleItem()
}
and inside your struct:
mutating func toggleItem() {
self.isChecked.toggle()
}
But i would recommend you to use #State instead, because what you are trying to do is straight forward related to how you represent your view. And later, when user is willing to do something with that selection you send that data to your model

How do i change a StateObject Array in a forEach?

I have a StateObject that is being initialized, it contains an Array of "Activity" which itself contains a property name that holds a string. When i iterate the array in a forEach i try to change the name of the Activity in another view yet the change doesn't show in the List. Here is the code:
struct ContentView: View {
#StateObject var store = ActivityStore()
#State var showAdd = false
var body: some View {
NavigationView {
VStack {
if store.Activities.isEmpty {
Text("Please Add an Activity")
} else {
List {
Section(header: Text("Activities: ") ) {
ForEach(store.Activities) { activity in
NavigationLink( destination: DetailView(activity: activity))
{
Text(verbatim: activity.name)
}
}
}
}
}
}
.navigationTitle("ITrack")
.navigationBarItems(trailing: Button(action: { showAdd = true }) {
Text("Add")
})
.sheet(isPresented: $showAdd) {
AddView(store: store)
}
}
}
}
struct DetailView: View {
#Binding var activity: Activity
var body: some View {
VStack {
TextField("name", text: $activity.name )
Text(activity.description)
}
}
}
struct Activity: Identifiable {
var id = UUID()
var name: String
var description: String
var log: [String] = []
init(name: String, description: String) {
self.name = name
self.description = description
}
}
class ActivityStore: ObservableObject {
#Published var Activities: [Activity] = []
func demo() -> [Activity] {
let activities: [Activity] = []
Activities.append(Activity(name: "br1", description: "br111111"))
Activities.append(Activity(name: "br2", description: "br222222"))
Activities.append(Activity(name: "br3", description: "br333333"))
Activities.append(Activity(name: "br4", description: "br444444"))
return activities
}
}
Here are needed updates
1)
struct Activity: Identifiable, Hashable {
ForEach(Array(store.Activities.enumerated()), id: \.1) { i, activity in
NavigationLink( destination: DetailView(activity: $store.Activities[i]))
{
Text(verbatim: activity.name)
}
}

Update a row in a list (SwiftUI)

I'm an early bird in programming so I know this question can be ridiculous from the point of view of an expert but I'm stuck in this situation from several days.
I would like to update a row by using a button "Edit" (pencil) after having used another button to store the item with a TextField.
Here's the code:
class Food: Hashable, Codable, Equatable {
var id : UUID = UUID()
var name : String
init(name: String) {
self.name = name
}
static func == (lhs: Food, rhs: Food) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
class Manager: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var shoppingChart: [Food] = []
init() {
let milk = Food(name: "Milk")
let coffee = Food(name: "Coffee")
shoppingChart.append(milk)
shoppingChart.append(coffee)
}
func newFood(name: String) {
let food = Food(name: name)
shoppingChart.insert(food, at: 0)
}
}
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
var body: some View {
VStack {
List {
ForEach(self.dm.shoppingChart, id:\.self) { food in
HStack {
Text(food.name)
Image(systemName: "pencil")
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}
struct Add: View {
#ObservedObject var dm : Manager
#State var fieldtext : String = ""
#Binding var isAddFoodOpened : Bool
var body: some View {
VStack {
TextField("Write a food", text: $fieldtext)
buttonSave
}
}
var buttonSave : some View {
Button(action: {
self.dm.newFood(name: self.fieldtext)
self.isAddFoodOpened = false
}) {
Text("Save")
}
}
}
The #ObservedObject var dm : Manager object is never initialized.
Try initialized dm in ContentView like this:
#ObservedObject var dm = Manager()
Ok, so if I understand correctly you want to update/edit a row by using a button "Edit".
This will do it:
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
#State var isEditOpened = false
#State var fieldtext : String = ""
var body: some View {
VStack {
List {
ForEach(0..<self.dm.shoppingChart.count, id:\.self) { i in
HStack {
Text(self.dm.shoppingChart[i].name)
Button(action: { self.isEditOpened.toggle() }) {
Image(systemName: "pencil")
}.sheet(isPresented: self.$isEditOpened) {
TextField(self.dm.shoppingChart[i].name, text: self.$fieldtext, onEditingChanged: { _ in
self.dm.shoppingChart[i].name = self.fieldtext
})
}
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}