In my app I've a List view with an array of Dates, var calcWeights : [EggDay]
struct EggDay: Identifiable {
var id : Int
var date : Date
var weight : Double
var measured : Bool
var dailyLoss : Double
init (id : Int, date: Date, weight : Double, measured : Bool, dailyLoss : Double) {
self.id = id
self.date = date
self.weight = weight
self.measured = measured
self.dailyLoss = dailyLoss
}
}
It is sorted on the date, starting from day 1, as you can see in View 1. What I would like to achieve is View 2... so the List should be presented on the current day.
Related
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()
}
}
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")
]
}
}
}
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 can I get the first date and the last date from the ClosedRange? I tried using myDateRange.First, but it give me an error saying
Referencing property 'first' on 'ClosedRange' requires that 'Date' conform to 'Strideable'
Here is what I have:
#State private var myDateClosedRange: ClosedRange<Date>? = nil
VStack {
MultiDatePicker(sampleDateClosedRange: self.$myDateClosedRange)
if let myDateRange = myDateClosedRange {
Text("\(myDateRange)").padding()
//Print(myDateRange.First)
} else {
Text("Select two dates").padding()
}
}
I think you're looking for upperBound and lowerBound.
let dateRange : ClosedRange<Date> = Date()...Date().addingTimeInterval(60)
dateRange.upperBound
dateRange.lowerBound
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
}
}