UserDefaults insanity, take 2 with a DatePicker - swift

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

Related

Check Same Dates Inside ForEach 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)
}
}

Linking ObservableObject to DatePicker in SwiftUI

I am new to Swift and all its frameworks. I have a JSON file with readings for each day of the year. I have made a Decodable struct for the reading and an ObservableObject class which stores the readings in an array. I have made the ObservableObject an #EnvironmentObject so it can be accessed in all views. Can I link the readings to the date picker so that selecting a date will take me to a detailed view?
import SwiftUI
struct CalendarView: View {
// this is where ObserveableObject is required
#EnvironmentObject var days: Days
#State private var date = Date()
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .long
formatter.dateFormat = "LLLL d"
return formatter
}()
var body: some View {
VStack {
Text("Select a date to read")
.font(.largeTitle)
DatePicker("Select a date", selection: $date, displayedComponents: .date)
.datePickerStyle(GraphicalDatePickerStyle())
.labelsHidden()
.frame(maxHeight: 400)
Text("\(date, formatter: dateFormatter)")
}
.navigationTitle("Datepicker")
}
}
struct CalendarView_Previews: PreviewProvider {
static var previews: some View {
CalendarView()
}
}
//Replace this with your Decodable object
class Day : ObservableObject{
let id: UUID = UUID()
var date: Date
init( date: Date) {
self.date = date
}
}
class CalendarViewModel: ObservableObject, Identifiable {
#Published var days: [Day] = []
#Published var selectedDate: Date = Date()
var filteredDay: Day?{
days.filter({
Calendar.current.isDate($0.date, equalTo: selectedDate, toGranularity: .day)
}).first
}
init() {
//Create sample Days you can remove this and populate your array with acual data
for n in 1...30{
days.append(Day(date: Date().addingTimeInterval(TimeInterval(60*60*24*n))))
days.append(Day(date: Date().addingTimeInterval(TimeInterval(-60*60*24*n))))
}
}
}
struct CalendarView: View {
#StateObject var vm: CalendarViewModel = CalendarViewModel()
#State var isVisible: Bool = false
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .long
formatter.dateFormat = "LLLL d"
return formatter
}()
var body: some View {
NavigationView{
ScrollView {
Text("Select a date to read")
.font(.largeTitle)
DatePicker("Select a date", selection: $vm.selectedDate, displayedComponents: .date)
.datePickerStyle(GraphicalDatePickerStyle())
.labelsHidden()
.frame(maxHeight: 400)
Text("\(vm.selectedDate, formatter: dateFormatter)")
if vm.filteredDay != nil{
NavigationLink(
destination: DayView(day: vm.filteredDay!),
isActive: $isVisible,
label: {
Text("View Day")
})
}else{
Text("No results for selected day")
}
}
.navigationTitle("Datepicker")
}
}
}
struct DayView:View {
//Observe the actual Day here for changes
#ObservedObject var day: Day
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .long
formatter.dateFormat = "LLLL d"
return formatter
}()
var body: some View {
VStack{
Text(dateFormatter.string(from: day.date))
}
}
}

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

Model to search notes by date

How to model notes, so I can search them ,after, by dates? like on the picture.
Firstly, I thought about dictionary with key of type Date; but it didn't work to me.
So I've tried the following String format:
"04-28-2020" - US calendar
"28-04-2020" - Other calendar
One of the issues - if the telephone changes date style(region dependent), keys remain in dictionary, but aren't accessed any more...
My intuition that it should be the dictionary, but what are the sustainable way to make such a model?
Can it be an array of notes [Note], without keys?
struct Note: Codable, Identifiable {
let id = UUID()
var date: Date
var title: String
var description: String
var dateString: String {
let df = DateFormatter()
df.dateStyle = .short
df.timeStyle = .medium
return df.string(from: date)
}
// method returns "key" to be used in dictionary
static func dateKey(for date: Date) -> String {
let df = DateFormatter()
df.dateStyle = .short
return df.string(from: date)
}
}
class NotesOrganizer: ObservableObject {
// #Published var notes = [Note]() - tried initially
#Published var notesDictionary = [String : [Note]]()
}
struct ContentView: View {
#State private var title = ""
#State private var description = ""
#ObservedObject var notesOrganizer = NotesOrganizer()
var body: some View {
NavigationView {
VStack {
Button("Save note") {
let key = Note.dateKey(for: Date())
notesOrganizer.notesDictionary[key]?.append(Note(date: Date(), title: title, description: description))
?? (notesOrganizer.notesDictionary[key] = [Note(date: Date(), title: title, description: description)])
print("Date Key: \(Note.dateKey(for: Date()))")
}
NavigationLink(destination: DetailView(notesOrganizer: notesOrganizer))
{
Text("Log history ->")
.foregroundColor(.purple)
.underline()
}
}
.navigationBarHidden(true)
}
}
init() {
UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
UINavigationBar.appearance().shadowImage = UIImage()
}
}
DetailView()
extension Date {
var tomorrow: Date? {
return Calendar.current.date(byAdding: .day, value: 1, to: self)
}
var yesterday: Date? {
return Calendar.current.date(byAdding: .day, value: -1, to: self)
}
}
struct DetailView: View {
#ObservedObject var notesOrganizer: NotesOrganizer
#State private var dayLabel: String = Note.dateKey(for: Date())
#State private var chosenDate = Date()
var body: some View {
VStack {
HStack {
Button(action: {
chosenDate = chosenDate.yesterday!
dayLabel = Note.dateKey(for: chosenDate)
}, label: {
Image(systemName: "chevron.left")
})
Text(dayLabel)
Spacer()
Button(action: {
chosenDate = chosenDate.tomorrow!
dayLabel = Note.dateKey(for: chosenDate)
}, label: {
Image(systemName: "chevron.right")
})
}
.padding([.horizontal,.bottom])
List{
ForEach(notesOrganizer.notesDictionary[day] ?? []) { note in
HStack {
VStack(alignment:.leading) {
Text(note.title)
Text(note.description)
}.multilineTextAlignment(.leading)
Spacer()
Text(note.dateString)
}
}.onDelete(perform: removeItems)
}
.navigationBarTitle("", displayMode: .inline)
Spacer()
}

Check if today is a new day SwiftUI

How can I check if today is a new day?
How can I say when the app lunches or in background, whichever, if it's 8am of a new day then do some action...
Date() would only give me the current date.
I need this to do some resetting and possibly to send notification at a specific time
you could try something like this:
import SwiftUI
struct ContentView: View {
#State var lastDay: Date = Date()
#State var isToday = false
#State var selectedTime = 8 // 24 hour clock
var body: some View {
Text("your main view")
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
// store the date whenever you go into background
print("---> storing: \(Date())")
UserDefaults.standard.set(Date(), forKey: "lastDay")
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
// try to retrieve the date when you come back from background
print("\n----> try retrieve lastDay")
if let temDate = UserDefaults.standard.object(forKey: "lastDay") {
self.lastDay = temDate as! Date
print("----> retrieved lastDay: \(self.lastDay)")
if Calendar.current.isDate(Date(), inSameDayAs: self.lastDay) {
self.isToday = true
print("----> isToday: \(self.isToday)\n")
// if it is 8 am or later do something
if let thisHour = Calendar.current.dateComponents([.hour], from: Date()).hour {
if (thisHour >= self.selectedTime) {
print("----> it is 8am or later --> do something")
// self.doSomething()
}
}
}
}
}
}
}
NotificationCenter is Apple internal message system. SwiftUI can listen for specific events, like when the App goes into the background. This is what this line does:
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification))
Now, this line:
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification))
listens for when you come back from the background, as mentioned in the comments.
There is no slowing down because of these onReceives.
What I have shown is an approach, you can add other .onReceive, such as:
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification)) { _ in
These can be used for when you start the App, and when you quit the App.
So this all should work to be able to tell if the date has changed.
struct ContentView: View {
#State private var lastDateString = UserDefaults.standard.string(forKey: "lastDateString") ?? String()
//stores lastDateString in memory for if the app closes
#State var lastDate = Date()
#State var currentDate = Date()
#State var currentDateString = String()
#State var differentDate = false;
let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
//Creates a timer that goes every 1 second
func newDay() {
let formatter = DateFormatter()
formatter.dateFormat = "d MM y"
//sets the format so it will be day month year
if lastDateString == String() {
lastDateString = formatter.string(from: lastDate)
UserDefaults.standard.set(self.lastDateString, forKey: "lastDateString")
} //sets initial value for lastDateString for first time app ever launches
self.currentDate = Date()
currentDateString = formatter.string(from: currentDate)
//sets currentDateString for every time app launches
if lastDateString != currentDateString {
self.differentDate = true
self.lastDate = Date()
self.currentDate = Date()
lastDateString = formatter.string(from: lastDate)
UserDefaults.standard.set(self.lastDateString, forKey: "lastDateString")
currentDateString = formatter.string(from: currentDate)
}
//checks if the date has changed and sets differentDate to true if it has
}
var body: some View {
VStack{
Text("Hello, World!")
.onAppear(){ //used to run function when app launches
self.newDay()
}
}
.onReceive(self.timer) { _ in
self.newDay()
} //calls function newDay() every second from timer
}
}
Hope this helps and sorry that this may not be the most concise, but hopefully it helps.
if Calendar.current.isDate(lastVisit as! Date, inSameDayAs: Date()) {
print("Same day")
} else {
print("New day")
}