Reputation: 387
I have an accounting app with a screen that displays two views. One view displays Currently In Use Categories and another that displays Available Categories. The user may select which categories to use and remove other not needed. Tapping on a category in either box immediately moves the category to the other category box. These programmed categories may not be deleted.
In addition, the user may create their own optional categories. The trouble I'm having is deleting the optional categories. (The Add New Category works fine.) I know how to normally implement view delete using List and ForEach but this seems somewhat different. If I add onDelete at the bottom of the ForEach loop and change the VStack to a List I don't see any entries in the list. Changing the List to a VStack displays the categories in the list but doesn't let me delete any categories. Another possible problem is dragging a category to the left for deletion may also trigger the switch view button.
I am filtering the data because I was getting gaps in the lists for categories currently in the other view.
The code below is the logic for displaying the Currently In Use box.
struct GetCurrentCats: View {
var g: GeometryProxy
@State private var showStatus: Bool = false
@Binding var presentAlert: Bool
@Binding var presentDetail: String
@EnvironmentObject var categories: Categories
var body: some View {
//VStack (alignment: .leading, spacing: 6) {
List {
ForEach(categories.filteredInUse, id: \.id) { item in
Button(action: {
if item.catTotal == 0.0 { // must have a zero balance to remove
withAnimation {
showStatus = false
// Change the state of category
categories.setCatShow(forId: item.id)
}
} else {
presentAlert = true
presentDetail = item.catName
}
}){
HStack {
Image(systemName: item.catPix)
.resizable()
.foregroundColor(Color(.systemOrange))
.frame(width: 25, height: 25)
.frame(width: UIDevice.current.userInterfaceIdiom == .phone ? g.size.width * 0.40 : g.size.width * 0.20)
Text(item.catName)
.padding(.horizontal, 8)
.background(item.catTotal != 0.0 ? Color(.systemRed) : Color(.systemBlue))
.foregroundColor(Color.white)
.cornerRadius(8)
}
}
}.onDelete(perform: deleteCurCat)
}.frame(maxWidth: .infinity, alignment: .leading)
}
}
func deleteCurCat(indexSet: IndexSet) {
categories.catItem.remove(atOffsets: indexSet)
}
}
Here is class Categories:
// working categories
struct CatModel: Codable, Identifiable, Hashable {
var id = UUID()
var catName: String // category name
var catTotal: Double // category total
var catBudget: Double // category budget
var catPix: String // sf symbol
var catShow: Bool
/// Provides a new instance, toggling `catShow`.
var toggleCatShow: CatModel {
// Note: by creating a new instance, the new id will be different
CatModel(catName: catName, catTotal: catTotal, catBudget: catBudget, catPix: catPix, catShow: !catShow)
}
}
class Categories: ObservableObject {
@Published var filteredInUse: [CatModel] = []
@Published var filteredNotInUse: [CatModel] = []
@Published var catItem: [CatModel] {
didSet {
self.save()
}
}
init() {
// read in category data
if let catItem = UserDefaults.standard.data(forKey: StorageKeys.workCat.rawValue) {
if let decoded = try? JSONDecoder().decode([CatModel].self, from: catItem) {
self.catItem = decoded
self.updateStatus()
return
}
}
catItem = []
// catShow: 1 = category available for use; 2 = category currently being used
let item0 = CatModel(catName: "Lodging", catTotal: 0.0, catBudget: 0, catPix: "bed.double.fill", catShow: false)
self.catItem.append(item0)
let item1 = CatModel(catName: "Food", catTotal: 0.0, catBudget: 0, catPix: "cart", catShow: false)
self.catItem.append(item1)
let item2 = CatModel(catName: "Airplane", catTotal: 0.0, catBudget: 0, catPix: "airplane", catShow: false)
self.catItem.append(item2)
let item3 = CatModel(catName: "Train", catTotal: 0.0, catBudget: 0, catPix: "tram", catShow: false)
self.catItem.append(item3)
let item4 = CatModel(catName: "Bus", catTotal: 0.0, catBudget: 0, catPix: "bus.fill", catShow: false)
self.catItem.append(item4)
let item5 = CatModel(catName: "Ferry", catTotal: 0.0, catBudget: 0, catPix: "ferry", catShow: false) // "helm"
self.catItem.append(item5)
let item6 = CatModel(catName: "Local Transit", catTotal: 0.0, catBudget: 0, catPix: "textbox", catShow: false)
self.catItem.append(item6)
let item7 = CatModel(catName: "Sightseeing ", catTotal: 0.0, catBudget: 0, catPix: "photo", catShow: false)
self.catItem.append(item7)
let item8 = CatModel(catName: "Entertainment", catTotal: 0.0, catBudget: 0, catPix: "music.mic", catShow: false)
self.catItem.append(item8)
let item9 = CatModel(catName: "Souvenirs", catTotal: 0.0, catBudget: 0, catPix: "gift", catShow: false)
self.catItem.append(item9)
let item10 = CatModel(catName: "Laundry", catTotal: 0.0, catBudget: 0, catPix: "scribble", catShow: false)
self.catItem.append(item10)
let item11 = CatModel(catName: "Rental Car", catTotal: 0.0, catBudget: 0, catPix: "car", catShow: false)
self.catItem.append(item11)
let item12 = CatModel(catName: "Fuel", catTotal: 0.0, catBudget: 0, catPix: "fuelpump", catShow: false) // gauge
self.catItem.append(item12)
let item13 = CatModel(catName: "Parking", catTotal: 0.0, catBudget: 0, catPix: "parkingsign.circle", catShow: false)
self.catItem.append(item13)
self.updateStatus()
}
func updateStatus() {
filteredInUse = catItem.filter({ (user) -> Bool in
return user.catShow == true
})
filteredNotInUse = catItem.filter({ (user) -> Bool in
return user.catShow == false
})
}
// Replaces an instance of `CatModel` by changing its `catShow` value.
func setCatShow(forId id: UUID) {
let index = catItem.firstIndex { $0.id == id }
if let index = index {
catItem.replaceSubrange(index...index, with: [catItem[index].toggleCatShow])
}
// Refresh the filtered arrays
updateStatus()
}
add new optional category (8 max)
func addNewCatetory(catName: String) -> () {
let item = CatModel(catName: catName, catTotal: 0.0, catBudget: 0, catPix: "person.2", catShow: true)
catItem.append(item)
}
}
Here is the upper piece of my Update Categories logic. If I run the example code with GetCurrentCats(), it displays the programmed categories. If I include this upper portion of the logic that sets up the two boxes and text above and below each box then GetCurrentCats doesn't display any programmed categories but GetAvailableCats() does display its categories. It appears to be something with the onDelete in GetCurrentCats that it doesn't like.
struct ContentView: View {
@EnvironmentObject var categories: Categories
@State private var presentAlert = false
@State private var presentDetail: String = ""
var body: some View {
GeometryReader { g in
VStack (alignment: .center) {
Text("Current Categories")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 6)
.padding(.bottom, -3)
ScrollView {
GetCurrentCats(g: g, presentAlert: $presentAlert, presentDetail: $presentDetail)
} .frame(width: g.size.width * 0.80, height: g.size.height * 0.35)
.border(Color(.systemBlue), width: 4)
Text("Tap category to remove from system.")
.font(.footnote)
Text("Red categories have a nonzero balance.")
.font(.footnote)
Text("Categories must have a zero balance to remove.")
.font(.footnote)
.padding(.bottom, g.size.height * 0.04)
Text("Available Categories")
.font(.headline)
.padding(.top, g.size.height * 0.05)
.padding(.bottom, -3)
ScrollView {
GetAvailableCats(g: g)
} .frame(width: g.size.width * 0.80, height: g.size.height * 0.35).border(Color(.systemBlue), width: 4)
Text("Tap category to add to system.")
.font(.footnote)
}.alert(
"Unable to remove category.", isPresented: $presentAlert,
presenting: presentDetail
) { detail in
Button("OK") {}
} message: { detail in
Text("\(presentDetail) category has a nonzero balance.")
}
.navigationBarTitle("Setup Categories", displayMode: .inline)
.navigationViewStyle(StackNavigationViewStyle())
/* .navigationBarItems(trailing: NavigationLink (destination: AddCatView()) {
Image(systemName: "plus")
.resizable()
.frame(width: 18, height: 18)
}) */
}
}
}
Upvotes: 0
Views: 176
Reputation: 36304
you should not be using duplicate, triplicate arrays to store CatModels
of different
catShow
values. Use only one array catItem
and filter the array according to your needs.
Such as:
filteredInUse
equivalent, use categories.catItem.filter{ $0.catShow.wrappedValue }
and
filteredNotInUse
equivalent, use categories.catItem.filter{ !$0.catShow.wrappedValue }
in the ForEach
loop.
I've updated your code using this approach.
// working categories
struct CatModel: Codable, Identifiable, Hashable {
var id = UUID()
var catName: String // category name
var catTotal: Double // category total
var catBudget: Double // category budget
var catPix: String // sf symbol
var catShow: Bool
}
class Categories: ObservableObject {
// the array of CatModel
@Published var catItem: [CatModel] {
didSet {
// self.save()
}
}
init() {
// you should not use this to store the array of CatModels.
// UserDefaults is meant to be used only for small amount of data.
// read in category data
// if let catItem = UserDefaults.standard.data(forKey: StorageKeys.workCat.rawValue) {
// if let decoded = try? JSONDecoder().decode([CatModel].self, from: catItem) {
// self.catItem = decoded
// return
// }
// }
catItem = []
let item0 = CatModel(catName: "Lodging", catTotal: 0.0, catBudget: 0, catPix: "bed.double.fill", catShow: true)
self.catItem.append(item0)
let item1 = CatModel(catName: "Food", catTotal: 0.0, catBudget: 0, catPix: "cart", catShow: false)
self.catItem.append(item1)
let item2 = CatModel(catName: "Airplane", catTotal: 0.0, catBudget: 0, catPix: "airplane", catShow: true)
self.catItem.append(item2)
let item3 = CatModel(catName: "Train", catTotal: 0.0, catBudget: 0, catPix: "tram", catShow: false)
self.catItem.append(item3)
let item4 = CatModel(catName: "Bus", catTotal: 0.0, catBudget: 0, catPix: "bus.fill", catShow: true)
self.catItem.append(item4)
let item5 = CatModel(catName: "Ferry", catTotal: 0.0, catBudget: 0, catPix: "ferry", catShow: false) // "helm"
self.catItem.append(item5)
let item6 = CatModel(catName: "Local Transit", catTotal: 0.0, catBudget: 0, catPix: "textbox", catShow: true)
self.catItem.append(item6)
let item7 = CatModel(catName: "Sightseeing ", catTotal: 0.0, catBudget: 0, catPix: "photo", catShow: false)
self.catItem.append(item7)
let item8 = CatModel(catName: "Entertainment", catTotal: 0.0, catBudget: 0, catPix: "music.mic", catShow: true)
self.catItem.append(item8)
let item9 = CatModel(catName: "Souvenirs", catTotal: 0.0, catBudget: 0, catPix: "gift", catShow: false)
self.catItem.append(item9)
let item10 = CatModel(catName: "Laundry", catTotal: 0.0, catBudget: 0, catPix: "scribble", catShow: true)
self.catItem.append(item10)
let item11 = CatModel(catName: "Rental Car", catTotal: 0.0, catBudget: 0, catPix: "car", catShow: false)
self.catItem.append(item11)
let item12 = CatModel(catName: "Fuel", catTotal: 0.0, catBudget: 0, catPix: "fuelpump", catShow: true) // gauge
self.catItem.append(item12)
let item13 = CatModel(catName: "Parking", catTotal: 0.0, catBudget: 0, catPix: "parkingsign.circle", catShow: false)
self.catItem.append(item13)
}
// add new optional category (8 max)
func addNewCatetory(catName: String) {
catItem.append(CatModel(catName: catName, catTotal: 0.0, catBudget: 0, catPix: "person.2", catShow: true))
}
}
// for testing
struct ContentView: View {
@StateObject var categories = Categories()
@State var presentAlert = false
@State var presentDetail: String = ""
var body: some View {
GeometryReader { geom in
GetCurrentCats(g: geom, presentAlert: $presentAlert, presentDetail: $presentDetail)
.environmentObject(categories)
}
}
}
struct GetCurrentCats: View {
var g: GeometryProxy
@Binding var presentAlert: Bool
@Binding var presentDetail: String
@EnvironmentObject var categories: Categories
var body: some View {
VStack (alignment: .leading, spacing: 6) {
List {
// catShow=true , filteredInUse equivalent list
ForEach($categories.catItem.filter{ $0.catShow.wrappedValue }, id: \.id) { $item in
Button(action: {
if item.catTotal == 0.0 { // must have a zero balance to remove
withAnimation {
// Change the state of category
item.catShow = true
}
} else {
presentAlert = true
presentDetail = item.catName
}
}){
HStack {
Image(systemName: item.catPix)
.resizable()
.foregroundColor(Color(.systemOrange))
.frame(width: 25, height: 25)
.frame(width: UIDevice.current.userInterfaceIdiom == .phone ? g.size.width * 0.40 : g.size.width * 0.20)
Text(item.catName)
.padding(.horizontal, 8)
.background(item.catTotal != 0.0 ? Color(.systemRed) : Color(.systemBlue))
.foregroundColor(Color.white)
.cornerRadius(8)
}
}
}.onDelete(perform: deleteCurCat)
}.frame(maxWidth: .infinity, alignment: .leading)
}
}
func deleteCurCat(indexSet: IndexSet) {
if let index = indexSet.first {
// you have to filter exactly like in the ForEach
let cats = categories.catItem.filter({ $0.catShow })
if index < cats.count {
if let ndx = categories.catItem.firstIndex(where: { $0.id == cats[index].id }) {
// categories.catItem.remove(at: ndx) // if you want to totally remove the catItem
// to switch/transfer category, just toggle the catShow
categories.catItem[ndx].catShow.toggle()
}
}
}
}
}
EDIT-1: to show the list of categories:
Remove the ScrollView
(recommended solution), or set the frame of GetCurrentCats
, such as:
ScrollView {
GetCurrentCats(g: g, presentAlert: $presentAlert, presentDetail: $presentDetail)
.frame(width: g.size.width * 0.80, height: g.size.height * 0.35)
}
.frame(width: g.size.width * 0.80, height: g.size.height * 0.35)
.border(Color(.systemBlue), width: 4)
Upvotes: 1