Set not Removing Duplicate Dates - swiftui

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")
]
}
}
}

Related

Conditional use in view for SwiftUI

I have a model with data string of name and a bool of UE. I'm trying to display item.name whenever UE is true. My issue is when whenever I run the code the data doesn't seem to read the UE. I got an error displaying the item.UE as a text view. The data that I am getting it from is from a database the item.name works without the conditional.
struct AttendingUsersView: View {
#ObservedObject var model = UserViewModel()
var body: some View {
VStack {
List (model.list) { item in
if item.UE == true {
Text(item.name)
} else {
Text("This isnt working")
}
}
DismissButton
}
}
I've tried displaying the item.UE to see what it would display but I get an error saying "No exact matches in call to initializer".
UserViewModel file
class UserViewModel: ObservableObject {
#Published var list = [Username]()
func addData(name: String, UE: Bool) {
//get a reference to the database
let db = Firestore.firestore()
// Add a new document in collection "username"
db.collection("usernames").document(UserDefaults.standard.object(forKey: "value") as! String).setData([
// MARK: Change the parameters to the users inputed choices
"name": name,
"UE": UE
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
func getData() {
//get a reference to the database
let db = Firestore.firestore()
//Read the documents at a specific path
db.collection("usernames").getDocuments { snapshot, error in
//checking for errors
if error == nil {
//no errors
if let snapshot = snapshot {
// update
DispatchQueue.main.async {
// Get all the documents and create usernames
self.list = snapshot.documents.map { d in
//Create a Username
return Username(id: d.documentID, name: d["name"] as? String ?? "", UE: (d["UE"] != nil)) //cast as a string and if not found return as a empty string
}
}
}
} else {
//Handle the error
}
}
}
}
Username model
struct Username: Identifiable {
var id: String
var name: String
var ue: Bool
}
Try this, with fixes for your ue in your getData, and
in the view display.
struct ContentView: View {
var body: some View {
AttendingUsersView()
}
}
struct AttendingUsersView: View {
#StateObject var model = UserViewModel() // <-- here
var body: some View {
VStack {
List (model.list) { item in
if item.ue { // <-- here
Text(item.name)
} else {
Text("This is working ue is false")
}
}
// DismissButton
}
}
}
class UserViewModel: ObservableObject {
// for testing
#Published var list:[Username] = [Username(id: "1", name: "item-1", ue: false),
Username(id: "2", name: "item-2", ue: true),
Username(id: "3", name: "item-3", ue: false)]
func addData(name: String, UE: Bool) {
//get a reference to the database
let db = Firestore.firestore()
// Add a new document in collection "username"
db.collection("usernames").document(UserDefaults.standard.object(forKey: "value") as! String).setData([
// MARK: Change the parameters to the users inputed choices
"name": name,
"UE": UE
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
func getData() {
//get a reference to the database
let db = Firestore.firestore()
//Read the documents at a specific path
db.collection("usernames").getDocuments { snapshot, error in
//checking for errors
if error == nil {
//no errors
if let snapshot = snapshot {
// update
DispatchQueue.main.async {
// Get all the documents and create usernames
self.list = snapshot.documents.map { d in
//Create a Username
// -- here, ue:
return Username(id: d.documentID, name: d["name"] as? String ?? "", ue: (d["UE"] != nil)) // <-- here ue:
}
}
}
} else {
//Handle the error
}
}
}
}
struct Username: Identifiable {
var id: String
var name: String
var ue: Bool
}

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)
}
}

Exchange Rate Key Value Lookup With Weird JSON File Format

I need help with currency exchange rate lookup given a key (3 digit currency code). The JSON object is rather unusual with no lablels such as date, timestamp, success, or rate. The first string value is the base or home currency. In the example below it is "usd" (US dollars).
I would like to cycle through all the currencies to get each exchange rate by giving its 3 digit currency code and storing it in an ordered array.
{
"usd": {
"aed": 4.420217,
"afn": 93.3213,
"all": 123.104693,
"amd": 628.026474,
"ang": 2.159569,
"aoa": 791.552347,
"ars": 111.887966,
"aud": 1.558363,
"awg": 2.164862,
"azn": 2.045728,
"bam": 1.9541,
"bbd": 2.429065,
"bch": 0.001278
}
}
In a slightly different formatted JSON object I used the following loop to copy exchange rates to an ordered array.
for index in 0..<userData.rateArray.count {
currencyCode = currCode[index]
if let unwrapped = results.rates[currencyCode] {
userData.rateArray[index] = 1.0 / unwrapped
}
}
The follow code is the API used to get the 3 digit currency codes and the exchange rates (called via UpdateRates).
class GetCurrency: Codable {
let id = UUID()
var getCurrencies: [String : [String: Double]] = [:]
required public init(from decoder: Decoder) throws {
do{
print(#function)
let baseContainer = try decoder.singleValueContainer()
let base = try baseContainer.decode([String : [String: Double]].self)
for key in base.keys{
getCurrencies[key] = base[key]
}
}catch{
print(error)
throw error
}
}
}
class CurrencyViewModel: ObservableObject{
#Published var results: GetCurrency?
#Published var selectedBaseCurrency: String = "usd"
func UpdateRates() {
let baseUrl = "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api#1/latest/currencies/"
let baseCur = selectedBaseCurrency // usd, eur, cad, etc
let requestType = ".json"
guard let url = URL(string: baseUrl + baseCur + requestType) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do{
let decodedResponse = try JSONDecoder().decode(GetCurrency.self, from: data)
DispatchQueue.main.async {
self.results = decodedResponse
// this prints out the complete table of currency code and exchange rates
print(self.results?.getCurrencies["usd"] ?? 0.0)
}
} catch {
//Error thrown by a try
print(error)//much more informative than error?.localizedDescription
}
}
if error != nil {
//data task error
print(error!)
}
}.resume()
}
}
Thanks lorem ipsum for your help. Below is the updated ASI logic that copies the exchange rates to the rateArray using key/value lookups.
class CurrencyViewModel: ObservableObject{
#Published var results: GetCurrency?
#Published var rateArray = [Double] ()
init() {
if UserDefaults.standard.array(forKey: "rates") != nil {
rateArray = UserDefaults.standard.array(forKey: "rates") as! [Double]
}else {
rateArray = [Double] (repeating: 0.0, count: 160)
UserDefaults.standard.set(self.rateArray, forKey: "rates")
}
}
func updateRates(baseCur: String) {
...
DispatchQueue.main.async {
self.results = decodedResponse
// loop through all available currencies
for index in 0..<currCode.count {
currencyCode = currCode[index]
// spacial handling for base currency
if currencyCode == baseCur {
self.rateArray[index] = 1.0000
} else {
let homeRate = self.results?.getCurrencies[baseCur]
// complement and save the exchange rate
if let unwrapped = homeRate?[currencyCode] {
self.rateArray[index] = 1.0 / unwrapped
}
}
}
}
} catch {
//Error thrown by a try
print(error)//much more informative than error?.localizedDescription
}
}
if error != nil {
//data task error
print(error!)
}
}.resume()
}
}

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")
}
}

How to publish changes to a single object in a object array

I have the following classes
class ListItem: Identifiable {
var id: UUID
var name: String
var description: String
var isFavorite: Bool
var debugDescription: String {
return "Name: \(self.name) | Favorite?: \(self.isFavorite)"
}
public init(name: String) {
self.name = name
id = UUID()
self.description = "Some text describing why \(self.name.lowercased()) is awesome"
self.isFavorite = false
}
}
class ListItems: ObservableObject {
#Published var items: [ListItem]
let defaultAnimals = ["Ant", "Bear", "Cat", "Dog", "Elephant",
"Fish", "Giraffe", "Hyena", "Iguana", "Jackal", "Kingfisher", "Leopard", "Monkey"]
public init(animals: [String] = []) {
let animalList: [String] = animals.count > 0 ? animals : defaultAnimals
self.items = animalList.sorted {
$0.lowercased() < $1.lowercased()
}.map {
ListItem(name: $0.firstUppercased)
}
}
}
and the following image view in ContentView
struct ContentView: View {
#ObservedObject var list: ListItems = ListItems()
var body: some View {
List(list.items) {
animal in HStack {
// ...
Image(systemName: animal.isFavorite ? "heart.fill" : "heart").foregroundColor(.pink).onTapGesture {
let index = self.list.items.firstIndex { $0.id == animal.id } ?? -1
if (index >= 0) {
self.list.items[index].isFavorite = !animal.isFavorite
self.list.items = Array(self.list.items[0...self.list.items.count-1]) // <--
}
}
// ...
}
}
}
}
Everytime, the image view is tapped, I am basically reassigning the entire array like this so that the changes can be reflected in the UI
self.list.items = Array(self.list.items[0...self.list.items.count-1])
My question: How can I refactor my code to prevent reassigning the entire object array every time some object property changes?
I am fairly new to Swift & iOS development, not sure if I am missing something basic.
Declare ListItem as an struct instead of a class, this way the view will be notified when isFavorite changes. And just a little suggestion; you can use toggle to change the value of a boolean: self.list.items[index].isFavorite.toggle()