Check if today is a new day SwiftUI - swift

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

Related

SwiftUI find time elapsed between first time and now

I'm trying to do a timer in my app, but to have the ability to continue even if the user kill the app in the multitask I use #AppStorrage to store my initial time.
As it is not possible to save a DATE() in the #AppStorrage I save my initial hours:min into a String.
Then in my Timer function I'd like to compare the initial saved time with the current time using Calendar.current.dateComponents([.hour, .minute, .second], from: initialTime, to: Date())
I have this error on this line : Cannot assign value of type 'DateComponents' to type 'Double'
The code where I define the #AppStorage var :
#AppStorage("flightMode_DepTime") private var flightModeDepTime: String = ""
#ObservedObject var timerManager = TimerFunc()
var body: some View {
// ...
Button {
isStandbyMode.toggle()
if !isStandbyMode{
UserDefaults.standard.set(Date().formatted(.dateTime.hour().minute()), forKey: "flightMode_DepTime")
timerManager.start()
}else{
timerManager.pause()
}
} label: {
if isStandbyMode{
Image(systemName: "timer")
Text("STBY MODE ")
}else{
Image(systemName: "airplane")
Text("FLIGHT MODE ")
}
}
// ...
}
And here is my timer function :
#AppStorage("flightMode_DepTime") private var flightModeDepTime: String = ""
private var initialTime: Date = Date()
private var now: Date = Date()
func start() {
mode = .running
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {timer in
let formatter = DateFormatter()
formatter.timeStyle = .medium
initialTime = formatter.date(from: self.flightModeDepTime) ?? Date()
self.secondsElapsed = Calendar.current.dateComponents([.hour, .minute, .second], from: initialTime, to: now)
}
}
Thanks for your help !

How to make a timer that shows how much time has passed in SwiftUI?

I am trying to add a timer to the top of my view that shows how long that view has been open.
So far this is what I have:
#State var isTimerRunning = false
#State private var startTime = Date()
#State private var timerString = "0:0"
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
Text(self.timerString)
.font(Font.system(.largeTitle, design: .monospaced))
.onReceive(timer) { _ in
if self.isTimerRunning {
timerString = String(format: "%.2f", (Date().timeIntervalSince( self.startTime)))
}
}
.onAppear() {
if !isTimerRunning {
timerString = "0:0"
startTime = Date()
}
isTimerRunning.toggle()
}
However it shows milliseconds and seconds in the form "1.32434234234234" when I want it to display seconds and minutes in the form "12:43".
You can use an extension of TimeInterval for this. You can customize the string pretty well by changing the formatter.unitsStyle (.positional will show 00:00:00, while .abbreviated will show 0h 0m 0s) and the formatter.zeroFormattingBehavior variables.
Credits for the extension found here.
extension TimeInterval {
func format(using units: NSCalendar.Unit) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = units
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
return formatter.string(from: self) ?? ""
}
}
struct TimerPlayground: View {
#State var isTimerRunning = false
#State private var startTime = Date()
#State var interval = TimeInterval()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text(interval.format(using: [.hour, .minute, .second]))
.font(Font.system(.largeTitle, design: .monospaced))
.onReceive(timer) { _ in
if self.isTimerRunning {
interval = Date().timeIntervalSince(startTime)
}
}
.onAppear() {
if !isTimerRunning {
startTime = Date()
}
isTimerRunning.toggle()
}
}
}
struct TimerPlayground_Previews: PreviewProvider {
static var previews: some View {
TimerPlayground()
}
}
Inline with #Duncan C's comment, here's an updated version which creates the formatter only once (locally) for better performance.
struct TimerPlayground: View {
#State var isTimerRunning = false
#State private var startTime = Date()
#State var interval = TimeInterval()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute, .second]
formatter.unitsStyle = .abbreviated
formatter.zeroFormattingBehavior = .pad
return formatter
}()
var body: some View {
Text(formatter.string(from: interval) ?? "")
.font(Font.system(.largeTitle, design: .monospaced))
.onReceive(timer) { _ in
if self.isTimerRunning {
interval = Date().timeIntervalSince(startTime)
}
}
.onAppear() {
if !isTimerRunning {
startTime = Date()
}
isTimerRunning.toggle()
}
}
}
Use dateComponents to get the difference between start time and the current time.
extension Date {
func passedTime(from date: Date) -> String {
let difference = Calendar.current.dateComponents([.minute, .second], from: date, to: self)
let strMin = String(format: "%02d", difference.minute ?? 00)
let strSec = String(format: "%02d", difference.second ?? 00)
return "\(strMin):\(strSec)"
}
}
And in view
struct ContentView: View {
#State private var isTimerRunning = false
#State private var startTime = Date()
#State private var timerString = "00:00"
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text(self.timerString)
.font(Font.system(.largeTitle, design: .monospaced))
.onReceive(timer) { _ in
if self.isTimerRunning {
timerString = Date().passedTime(from: startTime)
}
}
.onAppear() {
if !isTimerRunning {
timerString = "0:0"
startTime = Date()
}
isTimerRunning.toggle()
}
}
}

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

Swift HealthKit HKStatisticsCollectionQuery statisticsUpdateHandler not always called

I have a test project where I get the total number of falls for a user for each day over the course of the week. The initialResultsHandler works perfectly every time, however the statisticsUpdateHandler doesn't always fire off. If you start the app, then go to the health app and insert falls manually, switch back to the test app you should see the total for today update. In reality this works for about the first 3-6 times. After that the statisticsUpdateHandler doesn't get called anymore.
What's also odd is that if you delete data and then go back to the test app, or add data from a time earlier than now, the statisticsUpdateHandler gets called. This leads me to think that it has something to do with the statisticsUpdateHandler end date.
Apples documentation is pretty clear however I’m afraid they might be leaving something out.
If this property is set to nil, the statistics collection query will automatically stop as soon as it has finished calculating the initial results. If this property is not nil, the query behaves similarly to the observer query. It continues to run, monitoring the HealthKit store. If any new, matching samples are saved to the store—or if any of the existing matching samples are deleted from the store—the query executes the update handler on a background queue.
Is there any reason that statisticsUpdateHandler might not be called? I have included a test project below.
struct Falls: Identifiable{
let id = UUID()
let date: Date
let value: Int
var formattedDate: String{
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("MM/dd/yyyy")
return formatter.string(from: date)
}
}
struct ContentView: View {
#StateObject var manager = HealthKitManager()
var body: some View {
NavigationView{
List{
Text("Updates: \(manager.updates)")
ForEach(manager.falls){ falls in
HStack{
Text(falls.value.description)
Text(falls.formattedDate)
}
}
}
.overlay(
ProgressView()
.scaleEffect(1.5)
.opacity(manager.isLoading ? 1 : 0)
)
.navigationTitle("Falls")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class HealthKitManager: ObservableObject{
let healthStore = HKHealthStore()
let fallType = HKQuantityType.quantityType(forIdentifier: .numberOfTimesFallen)!
#Published var isLoading = false
#Published var falls = [Falls]()
#Published var updates = 0
init() {
let healthKitTypesToRead: Set<HKSampleType> = [fallType]
healthStore.requestAuthorization(toShare: nil, read: healthKitTypesToRead) { (success, error) in
if let error = error{
print("Error: \(error)")
} else if success{
self.startQuery()
}
}
}
func startQuery(){
let now = Date()
let cal = Calendar.current
let sevenDaysAgo = cal.date(byAdding: .day, value: -7, to: now)!
let startDate = cal.startOfDay(for: sevenDaysAgo)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: now, options: [.strictStartDate, .strictEndDate])
var interval = DateComponents()
interval.day = 1
// start from midnight
let anchorDate = cal.startOfDay(for: now)
let query = HKStatisticsCollectionQuery(
quantityType: fallType,
quantitySamplePredicate: predicate,
options: .cumulativeSum,
anchorDate: anchorDate,
intervalComponents: interval
)
query.initialResultsHandler = { query, collection, error in
guard let collection = collection else {
print("No collection")
DispatchQueue.main.async{
self.isLoading = false
}
return
}
collection.enumerateStatistics(from: startDate, to: Date()){ (result, stop) in
guard let sumQuantity = result.sumQuantity() else {
return
}
let totalFallsForADay = Int(sumQuantity.doubleValue(for: .count()))
let falls = Falls(date: result.startDate, value: totalFallsForADay)
print(falls.value, falls.formattedDate)
DispatchQueue.main.async{
self.falls.insert(falls, at: 0)
}
}
print("initialResultsHandler done")
DispatchQueue.main.async{
self.isLoading = false
}
}
query.statisticsUpdateHandler = { query, statistics, collection, error in
print("In statisticsUpdateHandler...")
guard let collection = collection else {
print("No collection")
DispatchQueue.main.async{
self.isLoading = false
}
return
}
DispatchQueue.main.async{
self.isLoading = true
self.updates += 1
self.falls.removeAll(keepingCapacity: true)
}
collection.enumerateStatistics(from: startDate, to: Date()){ (result, stop) in
guard let sumQuantity = result.sumQuantity() else {
return
}
let totalFallsForADay = Int(sumQuantity.doubleValue(for: .count()))
let falls = Falls(date: result.startDate, value: totalFallsForADay)
print(falls.value, falls.formattedDate)
print("\n\n")
DispatchQueue.main.async{
self.falls.insert(falls, at: 0)
}
}
print("statisticsUpdateHandler done")
DispatchQueue.main.async{
self.isLoading = false
}
}
isLoading = true
healthStore.execute(query)
}
}
I was so focused on the statisticsUpdateHandler and the start and end time that I didn't pay attention to the query itself. It turns out that the predicate was the issue. By giving it an end date, it was never looking for samples outside the the initial predicate end date.
Changing the predicate to this solved the issue:
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: nil, options: [.strictStartDate])

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