So i have created a class for a Day and for a Drink. and I'm trying to track how much you drink in a day, but I'm struggling with saving multiple days. I'm currently managing to save the current day(with the amount drunk that day) but i don't know how to save more than one day.
I want to save an array of type Day with all the days. how can i do this?
This is my Day class:
public class Day: NSObject {
var date: Date
var goalAmount: Drink
var consumedAmount: Drink
func saveDay() {
let formatting = DateFormatter()
formatting.dateFormat = "EEEE - dd/mm/yy"
UserDefaults.standard.set(formatting.string(from: date), forKey: "date")
UserDefaults.standard.set(goalAmount.amountOfDrink, forKey: "goal")
UserDefaults.standard.set(consumedAmount.amountOfDrink, forKey: "consumed")
}
func loadDay() {
let rawDate = UserDefaults.standard.value(forKey: "date") as? String ?? ""
let formatter = DateFormatter()
formatter.dateFormat = "EEEE - dd/mm/yy"
date = formatter.date(from: rawDate)!
goalAmount.amountOfDrink = UserDefaults.standard.float(forKey: "goal")
consumedAmount.amountOfDrink = UserDefaults.standard.float(forKey: "consumed")
}
}
This is my Drink class:
class Drink: NSObject {
var typeOfDrink: String
var amountOfDrink: Float
}
i am calling saveDay() when there are any changes made to the day, and then loadDay() when the app opens.
A better approach would be is to store the object of the class in userDefaults instead of storing particular properties of that class. And use [Date] instead of Date to save multiple days
For this first, you have Serialize the object to store in userDefaults and Deserialize to fetch the data from userDefaults.
import Foundation
class Day: Codable {
var date = Date()
var goalAmount: Drink
var consumedAmount: Drink
init(date: Date, goalAmount: Drink,consumedAmount: Drink ) {
self.date = date
self.goalAmount = goalAmount
self.consumedAmount = consumedAmount
}
static func saveDay(_ day : [Day]) {
do {
let object = try JSONEncoder().encode(day)
UserDefaults.standard.set(object, forKey: "days")
} catch {
print(error)
}
}
static func loadDay() {
let decoder = JSONDecoder()
if let object = UserDefaults.standard.value(forKey: "days") as? Data {
do {
let days = try decoder.decode([Day].self, from: object)
for day in days {
print("Date - ", day.date)
print("Goal Amount - ", day.goalAmount)
print("Consumed Amount - ",day.consumedAmount)
print("----------------------------------------------")
}
} catch {
print(error)
}
} else {
print("unable to fetch the data from day key in user defaults")
}
}
}
class Drink: Codable {
var typeOfDrink: String
var amountOfDrink: Float
init(typeOfDrink: String,amountOfDrink: Float ) {
self.typeOfDrink = typeOfDrink
self.amountOfDrink = amountOfDrink
}
}
Use saveAndGet() method to store and fetch details from userDefaults
func saveAndGet() {
// use any formats to format the dates
let date = Date()
let goalAmount = Drink(typeOfDrink: "Water", amountOfDrink: 5.0)
let consumedAmount = Drink(typeOfDrink: "Water", amountOfDrink: 3.0)
let day1 = Day(date: date, goalAmount: goalAmount, consumedAmount: consumedAmount)
let day2 = Day(date: date, goalAmount: goalAmount, consumedAmount: consumedAmount)
let day3 = Day(date: date, goalAmount: goalAmount, consumedAmount: consumedAmount)
let day4 = Day(date: date, goalAmount: goalAmount, consumedAmount: consumedAmount)
let days = [day1, day2, day3, day4]
Day.saveDay(days)
Day.loadDay()
}
1) You need to create array of object for this :
goalAmount = [Drink]()
var date = [Date]()
and append with each new element.
you can also add date variable inside your drink class.
2) you can also create array of dictionary:
var userData = [String : Any]()
key will be you date and Any contain related to drink data in Any you can store Anything.
Related
hi everyone I have this structure that I use to save data on firebase ... As you can see I have a var reservation_date: Date field
Firebase obviously saves a date with the time but I would need only the date without the time to be saved how can I get this?
This is how I save me
struct Reservations: Identifiable, Codable {
#DocumentID var id: String?
var reservation_date: Date
var user: String
#ExplicitNull var userID: String?
var reservation_id: String
#FirebaseFirestoreSwift.ServerTimestamp var createdAt: Timestamp?
enum CodingKeys: String, CodingKey {
case id
case reservation_date
case user
case userID
case reservation_id
case createdAt
}
static let sample = Reservations(reservation_date: Date(), user: "", userID: "", reservation_id: "")
}
// MARK: - Firebase
extension ReservationViewModel {
func addReservation() {
guard let newDate = Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: selectedDate) else {
print("Non è stato possibile creare una data di prenotazione valida")
return }
let dBPath = db.collection("Prenotazioni")
.document(newDate.formatted(.dateTime.year()))
.collection(newDate.formatted(.dateTime.month(.wide).locale(Locale(identifier: "it"))))
.document(newDate.formatted(.dateTime.day(.twoDigits)))
.collection("Ore \(newDate.formatted(.dateTime.hour(.twoDigits(amPM: .omitted)).locale(Locale(identifier: "it"))) + ":" + newDate.formatted(.dateTime.minute(.twoDigits)))")
do {
let _ = try dBPath.addDocument(from: reservation)
}
catch {
print(error)
}
}
}
If you're only interested in the date value then I would recommend using a string that conforms to the ISO 8601 standard, which in this particular case would just be yyyy-mm-dd (ex: "2022-02-13"). Swift has a ready-made ISO-8601 formatter exactly for this.
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate]
// From String to Date
let reservationDay = "2022-03-25" // save this in your database
if let date = formatter.date(from: reservationDay) {
print(date) // Swift Date object
}
// From Date to String
let reservationDate = Date()
let day = formatter.string(from: reservationDate)
print(day) // String "2022-02-13"
Be mindful of timezones, however, because the default is GMT and you will want to use GMT throughout the conversions (and then render the local timezone value) or the local timezone throughout the conversions (just don't mix them as you convert). To always deal in the local timezone you can set its property on the formatter:
formatter.timeZone = .current
I would like to sort my data by date from newest down to oldest. I am getting Transaction objects from Firestore in the form of "2020-12-29". I have seen previous answers on how to sort arrays by date but I am unsure how I can sort it with my current object structure.
func loadTransactions(){
if let catId = self.categoryId{
guard let user = Auth.auth().currentUser?.uid else { return }
db.collection("users").document(user).collection("Transactions")
.whereField("catId", isEqualTo: catId)
.getDocuments() {
snapshot, error in
if let error = error {
print("\(error.localizedDescription)")
} else {
self.budgetData.removeAll()
for document in snapshot!.documents {
let data = document.data()
let title = data["name"] as? String ?? ""
let date = data["date"] as? String ?? ""
let amount = data["amount"] as? Double ?? 0
let id = data["transId"] as? String ?? ""
let absolute = abs(amount)
let trans = Transaction(transId: id, catId:catId,title: title, dateInfo: date, image: UIImage.grocIcon, amount: absolute)
self.budgetData.append(trans)
// let testArray = ["25 Jun, 2016", "30 Jun, 2016", "28 Jun, 2016", //"2 Jul, 2016"]
// var convertedArray: [Date] = []
// var dateFormatter = DateFormatter()
// dateFormatter.dateFormat = "dd MM, yyyy"// yyyy-MM-dd"
// for dat in testArray {
// let date = dateFormatter.date(from: dat)
// if let date = date {
// convertedArray.append(date)
// }
// }
// var ready = convertedArray.sorted(by: { $0.compare($1) == .orderedDescending })
var tranSet = Set<Transaction>()
self.budgetData = self.budgetData.filter { (transaction) -> Bool in
if !tranSet.contains(transaction){
tranSet.insert(transaction)
return true
}
return false
}
DispatchQueue.main.async {
self.setTotalAmountOfCats()
self.tableView.reloadData()
}
}
}
}
}
}
You can sort the data in your Firebase query using order(by:): https://firebase.google.com/docs/firestore/query-data/order-limit-data
In your example, it'll probably look like:
db.collection("users").document(user).collection("Transactions")
.whereField("catId", isEqualTo: catId)
.order(by: "date")
If you implemented this, you could get rid of your tranSet and just append the Transactions in order (this is out of scope of the question, but I'd probably look at compactMap to transform snapshot.documents instead of appending them in order).
This assumes that your dates are in the format you listed (YYYY-MM-DD), which will alphabetize correctly, even though they aren't actually date/timestamp objects. If your data is in the format in your commented-out testArray section, than a different method would have to be used.
Essentially I have a tableview populated using JSON data, the tableview contains sections that groups the data using allowdate from the JSON.
allowdate as seen below is contains a date but is not formatted numerically instead looks like: March 26th 2020 so it makes difficult to control the order it is displayed in the tableview.
In the function fetchJSON I do:
self.structure.sort { $1. allowdate < $0.allowdate }
But this does not work correctly, and fails to put a date for example in January above one in March.
var sections = [TableSection]()
var structure = [TableStructure]()
private func fetchJSON() {
guard let url = URL(string: "\(URL.url)example"),
let value = name.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)
else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = "item1=\(value)&item2=\(value)".data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data else { return }
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
self.structure.sort { $1. allowdate < $0.allowdate }
let res = try decoder.decode([TableStructure].self, from: data)
let grouped = Dictionary(grouping: res, by: { $0. allowdate })
let keys = grouped.keys.sorted()
self.sections = keys.map({TableSection(date: $0, items: grouped[$0]!)})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
catch {
print(error)
}
}.resume()
}
JSON:
[
{
"person": "Jack",
"allowdate": "March 26th 2020",
"ready_time": "10:00 am"
}
]
To decode this JSON I am using the following structure:
struct TableSections {
let date : String
var items : [TableStructure]
}
struct TableStructure: Decodable {
let person: String
let allowdate: String
let ready_time: String
enum CodingKeys : String, CodingKey {
case person, allowdate, ready_time
}
}
Check this URL:
https://nsdateformatter.com
It will help you you to try different DateFormatter.
Now for your scenario, Try changing the struct like this:
struct TableStructure: Decodable {
let person: String
let allowdate: Date?
let ready_time: String
enum CodingKeys : String, CodingKey {
case person, allowdate, ready_time
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.person = try values.decode(String.self, forKey: .person)
self.ready_time = try values.decode(String.self, forKey: .ready_time)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMMM d'th',yyyy"
let allowDateStringValue = try values.decode(String.self, forKey: .allowdate)
self.allowdate = dateFormatter.date(from: allowDateStringValue)
}
}
and you can sort the array as:
structure.sort { (firstStructure, secondStructure) -> Bool in
if let firstDate = firstStructure.allowDate, let secondDate = secondStructure.allowDate {
return firstDate.timeIntervalSince1970 < secondDate.timeIntervalSince1970
}
return false
}
If you don't want to change your struct format then you can go like this:
struct TableStructure: Decodable {
let person: String
let allowdate: String
let ready_time: String
enum CodingKeys : String, CodingKey {
case person, allowdate, ready_time
}
}
and while sorting :
var dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMMM d'th',yyyy"
structure.sort { (firstStructure, secondStructure) -> Bool in
if let firstDate = dateFormatter.date(from: firstStructure.allowDate),
let secondDate = dateFormatter.date(from: secondStructure.allowDate) {
return firstDate.timeIntervalSince1970 < secondDate.timeIntervalSince1970
}
return false
}
I tried this in a playground. It seems to work with different ordinals:
import UIKit
let json = """
[
{
"person": "Jack",
"allowdate": "March 26th 2020",
"ready_time": "10:00 am"
},
{
"person": "Jill",
"allowdate": "January 1st 2020",
"ready_time": "1:00 pm"
},
{
"person": "Don",
"allowdate": "January 10th 2020",
"ready_time": "1:25 pm"
}
]
"""
struct TableStructure: Decodable {
// These first three come frm the json
let person: String
let allowdate: String
let readyTime: String
// We'll calculate this one later
let compareDate: Date
// This date formatter will read in dates like "January 10th 2020"
// So to use this, we will need to pull the ordinal letters off first
static let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM dd yyyy"
return dateFormatter
}()
enum DecodeError: Error {
case compareDateError
}
enum CodingKeys: String, CodingKey {
case person
case allowdate
case readyTime
}
// We decode the three key/values that are passed down, and we calculate a date that we can use to compare
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
person = try values.decode(String.self, forKey: .person)
readyTime = try values.decode(String.self, forKey: .readyTime)
allowdate = try values.decode(String.self, forKey: .allowdate)
if let date = TableStructure.comparingDate(from: allowdate) {
compareDate = date
} else {
throw DecodeError.compareDateError
}
}
// We pull the ordinal letters off of the date, and are left with something cleaner
// A regex could make this much simpler, but me and regex's, we don't get along so great
static func comparingDate(from dateString: String) -> Date? {
var datePurgedArray = dateString.split(separator: " ")
if datePurgedArray.count == 3 {
datePurgedArray[1] = datePurgedArray[1].filter("0123456789.".contains)
let newDateString = datePurgedArray.joined(separator:" ")
print(newDateString)
return TableStructure.dateFormatter.date(from: newDateString)
} else {
return nil
}
}
}
if let jsonData = json.data(using: .utf8) {
print("json Data is \(jsonData)")
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let array = try decoder.decode([TableStructure].self, from: jsonData).sorted { $0.compareDate < $1.compareDate }
print("array is \(array)")
} catch {
print(error.localizedDescription + "could not decode array")
}
} else {
print("could not get data from json")
}
The string date format cannot be sorted reliably.
You have to decode the date string as Date. That's what the date decoding strategies are designed for.
There is one problem: DateFormatter doesn't support literal ordinal date strings like 1st, you have to remove st, nd, rd, th. The most efficient way is replacingOccurrences with regularExpression option.
Declare the struct (once again the CodingKeys are not needed and name the struct members camelCased)
struct TableStructure: Decodable {
let person: String
let allowdate: Date
let readyTime: String
}
and add the strategy
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .custom({ decoder -> Date in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
let trimmedString = dateString.replacingOccurrences(of: "(st|nd|rd|th) ", with: " ", options: .regularExpression)
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "MMMM dd yyyy"
guard let result = formatter.date(from: trimmedString) else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Wrong date format")}
return result
})
self.structure.sort { $1. allowdate < $0.allowdate }
let res = try decoder.decode([TableStructure].self, from: data)
let grouped = Dictionary(grouping: res, by: { $0. allowdate })
let keys = grouped.keys.sorted()
self.sections = keys.map({TableSection(date: $0, items: grouped[$0]!)})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Consider that a Date is printed with a default date format. If you need a custom date format you have to add a second date formatter
I have an array of Person. The Person object has many fields between them inscriptionDate (a timestamp). I have to create a collectionView from this array but using sections. Every section has a header that is inscriptionDate as a date having this format dd/mm/yyyy. I have to sort the array by inscriptionDate but without time (only the format dd/mm/yyyy) in order to load the data in the collectionView (taking into consideration the sections). I have found from another question this solution. But how can I sort the array before doing this? How can I use this:
order = NSCalendar.currentCalendar().compareDate(now, toDate: olderDate,
toUnitGranularity: .Day)
in my case?
I agree with #Vasilii Muravev, you will need to clean your timestamp object first, either using extensions or functions. Timestamp isn't a valid Swift object btw, unless that is a custom class you created.
Then you can create a dictionary for your dataSource. I will use #Vasilii Muravev's extension:
//var myKeys : [Date] = []
let sortedPeople = persons.sorted { $0.inscriptionDate.noTime() < $1.inscriptionDate.noTime() }
//break your array into a dictionary([Date : [Person]])
//personsSortedByDateInSections : [Date : [Person]] = [:]
for person in sortedPeople {
if personsSortedByDateInSections[person.inscriptionDate] != nil {
personsSortedByDateInSections[person.inscriptionDate]!.append(person)
} else {
personsSortedByDateInSections[person.inscriptionDate] = [person]
}
}
myKeys = setKeyArray(personSortedByDateInSections.keys)
that will give you a dictionary object with all of your Person objects grouped(sectioned) by their inscriptionDate. Then you will just need to fill out your collectionView delegate and datasource methods.
override func numberOfSections(in: UICollectionView) -> Int {
return myKeys.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return personsSortedByDateInSections[myKeys[section]].count
}
UPDATE:
As stated in your comment there is a issue with grabbing a array of the keys with swift dictionaries(I don't think swift dictionaries had this issue in earlier version? I could be wrong)....anyway to workaround this I have used this function to set a class variable for the 'keyArray'.:
fileprivate func setKeysArray(_ keys: LazyMapCollection<Dictionary<Date, [Person]>, String) -> [Date]{
var keysArray = [Date]()
for key in keys {
keysArray.append(key)
}
return keysArray
}
First, you'll need to clean timestamp before the sorting. You can do that by using Calendar and Date extension:
extension Date {
func noTime() -> Date! {
let components = Calendar.current.dateComponents([.day, .month, .year], from: self)
return Calendar.current.date(from: components)
}
}
Then you'll just need to sort your array by date without time:
let sortedByDate = persons.sorted { $0.inscriptionDate.noTime() < $1.inscriptionDate.noTime() }
Note. Be careful with compareDate function of Calendar, since it comparing only specific component. If in this example: NSCalendar.currentCalendar().compareDate(now, toDate: olderDate, toUnitGranularity: .Day) you'll have same days in different months, the comparing result will show that dates are equal.
Assuming your inscriptionDate is a Date, why can't you sort on that? Since Date conforms to Comparable all you need is
let sortedByDate = persons.sorted { $0.inscriptionDate < $1.inscriptionDate }
I wright a code for you I thing this code useful for you.
//
// ViewController.swift
// sortData
//
// Created by Bijender singh on 26/01/18.
// Copyright © 2018 Bijender singh. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewDidAppear(_ animated: Bool) {
let myDataArray = NSMutableArray()
var myDataDic = NSMutableDictionary()
myDataDic.setValue("Bijender", forKey: "name")
myDataDic.setValue("1516965600", forKey: "date")
myDataArray.add(myDataDic)
myDataDic = NSMutableDictionary()
myDataDic.setValue("ben", forKey: "name")
myDataDic.setValue("1516965540", forKey: "date")
myDataArray.add(myDataDic)
myDataDic = NSMutableDictionary()
myDataDic.setValue("Deke", forKey: "name")
myDataDic.setValue("1516842180", forKey: "date")
myDataArray.add(myDataDic)
myDataDic = NSMutableDictionary()
myDataDic.setValue("Veer", forKey: "name")
myDataDic.setValue("1516842000", forKey: "date")
myDataArray.add(myDataDic)
myDataDic = NSMutableDictionary()
myDataDic.setValue("Ashfaq", forKey: "name")
myDataDic.setValue("1515981900", forKey: "date")
myDataArray.add(myDataDic)
print(myDataArray)
let sortedArray = self.sortDataWithDate(arrayData: myDataArray)
// sortedArray contane array which contan same date array
// you can use it according to you
// sortedArray.count is total diffrent dates in your array
// and (sortedArray.count[i] as! NSArray).count give you count of data of that date (0 < i < sortedArray.count)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func sortDataWithDate(arrayData : NSArray) -> NSArray {
// var chat: [ChatMessage]!
let returnArray = NSMutableArray()
var subArray = NSMutableArray()
let arrayDate = NSMutableArray()
for i in 0 ..< arrayData.count {
let msgDate = self.timeStampToDate(_timestamp: ((arrayData[i] as! NSDictionary).object(forKey: "date") as! String), _dateFormat: "dd/MM/yyyy")
print("dddt \(msgDate)")
// print("date array \(arrayDate) $$ msgDate \(msgDate)")
if arrayDate.contains(msgDate) {
subArray.add(arrayData[i])
}
else{
arrayDate.add(msgDate)
if arrayDate.count > 1 {
returnArray.add(subArray)
}
subArray = NSMutableArray()
subArray.add(arrayData[i])
}
}
if subArray != nil {
returnArray.add(subArray)
}
print(returnArray)
return returnArray
}
func timeStampToDate(_timestamp : String, _dateFormat : String) -> String{
// _dateFormat = "yyyy-MM-dd HH:mm:ss.SSSS"
var date = Date(timeIntervalSince1970: TimeInterval(_timestamp)!)
date += TimeInterval(Int(TimeZone.current.secondsFromGMT()) as NSNumber)
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone(abbreviation: "GMT") //Set timezone that you want
dateFormatter.locale = NSLocale.current
dateFormatter.dateFormat = _dateFormat
let strDate = dateFormatter.string(from: date)
return strDate
}
}
In this code I insert array as hard coded
(
{
date = 1516965600;
name = Bijender;
},
{
date = 1516965540;
name = ben;
},
{
date = 1516842180;
name = Deke;
},
{
date = 1516842000;
name = Veer;
},
{
date = 1515981900;
name = Ashfaq;
}
)
And out put array is
(
(
{
date = 1516965600;
name = Bijender;
},
{
date = 1516965540;
name = ben;
}
),
(
{
date = 1516842180;
name = Deke;
},
{
date = 1516842000;
name = Veer;
}
),
(
{
date = 1515981900;
name = Ashfaq;
}
)
)
I am trying to get the objects from realm, where newDate is later then firstDate. So if the date from firstDate is 05.10.2017, it will get objects after that date, example 06.10.2017, but not 04.10.2017.
This is how I am storing the date:
class User: Object {
#objc dynamic var firstDate = Date()
#objc dynamic var newDate = Date()
}
This is how I am saving the objects:
let date = Date()
let realm = try! Realm()
let myUser = User()
myUser.firstDate = self.date
This is how I am trying to retrieve the objects:
var userData: Results<User>?
if (homeIndexPathRow == 0) {
let getData = realm.objects(User.self).filter("firstDate > newDate")
userData = getData
print("userData", userData!)
}
When trying to retrieve the objects, the app crashes.. Is it something wrong with the filter format?
Try this:
var yourNSDate = NSDate()
let predicate = NSPredicate(format: "firstDate < %#", yourNSDate)
let dataResults = realm.objects(User.self).filter(predicate)
userData = dataResults
Replace that with the code below if (homeIndexPathRow == 0) { ...