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
Related
[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")
}
}
}
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()
}
}
I tried to create a list of editable objects in SwiftUI. Here is my idea.
First of all, the editable item is as follows:
struct Item: Identifiable {
var id: UUID
var ItemNum: Int
var notes: String = ""
}
final class ItemStore: ObservableObject {
#Published var items: [Item] = [
.init(id: .init(), ItemNum: 55),
.init(id: .init(), ItemNum: 57),
.init(id: .init(), ItemNum: 87)
]
}
After that I created a list that get data from the ItemStore:
struct ItemView: View {
#State private var editMode = EditMode.inactive
#ObservedObject var store: ItemStore
var body: some View {
NavigationView {
List {
ForEach(store.items.indexed(), id:\.1.id) {index, item in
NavigationLink(destination: ItemEditingView(item: self.$store.items[index])) {
VStack(alignment: .leading) {
Text("Item Num: \(item.itemNum)")
}
}
}
}
//.onAppear(perform: store.fetch) // want to fetch the data from the store whenever the list appear, however, no idea to perform the function?!
.navigationBarTitle("Items")
.navigationBarItems( trailing: addButton)
.environment(\.editMode, $editMode)
}
}
private var addButton: some View {
switch editMode {
case .inactive:
return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
default:
return AnyView(EmptyView())
}
}
private func onAdd() {
store.items.append(Item(id: UUID(), itemNum: 10))
}
}
The editView:
struct ItemEditingView: View {
#Environment(\.presentationMode) var presentation
#Binding var item: Item
var body: some View {
Form {
Section(header: Text("Item")) {
Text(Text("Item Num: \(item.itemNum)"))
TextField("Type something...", text: $item.notes)
}
Section {
Button("Save") {
self.presentation.wrappedValue.dismiss()
}
}
}.navigationTitle(Text("Item Num: \(item.itemNum)"))
}
}
My question here:
I would like to fetch the data from 'store' onAppear. but it fails.
After I quit the app, all the previous data gone. How can I make them to keep inside my app, even the app is kill?
Your second question first: In terms of storing (persisting your data), you have many options. The easiest would be to store it in UserDefaults, which I'll show in my example. You could also choose to use CoreData, which would be more of a process to set up, but would give you a more robust solution later on. Many more options like Realm, Firebase, SQLite, etc. exist as well.
struct Item: Identifiable, Codable {
var id: UUID = UUID()
var itemNum: Int
var notes: String = ""
}
final class ItemStore: ObservableObject {
#Published var items: [Item] = [] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "savedItems")
}
}
}
let defaultValues : [Item] = [
.init(itemNum: 55),
.init(itemNum: 57),
.init(itemNum: 87)
]
func fetch() {
let decoder = JSONDecoder()
if let savedItems = UserDefaults.standard.object(forKey: "savedItems") as? Data,
let loadedItems = try? decoder.decode([Item].self, from: savedItems) {
items = loadedItems
} else {
items = defaultValues
}
}
}
struct ContentView : View {
#State private var editMode = EditMode.inactive
#ObservedObject var store: ItemStore = ItemStore()
var body: some View {
NavigationView {
List {
ForEach(Array(store.items.enumerated()), id:\.1.id) { (index,item) in
NavigationLink(destination: ItemEditingView(item: self.$store.items[index])) {
VStack(alignment: .leading) {
Text("Item Num: \(item.itemNum)")
}
}
}
}
.onAppear(perform: store.fetch)
.navigationBarTitle("Items")
.navigationBarItems( trailing: addButton)
.environment(\.editMode, $editMode)
}
}
private var addButton: some View {
switch editMode {
case .inactive:
return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
default:
return AnyView(EmptyView())
}
}
private func onAdd() {
store.items.append(Item(id: UUID(), itemNum: 10))
}
}
struct ItemEditingView: View {
#Environment(\.presentationMode) var presentation
#Binding var item: Item
var body: some View {
Form {
Section(header: Text("Item")) {
Text("Item Num: \(item.itemNum)")
TextField("Type something...", text: $item.notes)
}
Section {
Button("Save") {
self.presentation.wrappedValue.dismiss()
}
}
}.navigationTitle(Text("Item Num: \(item.itemNum)"))
}
}
Regarding your first question, the reason that fetch failed is you had no fetch method. Plus, there was nothing to fetch, since the array of items just got populated upon creation of the ItemStore each time.
Notes:
Item now conforms to Codable -- this is what allows it to get transformed into a value that can be saved/loaded from UserDefaults
fetch is now called on onAppear.
Every time the data is changed, didSet is called, saving the new data to UserDefaults
There were a number of typos and things that just plain wouldn't compile in the original code, so make sure that the changes are reflected. Some of those include: enumerated instead of indexed in the ForEach, not calling Text(Text( with nested values, using the same capitalization of itemNum throughout, etc
Important: when testing this, make sure to give the simulator a few seconds after a change to save the data into UserDefaults before killing the app and opening it again.
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])
}
}
}
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)
}
}
}