How to use values in .onDelete to delete FMDB data - swiftui

I'm a Swift / SwiftUI newbie trying to integrate FMDB tables I've built into my app. I have been able to populate my list with a direct call to the FMDB tables and now want to be able to delete an item from the list and the corresponding underlying data in the FMDB table. I can't seem to figure out how to get the underlying row data to be able to run the SQL to delete the corresponding table data. In the Class for the TABLE I have:
// Budgeted_expense record for FMDB
struct BudgetedExpenseRecord: Hashable {
var expense_id: Int!
var account_code: Int!
var budget_code: Int!
var expense_code: Int!
var budget_year: Int!
var budget_month: Int!
var description: String!
var category: String!
var expense_budget: Double!
var expense_spent: Double!
var unexpected_expense: Double!
var category_code: Int!
var hidden: Bool!
}
The call to the DBMANAGER is:
// Select BudgetedExpense records for a given year / month
func selectBudgetedExpense(account_code: Int, budget_code: Int, year: Int, month: Int) -> [BudgetedExpense.BudgetedExpenseRecord]
{
var budgeted_expense_results: [BudgetedExpense.BudgetedExpenseRecord]
if openDatabase(){
budgeted_expense_results = BudgetedExpense.shared.selectBudgetedExpense(database: database, query: BudgetedExpense.shared.selectBudgetedExpense(account_code: account_code, budget_code: budget_code, year: year, month: month))
if checkTables {
print("\nQUERY: BudgetedExpense.shared.selectBudgetedExpense(account_code: \(account_code), budget_code: \(budget_code), year: \(year), month: \(month)")
print(BudgetedExpense.shared.selectBudgetedExpense(account_code: account_code, budget_code: budget_code, year: year, month: month))
}
database.close()
return budgeted_expense_results
}else {
print("Database was not opened. Couldn't execute the query. Returning empty results")
fatalError("Database was not opened. Couldn't execute the query. Returning empty results")
}
}
In my SwiftUI view I can populate the list as below:
List {
ForEach (DBManager.shared.selectBudgetedExpense(account_code: self.account_code, budget_code: self.budget_code, year: self.budget_year, month: self.budget_month), id: \.self) { record in
BudgetedExpenseEditRow(
expense_id: record.expense_id,
account_code: record.account_code,
budget_code: record.budget_code,
budget_year: record.budget_year,
budget_month: record.budget_month,
description: record.description,
category: record.category,
expense_budget: roundToPlaces(value: record.expense_budget,
places: 3),
expense_spent: record.expense_spent,
unexpected_expense: record.unexpected_expense,
hidden: record.hidden
)
}
.onDelete(perform: self.delete)
}.navigationBarTitle("Budgeted Expenses")
I haven't been able to figure out how onDelete to get the expense_id which is a unique row id in the table so that I can call a delete SQL?

Thanks to the comment above I was able to solve the problem I was having. I
put the db call into a local array:
var budgetedExpenses: [BudgetedExpense.BudgetedExpenseRecord] {
return DBManager.shared.selectBudgetedExpense(account_code: self.account_code, budget_code: self.budget_code, year: self.budget_year, month: self.budget_month)
}
then I used that array to populate the view.
List {
ForEach (self.budgetedExpenses, id: \.self) { record in
BudgetedExpenseRow(
expense_id: record.expense_id,
account_code: record.account_code,
budget_code: record.budget_code,
expense_code: record.expense_code,
budget_year: record.budget_year,
budget_month: record.budget_month,
description: record.description,
category: record.category,
expense_budget: roundToPlaces(value: record.expense_budget, places: 3),
expense_spent: record.expense_spent,
unexpected_expense: record.unexpected_expense,
hidden: record.hidden
)
}
}.navigationBarTitle("Budgeted Expenses")
and in the onDelete I was able to get the data that I needed.
func delete(at offsets: IndexSet) {
offsets.forEach { index in
let budgetExpense = self.budgetedExpenses[index]
let queryString = ExpenseTransactions.shared.deleteExpenseTransactionsForYearQuery(acctcode: budgetExpense.account_code!, budgetcode: budgetExpense.budget_code!, expensecode: budgetExpense.expense_code!, year: budgetExpense.budget_year!) + "\n" +
BudgetedExpenseDetails.shared.deleteBudgetedExpenseDetailsForYearQuery(acctcode: budgetExpense.account_code!, budgetcode: budgetExpense.budget_code!, expensecode: budgetExpense.expense_code!, year: budgetExpense.budget_year!) + "\n" +
BudgetedExpense.shared.deleteBudgetedExpenseForYearQuery(acctcode: budgetExpense.account_code!, budgetcode: budgetExpense.budget_code!, expensecode: budgetExpense.expense_code!, year: budgetExpense.budget_year!) + "\n" +
DBManagerSupport.shared.rollupBudgetTotals()
if !DBManager.shared.executeSQLStatements(query: queryString)
{
print("Failed to delete table data")
}
var budget_result: Budget.BudgetRecord
budget_result = DBManager.shared.selectBudgetYearMonth(acctCode: self.account_code, budgetCode: self.budget_code, budgetYear: self.budget_year, budgetMonth: self.budget_month)
self.total_budgeted_expense = budget_result.expected_expense
self.total_expense_spent = budget_result.expected_expense_spent
self.total_unexpected_expense = budget_result.unexpected_expense
self.recordDeleted = true
self.isEditing = false
}
}

Related

Swift Charts - changing the X axis literals

I am moving my charts over from Daniel Gindi to Apple's Chart and haven't worked out how to control the X axis labels.
The X axis generates a daily angle from day 1 to 365 for the year, so is an integer. I want to display the months of the year, as shown in the previous version of the chart.
Is it possible to use .chartXScale for this purpose? I am not sure how to get it to accept strings or if I need to try a different approach.
var monthValues: [String] = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sept","Oct","Nov","Dec"]
Chart{
ForEach(dayTilt) {item in
LineMark(
...
)
}
.frame(height: 360)
.chartXScale(domain: monthValues[0...11])
You should probably store a Date rather than an Int for a day, so something like:
struct DayTilt: Identifiable {
internal init(day: Int, value: Float) {
date = Calendar.current.date(from: DateComponents(day: day))!
self.value = value
}
let id = UUID()
let date: Date
let value: Float
}
then you can use the following PlottableValue
value(_:date:unit:calendar:)
e.g.
struct ContentView: View {
#State var dayTilts: [DayTilt] = …
var body: some View {
Chart(dayTilts) { dayTilt in
LineMark(x: .value("Date", dayTilt.date, unit: .day),
y: .value("Angle", dayTilt.value))
}
.padding()
}
}

Set not Removing Duplicate Dates

I have a structure that displays entries sorted by date. The date is displayed once for all entries of the same date. The problem I have is that Set
is not removing duplicate dates. If I have two entries with the same date, I have two blocks in the view with same entries in each block. See my original post here. If I enter multiple entries with the same date, uniqueDates (looking with the debugger) shows the same number of elements with the same date.
My theory is that Array(Set(wdvm.wdArray)) is sorting on the complete unformatted date which includes the time or other variables in each element. Therefore it thinks all the dates are unique. Is there anyway to use formatted dates for sorting?
struct WithdrawalView: View {
#StateObject var wdvm = Withdrawal()
var uniqueDates: [String] {
Array(Set(wdvm.wdArray)) // This will remove duplicates, but WdModel needs to be Hashable
.sorted { $0.wdDate < $1.wdDate } // Compare dates
.compactMap {
$0.wdDate.formatted(date: .abbreviated, time: .omitted) // Return an array of formatted the dates
}
}
// filters entries for the given date
func bankEntries(for date: String) -> [WdModel] {
return wdvm.wdArray.filter { $0.wdDate.formatted(date: .abbreviated, time: .omitted) == date }
}
var body: some View {
GeometryReader { g in
VStack (alignment: .leading) {
WDTitleView(g: g)
List {
if wdvm.wdArray.isEmpty {
NoItemsView()
} else {
// outer ForEach with unique dates
ForEach(uniqueDates, id: \.self) { dateItem in // change this to sort by date
Section {
// inner ForEach with items of this date
ForEach(bankEntries(for: dateItem)) { item in
wdRow(g: g, item: item)
}
} header: {
Text("\(dateItem)")
}
}.onDelete(perform: deleteItem)
}
}
.navigationBarTitle("Bank Withdrawals", displayMode: .inline)
Below is the class used by this module
struct WdModel: Codable, Identifiable, Hashable {
var id = UUID()
var wdDate: Date // bank withdrawal date
var wdCode: String // bank withdrawal currency country 3-digit code
var wdBank: String // bank withdrawal bank
var wdAmtL: Double // bank withdrawal amount in local currency
var wdAmtH: Double // bank withdrawal amount in home currency
var wdCity: String
var wdState: String
var wdCountry: String
}
class Withdrawal: ObservableObject {
#AppStorage(StorageKeys.wdTotal.rawValue) var withdrawalTotal: Double = 0.0
#Published var wdArray: [WdModel]
init() {
if let wdArray = UserDefaults.standard.data(forKey: StorageKeys.wdBank.rawValue) {
if let decoded = try? JSONDecoder().decode([WdModel].self, from: wdArray) {
self.wdArray = decoded
return
}
}
self.wdArray = []
// save new withdrawal data
func addNewWithdrawal(wdDate: Date, wdCode: String, wdBank: String, wdAmtL: Double, wdAmtH: Double, wdCity: String, wdState: String, wdCountry: String) -> () {
self.withdrawalTotal += wdAmtH
let item = WdModel(wdDate: wdDate, wdCode: wdCode, wdBank: wdBank, wdAmtL: wdAmtL, wdAmtH: wdAmtH, wdCity: wdCity, wdState: wdState, wdCountry: wdCountry)
wdArray.append(item)
if let encoded = try? JSONEncoder().encode(wdArray) { // save withdrawal entries
UserDefaults.standard.set(encoded, forKey: StorageKeys.wdBank.rawValue)
}
}
}
I am trying to display all entries of the same date under the one date. This example shows what I want but not the 3 copies of the date and entries.
For Set to remove duplicate dates, try something like this:
var uniqueDates: [String] {
Array(Set(wdvm.wdArray.map { $0.wdDate }))
.sorted { $0 < $1 }
.compactMap {
$0.formatted(date: .abbreviated, time: .omitted)
}
}
EDIT-3:
to display unique bankEntries for a given date, based on day, month and year of a date (not seconds,etc...):
struct ContentView: View {
#State var wdArray = [WdModel]()
let frmt: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMM dd, yyyy"
return formatter
}()
func bankEntries(for date: String) -> [WdModel] {
return wdArray.filter { frmt.string(from: $0.wdDate) == date }
}
var uniqueDates: [String] {
Array(Set(wdArray.map { frmt.string(from: $0.wdDate) }))
.sorted { frmt.date(from: $0) ?? Date() < frmt.date(from: $1) ?? Date() }
.compactMap { $0 }
}
var body: some View {
List {
// outer ForEach with unique dates
ForEach(uniqueDates, id: \.self) { dateItem in // change this to sort by date
Section {
// inner ForEach with items of this date
ForEach(bankEntries(for: dateItem)) { item in
// wdRow(g: g, item: item)
HStack {
Text(item.wdDate.formatted(date: .abbreviated, time: .omitted))
Text(item.wdCode).foregroundColor(.red)
}
}
} header: {
Text("\(dateItem)")
}
}
}
.onAppear {
let today = Date() // <-- here
let otherDate = Date(timeIntervalSince1970: 345678)
wdArray = [
WdModel(wdDate: today, wdCode: "EUR", wdBank: "Bank of Innsbruck", wdAmtL: 4575, wdAmtH: 1625, wdCity: "Innsbruck", wdState: " Tyrol", wdCountry: "Aus"),
WdModel(wdDate: otherDate, wdCode: "CHF", wdBank: "Bank of Interlaken", wdAmtL: 6590, wdAmtH: 2305, wdCity: "Interlaken", wdState: "Bernese Oberland ", wdCountry: "CHF"),
WdModel(wdDate: today, wdCode: "USD", wdBank: "Bank X", wdAmtL: 1200, wdAmtH: 3275, wdCity: "Las Vegas", wdState: "NV", wdCountry: "USA")
]
}
}
}

Don't Display Entry Date if Same as previous Entry Date in List

I have a list of entries containing dates. I would like to only display the date if it is different from the previous entry date.
I am reading in the entries from core data and passing them to the method ckEntryDate for determination of whether to display the date. The method is called from inside a list. If the string returned by ckEntryDate is blank (string.isEmpty) I know that the current entry date is the same as the previous date and I don't need to display the date.
There are no errors occurring, but the current entry date is not being saved via userDefaults. I would appreciate any ideas on how to save the current date or how to check for identical dates.
Thanks
struct HistoryView: View {
#EnvironmentObject var userData: UserData
#Environment(\.managedObjectContext) var viewContext
// fetch core data
#FetchRequest(
entity: CurrTrans.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CurrTrans.entryDT, ascending: true)]
) var currTrans: FetchedResults<CurrTrans>
var body: some View {
GeometryReader { g in
VStack (alignment: .leading) {
ShowTitle(g:g, title: "History")
ShowHistoryHeader(g: g)
ScrollView (.vertical) {
List {
ForEach(currTrans, id: \.id) { item in
let entryDate = userData.ckEntryDate( item: item)
showRow(g:g, item: item, entryDate: entryDate)
}
.onDelete(perform: deleteItem)
}
}.font(.body)
}
}
}
This method is part of the class UserData: ObservableObject {
// check if history entry date is same as previous date or the first entry
func ckEntryDate( item: CurrTrans) -> (String) {
var outDate: String = ""
var savedDate: String = ""
//read in savedDate
if UserDefaults.standard.string(forKey: "storeDate") != "" {
savedDate = UserDefaults.standard.string(forKey: "storeDate") ?? ""
}else {
savedDate = ""
}
// convert Date? to String
let cdate = item.entryDT ?? Date()
let currDate = cdate.getFormattedDate()
// check if no previous entries
if savedDate.isEmpty {
outDate = currDate
}
else { // savedDate is not blank
if savedDate == currDate {
outDate = ""
}
else { // date entries different
outDate = currDate
}
savedDate = currDate
}
// save savedDate
UserDefaults.standard.set(savedDate, forKey: "saveDate")
return outDate
}
}
extension Date {
func getFormattedDate() -> String {
// localized date & time formatting
let dateformat = DateFormatter()
dateformat.dateStyle = .medium
dateformat.timeStyle = .none
return dateformat.string(from: self)
}
}
Assuming your function ckEntryDate works correctly, you could try this approach of filtering the data at the ForEach:
ForEach(currTrans.filter { "" != userData.ckEntryDate(item: $0) }, id: \.id) { item in
showRow(g:g, item: item, entryDate: userData.ckEntryDate(item: item))
}
You can also try this:
ForEach(currTrans, id: \.id) { item in
let entryDate = userData.ckEntryDate(item: item)
if !entryDate.isEmpty {
showRow(g:g, item: item, entryDate: entryDate)
}
}
I have been looking for a way to persist the current entry date for comparison to the next entry date for checking if the dates are the same.
I discovered that I could do this by simply placing a variable at the top of the class that contains the method ckEntryDate.
class UserData: ObservableObject {
var storedDate: String = ""
So thanks to all who took the time to consider a possible answers.
// check if history entry date is the 1st entry or the same as previous date
func ckEntryDate( item: CurrTrans) -> (String) {
var outDate: String = ""
// initialzie the entry date
let cdate = item.entryDT ?? Date()
let entryDate = cdate.getFormattedDate()
// if savedDate is blank -> no previous entries
if storedDate.isEmpty {
outDate = entryDate
}
else { // savedDate is not blank
if storedDate == entryDate {
outDate = ""
}
else { // date entries different
outDate = entryDate
}
}
storedDate = entryDate
// outDate returns blank or the current date
return (outDate)
}
}

How to read and filter large Realm dataset in SwiftUI?

I'm storing ~100.000 dictionary entries in a realm database and would like to display them. Additionally I want to filter them by a search field. Now I'm running in a problem: The search function is really inefficient although I've tried to debounce the search.
View Model:
class DictionaryViewModel : ObservableObject {
let realm = DatabaseManager.sharedInstance
#Published var entries: Results<DictionaryEntry>?
#Published var filteredEntries: Results<DictionaryEntry>?
#Published var searchText: String = ""
#Published var isSearching: Bool = false
var subscription: Set<AnyCancellable> = []
init() {
$searchText
.debounce(for: .milliseconds(800), scheduler: RunLoop.main) // debounces the string publisher, such that it delays the process of sending request to remote server.
.removeDuplicates()
.map({ (string) -> String? in
if string.count < 1 {
self.filteredEntries = nil
return nil
}
return string
})
.compactMap{ $0 }
.sink { (_) in
} receiveValue: { [self] (searchField) in
filter(with: searchField)
}.store(in: &subscription)
self.fetch()
}
public func fetch(){
self.entries = DatabaseManager.sharedInstance.fetchData(type: DictionaryEntry.self).sorted(byKeyPath: "pinyin", ascending: true)
self.filteredEntries = entries
}
public func filter(with condition: String){
self.filteredEntries = self.entries?.filter("pinyin CONTAINS[cd] %#", searchText).sorted(byKeyPath: "pinyin", ascending: true)
}
In my View I'm just displaying the filteredEtries in a ScrollView
The debouncing works well for short text inputs like "hello", but when I filter for "this is a very long string" my UI freezes. I'm not sure whether something with my debounce function is wrong or the way I handle the data filtering in very inefficient.
EDIT: I've noticed that the UI freezes especially when the result is empty.
EDIT 2:
The .fetchData() function is just this here:
func fetchData<T: Object>(type: T.Type) -> Results<T>{
let results: Results<T> = realm.objects(type)
return results
}
All realm objects have a primary key. The structure looks like this:
#objc dynamic var id: String = NSUUID().uuidString
#objc dynamic var character: String = ""
#objc dynamic var pinyin: String = ""
#objc dynamic var translation: String = ""
override class func primaryKey() -> String {
return "id"
}
EDIT 3: The filtered results are displayed this way:
ScrollView{
LazyVGrid(columns: gridItems, spacing: 0){
if (dictionaryViewModel.filteredEntries != nil) {
ForEach(dictionaryViewModel.filteredEntries!){ entry in
Text("\(entry.translation)")
}
} else {
Text("No results found")
}
}

Using a button to add data that a user inputted

Okay so I've been working on this for several days now and have not had any luck with an answer that makes any sense. I have a form in SwiftUI, using #ObservedObject to pull variables from a struct. In that form, I have a variety of text fields and pickers that the user can interact with. HOWEVER, I cannot figure out how to get my "Add" button to actually add that data to any of the other views in the app. I followed the sandwiches tutorial from WWDC20, with significant changes, so there is a swift file with "testData" and essentially I'm trying to get it so that the button uses the user input to append the testData and show that instead of nothing.
struct Horse: Identifiable {
var id = UUID()
var name: String
var gender: String
var breed: String
var type: String
var scale: String
var brand: String
var finish: String
var specialty: String
var imageName: String { return name }
var thumbnailName: String { return name + "Thumb" }
}
let testData = [
Horse(name: "Van Gogh", gender: "Stallion", breed: "Unknown", type: "Customized", scale: "Stablemate", brand: "Peter Stone", finish: "Gloss", specialty: "OOAK")
]
So this is what I'm using to establish testData and the parameters for what should be included in it.
func addANewHorse() {
withAnimation {
testStore.horses.append(Horse(name: "\(horseDetails.horseName)", gender: "\(horseDetails.selectedGender.rawValue)", breed: "\(horseDetails.horseBreed)", type: "\(horseDetails.type.rawValue)", scale: "\(horseDetails.scale.rawValue)", brand: "\(horseDetails.brand.rawValue)", finish: "\(horseDetails.finish.rawValue)", specialty: "\(horseDetails.specialty.rawValue)"))
}
}
Button("Add", action: {
addANewHorse();
self.presentationMode.wrappedValue.dismiss()
})
And that is what I'm using to try and append the testData to update with the users input. I know this is kind of choppy but does anyone have any advice whatsoever?
---EDIT---
My main app file looks like this...
#main
struct Pferd_HerdApp: App {
#StateObject private var store = HorseStore()
#StateObject private var horseDetails = HorseDetails()
var body: some Scene {
WindowGroup {
ContentView(store: store, horseDetails: HorseDetails())
}
}
}
my horse store class looks like this...
class HorseStore: ObservableObject {
#Published var horses: [Horse]
init(horses: [Horse] = []) {
self.horses = horses
}
}
let testStore = HorseStore(horses: testData)
Also, "HorseDetails" is the observableobject I'm trying to pull data from to append the testData, so here is the code for that
class HorseDetails: ObservableObject {
#Published var horseName = ""
#Published var selectedGender = Gender.allCases[0]
#Published var horseBreed = ""
#Published var purchaseDate = Date()
#Published var winCount = ""
#Published var notes = ""
#Published var brand = Brands.allCases[0]
#Published var type = Type.allCases[0]
#Published var scale = Scale.allCases[0]
#Published var finish = Finish.allCases[0]
#Published var specialRun = false
#Published var specialty = Specialty.allCases[0]
}
var horseDetails = HorseDetails()
and I changed the let for testData to a variable
Since your Question leaves a lot of code out, I will be making a few assumptions. I'm assuming that your form (where you have the button to add data) and your view for displaying the data are in two different views. You have not included your view model in the code, although there was an instance of your view model (testStore) used in the code above. You need to make sure that somewhere at the root of your view hierarchy, you made an instance of your view model (I'm assuming its called TestStoreViewModel) and passed that as an environment object to your subviews. For example, you should something like this
#main
struct YourApp: App {
let testStoreViewModel = TestStoreViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(testStoreViewModel)
}
}
}
in all of your views where you need to use the data from your TestStoreViewModel, you should declare it like so
#EnvironmentObject var testStore:TestStoreViewModel
Using environment objects means that your observable object is automatically synced across all of your views that use the environment object. Everything else in the code above should work fine with the use of EnvironmentObjects and a single source of truth. For more on environment objects, you can check out this article which in my opinion is great at explaining Environment Objects in swiftui. It is also important to note that in that article, it mentioned the use of a SceneDelegte and the ContentView being wrapped around a UIHostingController. That was replaced by the first block of code I showed you above.