I am leaving a question for the first time. I am Korean, and I started developing it on my own a while ago. I am in a bind due to an ongoing issue recently. I am not even sure how many codes I need to attach to fill out a question for the first time. Please understand.
First of all, below is the Expense Model that I’m using. It happens when I delete DisclosureGroup as a swipe on the CategoryView screen. One peculiarity is that there is a difference between a real device and a virtual device using IOS 17 the same. In a virtual device, there is no problem of forced termination even if the process is repeated over and over again, but in a real device, forced termination occurs every time. I can’t get a sense of which part is the problem, so I’m asking for your help.
import SwiftUI
import SwiftData
@Model
class Expense : Identifiable {
var title: String
var amount: Double
var date: Date
var category: Category?
init(title: String, amount: Double, date: Date, category: Category? = nil) {
self.title = title
self.amount = amount
self.date = date
self.category = category
}
And below is the full text of the code where the problem occurs.
import SwiftUI
import SwiftData
struct CategoriesView: View {
@Query(animation: .snappy) private var allCategories: [Category]
@Query(sort: [SortDescriptor(\Expense.date, order: .reverse)], animation: .snappy) var allExpenses: [Expense]
@Environment(\.modelContext) private var context
@StateObject private var settingsViewModel = SettingsViewModel()
@State var addCategory: Bool = false
@State var categoryName: String = ""
@State private var deleteRequest: Bool = false
@State private var requestedCategory: Category?
@State var addExpense: Bool = false
@State var setting: Bool = false
@State var choosedDate : Date?
@State private var addedCategory: Category?
@State private var categoryNames: [String] = []
@State private var currentTab: String = "Categories"
@State private var categories: [Category] = []
func sortedExpenses(for category: Category) -> [Expense]? {
return allExpenses.filter { $0.category == category }.sorted(by: { $0.date < $1.date })
}
func categoryTotalAmount(category: Category) -> Double {
let sortedExpenses = allExpenses.filter { $0.category == category }.sorted(by: { $0.date < $1.date })
return sortedExpenses.reduce(0) { $0 + $1.amount }
}
var body: some View {
NavigationStack {
List {
ForEach(allCategories.sorted(by: {
($0.expenses?.count ?? 0) > ($1.expenses?.count ?? 0)
})) { category in
Section {
DisclosureGroup {
if let sortedExpenses = sortedExpenses(for: category) {
ForEach(sortedExpenses.indices, id: \.self) { index in
let expense = sortedExpenses[index]
HStack {
Text(expense.date, formatter: dateFormatter)
.textScale(.secondary)
ExpenseCardView(expense: expense, displayTag: false)
.environmentObject(settingsViewModel)
}
if index == sortedExpenses.indices.last {
HStack {
Text("total:")
.multilineTextAlignment(.leading)
Spacer()
Text(formatCurrency(amount: categoryTotalAmount(category: category)))
.foregroundColor(categoryTotalAmount(category: category) >= 0 ? .incomeAmount : .expenseAmount)
}
}
}
if !addExpense && sortedExpenses.indices.last == 0 {
HStack {
Spacer()
}
}
}else {
ContentUnavailableView {
Label("List is Empty", systemImage: "tray.fill")
}
}
} label: {
Text(category.categoryName)
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button {
deleteRequest.toggle()
requestedCategory = category
} label: {
Image(systemName: "trash")
}
.tint(.delete)
}
}
}
}
.navigationTitle("Category")
.overlay(content: {
if allCategories.isEmpty {
ContentUnavailableView {
Label("생성된 카테고리가 없습니다", systemImage: "tray.fill")
}
}
})
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
AppMenuView(addExpense: $addExpense, addCategory: $addCategory, setting: $setting)
}
}
.onAppear {
settingsViewModel.categoryNames = allCategories.map { $0.categoryName }
categories = allCategories
}
.onChange(of: settingsViewModel.categoryListChanged) { newValue in
settingsViewModel.categoryListChanged = newValue
if currentTab == "Categories" {
currentTab = "Expenses"
}
}
.onChange(of: allCategories) { updatedCategories in
settingsViewModel.categoryNames = updatedCategories.map { $0.categoryName }
categories = updatedCategories
if currentTab == "Expenses" {
settingsViewModel.categoryListChanged.toggle()
}
}
.onChange(of: allCategories) { updatedCategories in
settingsViewModel.categoryNames = updatedCategories.map { $0.categoryName }
settingsViewModel.categoryListChanged.toggle()
}
.sheet(isPresented: $addCategory) {
categoryName = ""
} content: {
NavigationStack {
List {
Section("Category Name") {
TextField("Please enter a category name.", text: $categoryName)
}
}
.navigationTitle("New Category Name")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("cancel") {
addCategory = false
}
.tint(.cancel)
}
ToolbarItem(placement: .topBarTrailing) {
Button("add") {
let category = Category(id: UUID(), categoryName: categoryName)
context.insert(category)
addedCategory = category
categoryName = ""
addCategory = false
}
.disabled(categoryName.isEmpty)
}
}
}
.presentationDetents([.height(180)])
.interactiveDismissDisabled()
}
}
.alert("When you delete a category, the details specified in the category are also deleted. Are you sure you want to delete it?", isPresented: $deleteRequest){
Button(role: .destructive) {
if let requestedCategory {
let expensesToDelete = allExpenses.filter { $0.category == requestedCategory }
expensesToDelete.forEach { context.delete($0) }
context.delete(requestedCategory)
self.requestedCategory = nil
settingsViewModel.categoryListChanged.toggle()
}
} label: {
Text("delete")
}
Button(role: .cancel) {
requestedCategory = nil
} label: {
Text("cancel")
}
}
.sheet(isPresented: $addExpense) {
AddExpenseView()
.interactiveDismissDisabled()
}
.sheet(isPresented: $setting) {
SettingsView()
.interactiveDismissDisabled()
}
}
At first, I thought that there was a problem because of the overlapping part of the on-change, so I tried to modify the on-change, delete unnecessary comments, and so on, but I still haven’t solved it.
I’m attaching it together because I might need it
https://github.com/gara-age/Malendar




