Check Same Dates Inside ForEach Swift - swift

How can I check if item 1 date is equal to item 2 date inside the foreach. Can I check the same dates inside the predicate like below without using 2 foreach loops?
struct TimedView: View {
static let taskDateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
#State var releaseDate = Date()
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest var reminder: FetchedResults<CDReminder>
init(){
var calendar = Calendar.current
calendar.timeZone = NSTimeZone.local
let dateFrom = calendar.startOfDay(for: Date()) // eg. 2016-10-10 00:00:00
let dateTo = calendar.date(byAdding: .day, value: 1, to: dateFrom)
let predicate = NSPredicate(format : "date >= %# AND date == %#", dateFrom as CVarArg)
self._reminder = FetchRequest(
entity: CDReminder.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CDReminder.date, ascending: false)],
predicate: predicate)
}
var body: some View {
NavigationView {
VStack{
HStack{
Text("Zamanlanmis")
.font(.system(size: 40, weight: .bold, design: .rounded))
.foregroundColor(.red)
Spacer()
}
.padding(.leading)
List{
ForEach(reminder, id: \.self) { item in
DatedReminderCell(reminder: item, isSelected: false, onComplete: {})
.foregroundColor(.black)
}
}
}
}
}
Now my output is coming like this

Date objects are only equal if they represent the exact same instant, down to the fraction of a second.
If you want to see if a date falls on a give month/day/year, you will need to use a Calendar object to generate beginning_of_day and end_of_day dates and see if each date fall between them.
This thread includes several examples of that. Most of the code is in Objective-C but it should be pretty easy to translate the approach to Swift, if not the exact code.

To solve this complex problem I created a helper file called ScheduledViewHelper. Inside this helper, I have created a new array that has two elements(reminder and date). Inside this helper first I fetched all the reminders from core data then I compared all their date data and when I find a new date I append the date element inside the array. Finally, I sorted those dates and return [ReminderList] to be able to get this array from my ScheduledView.
My ScheduledViewHelper is
public struct ReminderList: Hashable {
let date: String
let reminder: [CDReminder]
}
open class ScheduledViewHelper: NSObject {
public static let shared = ScheduledViewHelper()
private let viewContext = PersistenceController.shared.container.viewContext
private override init() {
super.init()
}
private func getReminders() -> [CDReminder] {
let request: NSFetchRequest<CDReminder> = CDReminder.fetchRequest()
do {
let items = try viewContext.fetch(request) as [CDReminder]
return items
} catch {
print("Error occured: ", error)
}
return []
}
public func getScheduledData() -> [ReminderList] {
let calendar = Calendar.current
var list: [ReminderList] = []
var dateList: [String] = []
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy"
let reminders = getReminders()
reminders.forEach { (reminder) in
let date = dateFormatter.string(from: reminder.date!)
if !dateList.contains(date) && date >= dateFormatter.string(from: Date()) {
dateList.append(date)
}
}
dateList.forEach { (date) in
let dateFrom = calendar.startOfDay(for: dateFormatter.date(from: date)!) // eg. 2016-10-10 00:00:00
let dateTo = calendar.date(byAdding: .day, value: 1, to: dateFrom)
let predicate = NSPredicate(format: "date <= %# AND date >= %#", dateTo! as NSDate, dateFrom as NSDate)
let request: NSFetchRequest<CDReminder> = CDReminder.fetchRequest()
request.predicate = predicate
do {
let items = try viewContext.fetch(request) as [CDReminder]
let newItem = ReminderList(date: date, reminder: items)
list.append(newItem)
} catch {
print("Error occured: ", error)
}
}
return list.sorted {
$0.date < $1.date
}
}
private func saveContext() {
do {
try viewContext.save()
} catch {
print("Error occured: ", error)
}
}
}
I added to my ScheduledView those lines:
created a new var before init() statement
#State var remindersList : [ReminderList] = []
just under NavigationView I initialized remindersList with onAppear
.onAppear {
remindersList = ScheduledViewHelper.shared.getScheduledData()
}
Reorganized my List like this
List{
ForEach(remindersList, id: .self) { item in
VStack(alignment: .leading) {
Text(item.date)
ForEach(item.reminder, id: .self) { reminder in
DatedReminderCell(reminder: reminder, isSelected: false, onComplete: {})
}
}
.foregroundColor(.black)
}
}

Related

Strings Dictionary with dynamic units

I would like to dynamically specify the unit to be used (e.g. hours, minutes, seconds) in the Strings Dictionary when called:
Text("\(unit) \(value))", tableName: "SingularAndPlural")
unit contains the unit as a string
value contains the value as an integer
But that doesn't work, it doesn't resolve.
I've tried all possible variants, but I can't get any further.
Rather than use stringsDict you should probably use localisation provided by the built-in Formatters. In this case DateComponentsFormatter probably does what you need:
struct ContentView: View {
var body: some View {
List {
ForEach(["en", "ru", "el", "th"], id: \.self) { localeId in
Section(localeId) {
ForEach(0..<3, id: \.self) { i in
HStack {
Text(formatter(localeId).string(from: DateComponents(minute: i))!)
Spacer()
Text(formatter(localeId).string(from: DateComponents(second: i))!)
}
}
}
}
}
}
func formatter(_ localeId: String) -> DateComponentsFormatter {
var calendar = Calendar.current
calendar.locale = Locale(identifier: localeId)
let formatter = DateComponentsFormatter()
formatter.calendar = calendar
formatter.unitsStyle = .full
return formatter
}
}
It's even simpler if you just want to format for the device's current locale:
struct ContentView: View {
var body: some View {
List {
ForEach(0..<3, id: \.self) { i in
HStack {
Text(DateComponents(minute: i), formatter: Self.formatter)
Spacer()
Text(DateComponents(second: i), formatter: Self.formatter)
}
}
}
}
static var formatter: DateComponentsFormatter {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
return formatter
}
}

How can I initialize SwiftUI DatePicker .hourAndMinute element?

I am trying to initialize this "WakeUpDate" date element so that the default displayed value is 10:00 AM. This date picker is HourandMinute only and is being stored in userdefaults.
I tried to init the date element but it is not building. With this error: Cannot assign value of type 'State<Date>' to type 'Published<Date>'
UserData: Currently, the following init does not build
import Foundation
import SwiftUI
class UserData: ObservableObject {
init() {
_wakeUpTime = State<Date>(initialValue: Calendar.current.date(DateComponents(Hour: 10)) ?? Date())
}
#Published var wakeUpTime: Date = UserDefaults.standard.object(forKey: "wakeUpTime") as? Date ?? Date() {
didSet {
UserDefaults.standard.set(self.wakeUpTime, forKey: "wakeUpTime")
}
}
}
SettingsDetail: Where the DatePicker is being selected:
struct SettingsDetailView: View {
#ObservedObject var userData: UserData
var body: some View {
Form{
DatePicker("Select a new time", selection: $userData.wakeUpTime, displayedComponents: .hourAndMinute)
}
}
}
MainSettings: Where the selected DatePicker Date is being displayed:
import SwiftUI
import UserNotifications
struct SettingsView: View {
#ObservedObject var userData = UserData()
static var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "hh:mm a"
return formatter
}()
var body: some View {
NavigationView {
VStack (alignment: .center, spacing: 0) {
Form{
Section(header: Text("NOTIFICATION SETTINGS")) {
HStack {
Text("Current Notification Time")
.foregroundColor(Color("MainText"))
Spacer()
Text("\(self.userData.wakeUpTime, formatter: SettingsView.self.dateFormatter)")
}
}
}
}
}
}
}
EDIT I tried initializing UserData like this, but now when I pick a new time with the date picker and quit the app, the new time is gone and 5PM (the init time) is displayed again.
import Foundation
import SwiftUI
class UserData: ObservableObject {
init() {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm"
if let date = dateFormatter.date(from:"17:00") {
wakeUpTime = date
}
}
#Published var wakeUpTime: Date = UserDefaults.standard.object(forKey: "wakeUpTime") as? Date ?? Date() {
didSet {
UserDefaults.standard.set(self.wakeUpTime, forKey: "wakeUpTime")
}
}
}
How can I run init only on the first launch, and be removed once the selected time has been picked with the datepicker?
I figured it out by doing this:
class UserData: ObservableObject {
#AppStorage("userdatahaslaunched") var userdatahaslaunched = false
init() {
if !userdatahaslaunched {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm"
if let date = dateFormatter.date(from:"10:00") {
wakeUpTime = date
}
userdatahaslaunched = true
}
}
If your intent is to let the user initialize a wake up time you should always expect a date after now. So what you are looking for is calendar method nextDate(after:) and you can match the desired components (hour and minute). Note that you don't need to include the minutes component when calling this method if it is zero.
let date = Calendar.current.nextDate(after: Date(), matching: .init(hour: 10), matchingPolicy: .strict)!
print(date.description(with: .current))

Thread 1: Fatal error: Can't remove last element from an empty collection SwiftUI

That is the link of the project https://github.com/m3rtkoksal/WalkerCoin
I am trying to reach all the historical step counts but when I change startDate value from -7 to something bigger than -7 I am getting Fatal error: Can't remove first element from an empty collection: file Swift/RangeReplaceableCollection.swift, line 624
I allowed all the necessary permissions from info.plist and added Healthkit from signing&capabilities.
If I try only 7 days back it works but when I increase that value it crashes.
import SwiftUI
import HealthKit
struct StepView: View {
private var healthStore: HealthStore?
#State private var selectedDay = Step(count: 0, date: Date())
#State private var steps: [Step] = [Step]()
init() {
healthStore = HealthStore()
}
private func updateUIFromStatistics(_ statisticsCollection: HKStatisticsCollection) {
steps = []
let now = Date()
let offset = -7
let startDate = Calendar.current.date(byAdding: .day, value: offset, to: Date())!
statisticsCollection.enumerateStatistics(from: startDate, to: now) { (statistics, stop) in
let count = statistics.sumQuantity()?.doubleValue(for: .count())
let step = Step(count: Int(count ?? 0), date: statistics.startDate)
steps.append(step)
}
}
var body: some View {
ZStack(alignment: .leading) {
Image("stepsTabBG")
.resizable()
.ignoresSafeArea(.all)
VStack {
HStack {
ScrollView(.horizontal) {
HStack(spacing: 30) {
ForEach(steps, id: \.id) { day in
Text("\(Calendar.current.dateComponents([.day], from: day.date).day ?? 0 )")
.foregroundColor(self.selectedDay.date == day.date ? Color.red : Color.black)
.onTapGesture {
selectedDay = day
}
}
}
}
.frame(width: UIScreen.main.bounds.width / 2)
.padding(10)
Spacer()
}
CircularProgress(steps: selectedDay.count)
and this is my HealthStore
import HealthKit
extension Date {
static func mondayAt12AM() -> Date {
return Calendar(identifier: .iso8601).date(from: Calendar(identifier: .iso8601).dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))!
}
}
class HealthStore {
var healthStore: HKHealthStore?
var query: HKStatisticsCollectionQuery?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
func calculateSteps(completion: #escaping (HKStatisticsCollection?) -> Void) {
let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
let offset = -7
let startDate = Calendar.current.date(byAdding: .day, value: offset, to: Date())!
let anchorDate = Date.mondayAt12AM()
let daily = DateComponents(day: 1)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate)
query = HKStatisticsCollectionQuery(quantityType: stepType, quantitySamplePredicate: predicate, options: .cumulativeSum, anchorDate: anchorDate, intervalComponents: daily)
query!.initialResultsHandler = { query, statisticsCollection, error in
completion(statisticsCollection)
}
if let healthStore = healthStore, let query = self.query {
healthStore.execute(query)
}
}
func requestAuthorization(completion: #escaping (Bool) -> Void) {
let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
guard let healthStore = self.healthStore else { return completion (false) }
healthStore.requestAuthorization(toShare: [], read: [stepType]) { (success, error) in
completion(success)
}
}
}
And that is the error description after the crash.
statisticsCollection.enumerateStatistics(from: startDate, to: now) { (statistics, stop) in
let count = statistics.sumQuantity()?.doubleValue(for: .count())
let step = Step(count: Int(count ?? 0), date: statistics.startDate)
DispatchQueue.main.async {
steps.append(step)
}
}
Try adding your append statement inside DispatchQueue.main.async. It worked for me. Hope it works for you as well.

Updating Text Using SwiftUI and FireStore

I store a value called month hours in my application that keeps track of the hours a person has used the apps and displays it in a line of text. The text if part of a stack in Swift UI, but I can't figure out how to make the text update once the information has been queried from I've tried quite a few ways of making this work from structs to classes to using #State.
This is just the latest thing I tried that didn't work if anyone can help that would be greatly appreciated.
let db = Firestore.firestore()
class Month {
var monthHours = "0"
func getMonthHours() {
db.addSnapshotListener(. //Im removing the actual query part to keep that private but the print statement below confirms the query is not the issue.
{ (docSnapShot, err) in
if let e = err {
print("There was an error retrieving the monthly hours:\n\(e.localizedDescription)")
} else {
let data = docSnapShot?.data()
if let h = data?[K.FStore.monthHoursField] as? Double {
self.monthHours = String(h.rounded())
print("These are the hours:\n\(self.monthHours)")
}
}
})
}
func getMonth() -> String {
let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
let result = formatter.string(from: date)
return result
}
init() {
getMonthHours()
}
}
struct ChartView : View {
#State private var month = Month()
//Struct variables
var body : some View {
ZStack {
Color(UIColor(named: K.BrandColors.grey)!).edgesIgnoringSafeArea(.all)
VStack {
Text("HOURS THIS MONTH \(month.monthHours)")
.font(.system(size: 18))
.fontWeight(.heavy)
}
}
}
This outlines one possible approach. The crux is to deal with the asynchronous function "getMonthHours". You need to wait till it is finished its fetching before you can use the results.
class Month {
var monthHours = "0"
// async fetch the month hours from Firestore, ... error handling todo
static func getMonthHours(handler: #escaping (String) -> Void) {
db.addSnapshotListener{ (docSnapShot, err) in
if let e = err {
print("There was an error retrieving the monthly hours:\n\(e.localizedDescription)")
return handler("") // should return some error here .... todo
} else {
if let data = docSnapShot?.data(),
let h = data?[K.FStore.monthHoursField] as? Double {
// return the result
return handler(String(h.rounded()))
} else {
return handler("") // should return some error here .... todo
}
}
}
}
func getMonth() -> String {
let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
let result = formatter.string(from: date)
return result
}
init() { }
}
struct ChartView : View {
#State private var monthHours = ""
var body : some View {
ZStack {
Color(UIColor(named: K.BrandColors.grey)!).edgesIgnoringSafeArea(.all)
VStack {
Text("HOURS THIS MONTH \(monthHours)")
.font(.system(size: 18))
.fontWeight(.heavy)
}
}.onAppear(perform: loadData)
}
}
func loadData() {
// when the fetching is done it will update the view
Month.getMonthHours() { hours in
self.monthHours = hours
}
}

UserDefaults insanity, take 2 with a DatePicker

Progressing on UserDefaults insanity in SwiftUI, after a previous post on basic UserDefaults which basically exposed the need to use a String() wrapper around UserDefaults values...
I am now stomped by the data flow :
The idea is to present a DatePicker, set to a UserDefaults value registered in AppDelegate on first launch.
Subsequently, the user picks another date that is set to the UserDefaults.
But every time I launch the app after having "killed" it (i.e swiped up from app switcher), the Picker displays the present date and NOT the one last saved in UserDefaults.
Also, I display some texts above and below the picker to try and make sense of the data flow, but it seems that there is a one step lag in the displaying of the dates, if anyone has the time to give it a try, here is the code :
1- In AppDelegate, I register my initial UserDefaults (like I always did in UIKit) :
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch
UserDefaults.standard.register(defaults: [
"MyBool 1": true,
"MyString": "Soo",
"MyDate": Date()
])
return true
}
2- in ContentView, I try to display them :
import SwiftUI
struct ContentView: View {
let defaults = UserDefaults.standard
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
// formatter.dateStyle = .long
formatter.dateFormat = "dd MMM yy"
return formatter
}
#State var selectedDate = Date()
init() {
self.loadData() // This should set "selectedDate" to the UserDefaults value
}
var body: some View {
VStack {
Text("The BOOL 1 value is : Bool 1 = \(String(defaults.bool(forKey: "MyBool 1")))")
Divider()
Text("My string says : \(String(defaults.string(forKey: "MyString")!))")
Divider()
Text("The date from UserDefaults is : \(dateFormatter.string(from: defaults.object(forKey: "MyDate") as! Date))")
Divider()
DatePicker(selection: $selectedDate, label: { Text("") })
.labelsHidden()
.onReceive([self.selectedDate].publisher.first()) { (value) in
self.saveDate()
}
Divider()
Text("The chosen date is : \(selectedDate)")
}
}
func loadData() {
selectedDate = defaults.object(forKey: "MyDate") as! Date
print("----> selected date in \"init\" from UserDefaults: \(dateFormatter.string(from: selectedDate) )) ")
}
private func saveDate() { // This func is called whenever the Picker sends out a value
UserDefaults.standard.set(selectedDate, forKey: "MyDate")
print("Saving the date to User Defaults : \(dateFormatter.string(from: selectedDate) ) ")
}
}
Any help would be appreciated !
here is my 2nd answer, if you want to update the text also...it is not "nice" and for sure not the best way, but it works (i have tested it)
struct ContentView: View {
let defaults = UserDefaults.standard
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
// formatter.dateStyle = .long
formatter.dateFormat = "dd MMM yy"
return formatter
}
#State var uiUpdate : Int = 0
#State var selectedDate : Date
#State var oldDate : Date = Date()
init() {
_selectedDate = State(initialValue: UserDefaults.standard.object(forKey: "MyDate") as! Date) // This should set "selectedDate" to the UserDefaults value
}
var body: some View {
VStack {
Text("The BOOL 1 value is : Bool 1 = \(String(defaults.bool(forKey: "MyBool 1")))")
Divider()
Text("My string says : \(String(defaults.string(forKey: "MyString")!))")
Divider()
Text("The date from UserDefaults is : \(dateFormatter.string(from: defaults.object(forKey: "MyDate") as! Date))")
.background(uiUpdate < 5 ? Color.yellow : Color.orange)
Divider()
DatePicker(selection: $selectedDate, label: { Text("") })
.labelsHidden()
.onReceive([self.selectedDate].publisher.first()) { (value) in
if self.oldDate != value {
self.oldDate = value
self.saveDate()
}
}
Divider()
Text("The chosen date is : \(selectedDate)")
}
}
func loadData() {
selectedDate = defaults.object(forKey: "MyDate") as! Date
print("----> selected date in \"init\" from UserDefaults: \(dateFormatter.string(from: selectedDate) )) ")
}
private func saveDate() { // This func is called whenever the Picker sends out a value
UserDefaults.standard.set(selectedDate, forKey: "MyDate")
print("Saving the date to User Defaults : \(dateFormatter.string(from: selectedDate) ) ")
uiUpdate = uiUpdate + 1
}
}
try this:
struct ContentView: View {
let defaults = UserDefaults.standard
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
// formatter.dateStyle = .long
formatter.dateFormat = "dd MMM yy"
return formatter
}
#State var selectedDate : Date
init() {
_selectedDate = State(initialValue: UserDefaults.standard.object(forKey: "MyDate") as! Date) // This should set "selectedDate" to the UserDefaults value
}
var body: some View {
VStack {
Text("The BOOL 1 value is : Bool 1 = \(String(defaults.bool(forKey: "MyBool 1")))")
Divider()
Text("My string says : \(String(defaults.string(forKey: "MyString")!))")
Divider()
Text("The date from UserDefaults is : \(dateFormatter.string(from: defaults.object(forKey: "MyDate") as! Date))")
Divider()
DatePicker(selection: $selectedDate, label: { Text("") })
.labelsHidden()
.onReceive([self.selectedDate].publisher.first()) { (value) in
self.saveDate()
}
Divider()
Text("The chosen date is : \(selectedDate)")
}
}
func loadData() {
selectedDate = defaults.object(forKey: "MyDate") as! Date
print("----> selected date in \"init\" from UserDefaults: \(dateFormatter.string(from: selectedDate) )) ")
}
private func saveDate() { // This func is called whenever the Picker sends out a value
UserDefaults.standard.set(selectedDate, forKey: "MyDate")
print("Saving the date to User Defaults : \(dateFormatter.string(from: selectedDate) ) ")
}
}
Every time you start the App, it re-register the defaults. You could use this:
if !UserDefaults.standard.bool(forKey: "first time only") {
UserDefaults.standard.register(defaults: [
"first time only": true,
"MyBool 1": true,
"MyString": "Soo",
"MyDate": Date()
])
}