I want to make an exercise app to help users track their daily exercise and I have a variable exerciseTime that I want to reset every day. Is there any way to do that?
The variable is stored in app storage #AppStorage("exerciseTime") var exerciseTime = 0.0 and connected to a timer.
Text("Time: \(format(seconds: timerStruct.countdownTimer))")
.padding()
.onReceive(timer) { _ in
if timerStruct.countdownTimer > 0 && timerStruct.timerRunning == true {
timerStruct.countdownTimer -= 1
exerciseTime += 1.0
} else {
timerStruct.timerRunning = false
if timerStruct.countdownTimer <= 0, timerStruct.timerRunning == false {
timerStruct.isAlertpresented = true
reset()
}
}
This function checks if the current date is in the same day as a saved date in UserDefaults
Just call it always when the app becomes active
func isNewDay() -> Bool {
let defaults = UserDefaults.standard
let now = Date.now
if let savedDate = defaults.object(forKey: "currentDate") as? Date,
Calendar.current.compare(savedDate, to: now, toGranularity: .day) == .orderedSame {
return false
}
defaults.set(now, forKey: "currentDate")
return true
}
Related
I want to track when was the last time the user refreshed the api, so I decided to do it like this:
#AppStorage("lastTimeUserRefreshedApi") var lastTimeUserRefreshedApi: Date = Date()
func canUserRefreshAPI() -> Bool {
let readyToBeRefreshed: Date = lastTimeUserRefreshedApi.addingTimeInterval(5) // in seconds
let currentTime: Date = Date()
var canUserRefresh: Bool = false
if(currentTime > readyToBeRefreshed) {
canUserRefresh = true
lastTimeUserRefreshedApi = lastTimeUserRefreshedApi.addingTimeInterval(5)
} else {
canUserRefresh = false
}
return canUserRefresh
}
The problem is that it's always returning true, but why? Also is there a simpler way to do this?
Thanks
EDIT:
This is the extension I'm using to be able to store Date in the #AppStorage:
extension Date: RawRepresentable {
public var rawValue: String {
self.timeIntervalSinceReferenceDate.description
}
public init?(rawValue: String) {
self = Date(timeIntervalSinceReferenceDate: Double(rawValue) ?? 0.0)
}
}
You are making it much harder than it should. Just save the "expiration" date. When you read it just compare if it is past or not.
#AppStorage("expiration")
var expiration: Date = Date(timeIntervalSinceNow: 5)
func canUserRefreshAPI() -> Bool {
let now = Date()
if expiration < now {
expiration = Date(timeIntervalSinceNow: 5)
return true
} else {
return false
}
}
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])
For a button, each time it is tapped, how to trigger a code1 (set a specific var to value1) when ‘onTap’ occured less than 60 seconds ago or code2 (set a specific variable to value2) when onTap occured more than 60 seconds ago ?
==> Working solution implemented for duration = 10 seconds
Button(action: {
self.date = Date()
let hour = self.calendar.component(.hour, from: self.date)
let minutes = self.calendar.component(.minute, from: self.date)
let seconds = self.calendar.component(.second, from: self.date)
self.newdate = hour*3600+minutes*60+seconds
if (self.lastdate == 0){
print(" First Tap...")
}else
if (self.newdate-self.lastdate) < 10 {
print("Tap before 10 sec => call Func1")
} else {
print("Tap > 10 seconds => call Func2")
}
self.lastdate = hour*3600+minutes*60+seconds
}) {
Text("One")
}
Here's a complete example that works for an interval of 5 seconds (to make testing quicker).
It uses #State vars to update the Date of the lastTap and message and uses .timeIntervalSince(last) to measure the time in seconds. The first tap is detected when lastTap is still nil.
Changing the message causes the view to redraw and update the message on the screen.
struct ContentView: View {
#State private var lastTap: Date? = nil
#State private var message = ""
var body: some View {
VStack(spacing: 40) {
Text(message)
Button("Tap me") {
let now = Date()
if let last = self.lastTap {
if now.timeIntervalSince(last) > 5 {
self.message = "Greater than 5 seconds"
} else {
self.message = "Less than 5 seconds"
}
} else {
self.message = "First tap"
}
self.lastTap = now
}
}
}
}
var lastTapTimestamp: Date?
#IBAction func buttonTapped(_ sender: UIButton) {
defer { lastTapTimestamp = Date() }
guard let date = lastTapTimestamp else {
callFunc1()
return
}
if Int(Date().timeIntervalSinceReferenceDate) - Int(date.timeIntervalSinceReferenceDate) >= 3600 {
callFunc2()
} else {
callFunc1()
}
}
I have a game that has 9 different levels which have their own highscore values for coins collected and special coins collected. I want to make a "Bank" that can store those values and add them up to be able to 'purchase' the unlock for some levels. I'm not sure if I implemented my scoring system in a way that won't allow this or if I'm missing something simple. Any insight is greatly appreciated!
import Foundation
struct ScoreManager {
static func getCurrentScore(for levelKey: String) -> [String:Int] {
if let existingData = UserDefaults.standard.dictionary(forKey: levelKey) as? [String:Int] {
return existingData
} else {
return [GameConstants.StringConstants.scoreScoreKey:0, GameConstants.StringConstants.scoreStarsKey:0, GameConstants.StringConstants.scoreCoinsKey:0]
}
}
static func updateScore(for levelKey: String, and score: [String:Int]) {
UserDefaults.standard.set(score, forKey: levelKey)
UserDefaults.standard.synchronize()
}
static func compare(scores: [[String:Int]], in levelKey: String) {
var newHighscore = false
let currentScore = getCurrentScore(for: levelKey)
var maxScore = currentScore[GameConstants.StringConstants.scoreScoreKey]!
var maxStars = currentScore[GameConstants.StringConstants.scoreStarsKey]!
var maxCoins = currentScore[GameConstants.StringConstants.scoreCoinsKey]!
for score in scores {
if score[GameConstants.StringConstants.scoreScoreKey]! > maxScore {
maxScore = score[GameConstants.StringConstants.scoreScoreKey]!
newHighscore = true
}
if score[GameConstants.StringConstants.scoreStarsKey]! > maxStars {
maxStars = score[GameConstants.StringConstants.scoreStarsKey]!
newHighscore = true
}
if score[GameConstants.StringConstants.scoreCoinsKey]! > maxCoins {
maxCoins = score[GameConstants.StringConstants.scoreCoinsKey]!
newHighscore = true
}
}
if newHighscore {
let newScore = [GameConstants.StringConstants.scoreScoreKey: maxScore, GameConstants.StringConstants.scoreStarsKey: maxStars, GameConstants.StringConstants.scoreCoinsKey: maxCoins]
updateScore(for: levelKey, and: newScore)
}
}
And this is called in the GameScene after you finish the level..
func finishGame() {
gameState = .finished
var stars = 0
let percentage = CGFloat(coins)/100.0
if percentage >= 0.8 {
stars = 3
} else if percentage >= 0.4 {
stars = 2
} else if coins >= 1 {
stars = 1
}
let scores = [
GameConstants.StringConstants.scoreScoreKey: coins,
GameConstants.StringConstants.scoreStarsKey: stars,
GameConstants.StringConstants.scoreCoinsKey: superCoins
]
ScoreManager.compare(scores: [scores], in: levelKey)
createAndShowPopup(type: 1, title: GameConstants.StringConstants.completedKey)
if level < 9 {
let nextLevelKey = "Level_\(world)-\(level+1)_Unlocked"
UserDefaults.standard.set(true, forKey: nextLevelKey)
UserDefaults.standard.synchronize()
}
}
Ignore the stars, I used these to show basically how well you did on the level. I'll gladly provide more code snippets if needed too. Thank you all again!
I have an array of object contains bookings
class MonthBookings: NSObject {
var date: Date = Date()
var bookings_count: Int = 0
}
var bookings = [MonthBookings]()
So I need to check if cell date is equal to some date in bookings array then change cell color for this date in cell.
I have tried this way down below but didn't work:
func calendar(_ calendar: JTAppleCalendarView, willDisplayCell cell: JTAppleDayCellView, date: Date, cellState: CellState) {
guard let sfbCell = cell as? SFBCalendarCell else {return}
let booking = self.bookingsMonths[cellState.row()]
if booking.date == date {
sfbCell.dayText.textColor = .red
} else {
sfbCell.dayText.textColor = .black
}
}
Are you doing a day compared or the actual date/Time?
Try something like this.
let order = CalendarManager.currentCalendar.compare(cellState.date, to:
Date(), toGranularity: .day)
if order == .orderedSame {
calendarCell.dayNumberLabel.textColor = UIColor.white
}
I Solved by using Contains method
if bookingsMonths.contains(where: { $0.date == cellState.date && $0.bookings_count != 0 }) {
sfbCell.dayText.font = UIFont.boldSystemFont(ofSize: 15)
} else {
sfbCell.dayText.font = UIFont.systemFont(ofSize: 15)
}