ClockKit CLKComplicationDataSource missing backward events - ios

I write a test app with complications support
For some reason clock faces presenting only 1-2 backward events, but I can see in logs 10-15 events before current date.
And when I return an empty array for forward events all my backward events start showing in clock face.
Here is my function
func getTimelineEntriesForComplication(complication: CLKComplication, beforeDate date: NSDate, limit: Int, withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
var entries: [CLKComplicationTimelineEntry] = []
let events = self.events.filter { (event: CEEvent) -> Bool in
return date.compare(event.startDate) == .OrderedDescending
}
var lastDate = date.midnightDate
for event in events {
let entry = CLKComplicationTimelineEntry(date: lastDate, complicationTemplate: event.getComplicationTemplate(complication.family))
if let endDate = event.endDate {
lastDate = endDate
} else {
lastDate = event.startDate
}
entries.append(entry)
if entries.count >= limit {
break
}
}
handler(entries)
}
P.S. I know about 'limit' parameter and it's always greater than my array's count
P.P.S. Sorry about my English :)

I've seen that identical behavior for watchOS 2.0.1 where time travel backwards initially only shows two earlier entries, even though the datasource was asked for and returned 100 entries.
About 15 minutes after launch, more entries started appearing for backwards time travel. About 30 minutes after launch, all 100 prior entries were present.
This was not due to any update I scheduled, as my complication's update interval is 24 hours.
It appears that the complication server prioritizes adding the forward entries, but defers populating the cache with all the backward time travel entries. You'd have to ask Apple whether it's an optimization or a bug.
I don't know if this is a coincidence, but my timeline entries are spaced 15 minutes apart. Perhaps when the complication server updates the complication to show the new timeline entry, it also adds more of the batched earlier entries?

Related

How can I get GNU Lib C TZ format output from NSTimeZone?

I need to set the timezone information of a remote clock to the one on the iOS device.
The remote clock only supports GNU lib C TZ format of:
std offset dst [offset],start[/time],end[/time]
e.g: EST+5EDT,M3.2.0/2,M11.1.0/2
So I need to produce a string similar to above from NSTimeZone.local time zone in Swift. Can't seem to access the current timezone rules as they would be in the IANA TZ database to produce the output.
Can this be done without the horrifying idea of caching a local copy of the TZ database in the app?
Update:
I haven't been able to find anything useful even through other programming languages. The best I was able to find was essentially parsing the tzfile in linux and making my own NSDictionary containing the info.
This was a fun exploration, largely because fitting the data into just the right format is pretty complex. Problem components:
We need the "current" TZ database rule that applies for a given time zone. This is a bit of a loaded concept, because:
Darwin platforms don't actually use the TZ database directly for most applications, but instead use ICU's time zone database, which comes in a different format and is more complex. Even if you produce a string in this format, it's not necessarily descriptive of the actual time behavior on device
While it is possible to read and parse the TZ database on iOS dynamically, the TZ database itself is not guaranteed to store information in the format needed here. rfc8536, the RFC governing the Time Zone Information Format says the following about the format you want:
The TZ string in a version 3 TZif file MAY use the following extensions to POSIX TZ strings. These extensions are described using the terminology of Section 8.3 of the "Base Definitions" volume of [POSIX].
Example: <-03>3<-02>,M3.5.0/-2,M10.5.0/-1
Example: EST5EDT,0/0,J365/25
While spelunking through the iOS TZ database, I found some database entries that do offer a rule at the end of the file in this format, but they appear to be a minority. You could parse these dynamically, but it's likely not worth it
So, we need to use APIs to produce a string in this format.
In order to produce a "rule" that is at least approximately correct on a given date, you need to know information about DST transitions around that date. This is an extremely thorny topic, because DST rules change all the time, and don't always make as much sense as you'd hope. At the very least:
Many time zones in the Northern hemisphere observe DST beginning in the spring and ending in the fall
Many time zones in the Southern hemisphere observe DST beginning in the fall and ending in the spring
Some time zones don't observe DST (are in standard time year-round)
Some time zones don't observe DST and are in daylight time year-round
Because the rules are so complex, the rest of this answer assumes you're okay with producing a "good enough" answer that represents a specific date in time, and is willing to send further strings to your clock some time in the future when corrections are needed. e.g., to describe "now", we will be assuming that producing a rule based off of the last DST transition (if any) and the next DST transition (if any) is "good enough", but this may not work for all situations in many time zones
Foundation provides DST transition information on TimeZone in the form of TimeZone.nextDaylightSavingTimeTransition/TimeZone.nextDaylightSavingTimeTransition(after:). Frustratingly, however, there's no way to get information about previous DST transitions, so we'll need to rectify that:
Foundation's localization support (including calendars and time zones) is based directly on the ICU library, which ships internally on all Apple platforms. ICU does provide a way to get information about previous DST transitions, but Foundation just doesn't offer this as API, so we'll need to expose it ourselves
ICU is a semi-private library on Apple platforms. The library is guaranteed to be present, and Xcode will offer you libicucore.tbd to link against in <Project> > <Target> > Build Phases > Link Binary with Libraries, but the actual headers and symbols are not directly exposed to apps. You can successfully link against libicucore, but you'll need to forward-declare the functionality we need in an Obj-C header imported into Swift
Somewhere in the Swift project, we need to expose the following ICU functionality:
#include <stdint.h>
typedef void * _Nonnull UCalendar;
typedef double UDate;
typedef int8_t UBool;
typedef uint16_t UChar;
typedef enum UTimeZoneTransitionType {
UCAL_TZ_TRANSITION_NEXT,
UCAL_TZ_TRANSITION_NEXT_INCLUSIVE,
UCAL_TZ_TRANSITION_PREVIOUS,
UCAL_TZ_TRANSITION_PREVIOUS_INCLUSIVE,
} UTimeZoneTransitionType;
typedef enum UCalendarType {
UCAL_TRADITIONAL,
UCAL_DEFAULT,
UCAL_GREGORIAN,
} UCalendarType;
typedef enum UErrorCode {
U_ZERO_ERROR = 0,
} UErrorCode;
UCalendar * _Nullable ucal_open(const UChar *zoneID, int32_t len, const char *locale, UCalendarType type, UErrorCode *status);
void ucal_setMillis(const UCalendar * _Nonnull cal, UDate date, UErrorCode * _Nonnull status);
UBool ucal_getTimeZoneTransitionDate(const UCalendar * _Nonnull cal, UTimeZoneTransitionType type, UDate * _Nonnull transition, UErrorCode * _Nonnull status);
These are all forward declarations / constants, so no need to worry about implementation (since we get that by linking against libicucore).
You can see the values in UTimeZoneTransitionType — TimeZone.nextDaylightSavingTimeTransition just calls ucal_getTimeZoneTransitionDate with a value of UCAL_TZ_TRANSITION_NEXT, so we can offer roughly the same functionality by calling the method with UCAL_TZ_TRANSITION_PREVIOUS:
extension TimeZone {
func previousDaylightSavingTimeTransition(before: Date) -> Date? {
// We _must_ pass a status variable for `ucal_open` to write into, but the actual initial
// value doesn't matter.
var status = U_ZERO_ERROR
// `ucal_open` requires the time zone identifier be passed in as UTF-16 code points.
// `String.utf16` doesn't offer a contiguous buffer for us to pass directly into `ucal_open`
// so we have to create our own by copying the values into an `Array`, then
let timeZoneIdentifier = Array(identifier.utf16)
guard let calendar = Locale.current.identifier.withCString({ localeIdentifier in
ucal_open(timeZoneIdentifier, // implicit conversion of Array to a pointer, but convenient!
Int32(timeZoneIdentifier.count),
localeIdentifier,
UCAL_GREGORIAN,
&status)
}) else {
// Figure out some error handling here -- we failed to find a "calendar" for this time
// zone; i.e., there's no time zone date for this time zone.
//
// With more enum cases copied from `UErrorCode` you may find a good way to report an
// error here if needed. `u_errorName` turns a `UErrorCode` into a string.
return nil
}
// `UCalendar` functions operate on the calendar's current timestamp, so we have to apply
// `date` to it. `UDate`s are the number of milliseconds which have passed since January 1,
// 1970, while `Date` offers its time interval in seconds.
ucal_setMillis(calendar, before.timeIntervalSince1970 * 1000.0, &status)
var result: UDate = 0
guard ucal_getTimeZoneTransitionDate(calendar, UCAL_TZ_TRANSITION_PREVIOUS, &result, &status) != 0 else {
// Figure out some error handling here -- same as above (check status).
return nil
}
// Same transition but in reverse.
return Date(timeIntervalSince1970: result / 1000.0)
}
}
So, with all of this in place, we can fill out a crude method to produce a string in the format you need:
extension TimeZone {
struct Transition {
let abbreviation: String
let offsetFromGMT: Int
let date: Date
let components: DateComponents
init(for timeZone: TimeZone, on date: Date, using referenceCalendar: Calendar) {
abbreviation = timeZone.abbreviation(for: date) ?? ""
offsetFromGMT = timeZone.secondsFromGMT(for: date)
self.date = date
components = referenceCalendar.dateComponents([.month, .weekOfMonth, .weekdayOrdinal, .hour, .minute, .second], from: date)
}
}
func approximateTZEntryRule(on date: Date = Date(), using calendar: Calendar? = nil) -> String? {
var referenceCalendar = calendar ?? Calendar(identifier: .gregorian)
referenceCalendar.timeZone = self
guard let year = referenceCalendar.dateInterval(of: .year, for: date) else {
return nil
}
// If no prior DST transition has ever occurred, we're likely in a time zone which is either
// standard or daylight year-round. We'll cap the definition here to the very start of the
// year.
let previousDSTTransition = Transition(for: self, on: previousDaylightSavingTimeTransition(before: date) ?? year.start, using: referenceCalendar)
// Same with the following DST transition -- if no following DST transition will ever come,
// we'll cap it to the end of the year.
let nextDSTTransition = Transition(for: self, on: nextDaylightSavingTimeTransition(after: date) ?? year.end, using: referenceCalendar)
let standardToDaylightTransition: Transition
let daylightToStandardTransition: Transition
if isDaylightSavingTime(for: date) {
standardToDaylightTransition = previousDSTTransition
daylightToStandardTransition = nextDSTTransition
} else {
standardToDaylightTransition = nextDSTTransition
daylightToStandardTransition = previousDSTTransition
}
let standardAbbreviation = daylightToStandardTransition.abbreviation
let standardOffset = formatOffset(daylightToStandardTransition.offsetFromGMT)
let daylightAbbreviation = standardToDaylightTransition.abbreviation
let startDate = formatDate(components: standardToDaylightTransition.components)
let endDate = formatDate(components: daylightToStandardTransition.components)
return "\(standardAbbreviation)\(standardOffset)\(daylightAbbreviation),\(startDate),\(endDate)"
}
/* These formatting functions can be way better. You'll also want to actually cache the
DateComponentsFormatter somewhere.
*/
func formatOffset(_ dateComponents: DateComponents) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute, .second]
formatter.zeroFormattingBehavior = .dropTrailing
return formatter.string(from: dateComponents) ?? ""
}
func formatOffset(_ seconds: Int) -> String {
return formatOffset(DateComponents(second: seconds))
}
func formatDate(components: DateComponents) -> String {
let month = components.month ?? 0
let week = components.weekOfMonth ?? 0
let day = components.weekdayOrdinal ?? 0
let offset = formatOffset(DateComponents(hour: components.hour, minute: components.minute, second: components.second))
return "M\(month).\(week).\(day)/\(offset)"
}
}
Note that there's lots to improve here, especially in clarity and performance. (Formatters are notoriously expensive, so you'll definitely want to cache them.) This also currently only produces dates in the expanded form "Mm.w.d" and not Julian days, but that can be bolted on. The code also assumes that it's "good enough" to restrict unbounded rules to the current calendar year, since this is what the GNU C library docs seem to imply about e.g. time zones which are always in standard/daylight time. (This also doesn't recognize well-known time zones like GMT/UTC, which might be sufficient to just write out as "GMT".)
I have not extensively tested this code for various time zones, and the above code should be considered a basis for additional iteration. For my time zone of America/New_York, this produces "EST-5EDT,M3.3.2/3,M11.2.1/1", which appears correct to me at first glance, but many other edge cases might be good to explore:
Boundary conditions around the start/end of the year
Giving a date which exactly matches a DST transition (consider TRANSITION_PREVIOUS vs. TRANSITION_PREVIOUS_INCLUSIVE)
Time zones which are always standard/daylight
Non-standard daylight/timezone offsets
There's a lot more to this, and in general, I'd recommend trying to find an alternative method of setting a time on this device (preferably using named time zones), but this might hopefully at least get you started.

iOS: execute code AFTER a particular date

I have some code I want to run after a particular date/time has passed. For example, if I want the code to run 7 days from now and the user opens the app at any time on day 7 or after the code will run but if they open the app before the beginning of day 7 nothing happens. Timers in the main runloop work but only if the app is still running in the background. I need a method that will work even if the user kills the app.
Your best option is to store it as local data Even though you only want the code to run once, the overhead is so low, the "check" will not impact the speed or feel of the application. Also this will allow you to run additional checks .. If someone deletes the app, for instance, and leaves the local storage behind. If they re-install you could theoretically "remember" that the application has been installed, and said code has already run (until the user clears application data)
Something like:
//Globally set key
struct defaultsKeys {
static let keyDate = "dateKey"
}
// Set the date in local storage
let defaults = UserDefaults.standard
defaults.set("Your Date String", forKey: defaultsKeys.dateKey)
// Get the date from local storage
let defaults = UserDefaults.standard
if let stringDate = defaults.string(forKey: defaultsKeys.dateKey) {
print(stringDate)
// Do your date comparison here
}
Very few lines of code, and even though the check happens every time the application starts .. The overhead is negligible.
You can either set the date you want your app to "remember" on your local storage or web service. Then, when the user opens your app, compare that date to current device time to determine if you should execute your code.
First, save the current time when you want. You can set the key name however you want.
UserDefaults.standard.setValue(Date(), forKey: "rememberTime")
And every time I open the app, You compare the current time with the saved time.
To do so, I created a function that compares time.
extension Date {
func timeAgoSince() -> Bool {
let calendar = Calendar.current
let unitFlags: NSCalendar.Unit = [.day]
let components = (calendar as NSCalendar).components(unitFlags, from: self, to: Date(), options: [])
if let day = components.day, day >= 7 {
// Returns true if more than 7 days have passed.
return true
}
return false
}
}
Recall the previously saved time and use the time comparison function.
let beforeTime: Date = (UserDefaults.standard.object(forKey: "rememberTime") as? Date)!
if beforeTime.timeAgoSince() {
// more than seven days later
...
} else {
...
}
If you have a problem, please leave a comment !
You can use the below sample code:
override func viewDidLoad() {
super.viewDidLoad()
let nextCodeRunDate = Date() + (7 * 24 * 60 * 60) // 7 Days
if let savedDate = UserDefaults.standard.value(forKey: "NEXT_DATE") as? Date {
if Date() > savedDate {
UserDefaults.standard.setValue(nextCodeRunDate, forKey: "NEXT_DATE")
runYourCode()
}
}else {
// First time
UserDefaults.standard.setValue(nextCodeRunDate, forKey: "NEXT_DATE")
runYourCode()
}
}
func runYourCode() {
// Your code
}

How can I find all the historical offset transition dates for a timezone?

I'm trying to write an app that displays information about a given timezone. It displays which periods of time that timezone is observing DST, and which periods of time it is not. Most importantly, it highlights times where the timezone changes irregularly, such as when Britain observed double DST during WWII, or when Samoa skipped a day in 2011.
For that, I would need to get a list of all the historical timezone offset transitions, as stored in the TZ database (I think there is a copy of the database in every macOS/iOS device). To be more specific, a "transition" (similar to java.time.zone.ZoneOffsetTransition) is modelled by the following 3 things:
the Date when it happened
the GMT offset in seconds before it happened
the GMT offset in seconds after it happened
From what I can see from the TimeZone API docs, there is no built-in method that does this (unlike how java.time does). The closest method I could find is nextDaylightSavingTimeTransition(after:), but that only tells me the transition date of one transition, when given a date, and I'm also not sure what date to give.
How can I get the list of transitions?
Example output for Asia/Ho_Chi_Minh:
Transition at 1906-06-30T16:53:20Z from 25600 to 25590
Transition at 1911-04-30T16:53:30Z from 25590 to 25200
Transition at 1942-12-31T16:00:00Z from 25200 to 28800
Transition at 1945-03-14T15:00:00Z from 28800 to 32400
Transition at 1945-09-01T15:00:00Z from 32400 to 25200
Transition at 1947-03-31T17:00:00Z from 25200 to 28800
Transition at 1955-06-30T16:00:00Z from 28800 to 25200
Transition at 1959-12-31T16:00:00Z from 25200 to 28800
Transition at 1975-06-12T16:00:00Z from 28800 to 25200
One solution I worked out is, start with passing distantPast to nextDaylightSavingTimeTransition:
someTimeZone.nextDaylightSavingTimeTransition(after: .distantPast)
This gets you the first transition date. Then do:
// second transition date
someTimeZone.nextDaylightSavingTimeTransition(after:
someTimeZone.nextDaylightSavingTimeTransition(after: .distantPast)
)
// third transition date
someTimeZone.nextDaylightSavingTimeTransition(after:
someTimeZone.nextDaylightSavingTimeTransition(after:
someTimeZone.nextDaylightSavingTimeTransition(after: .distantPast)
)
)
and so on. Of course, we will put this in a loop. The offset before and after the transition can be found by secondsFromGMT(for:). We will pass a two dates that only differ by something like 1 second.
Also note that although this is named nextDaylightSavingTimeTransition, it also gives you non-DST transitions, which is exactly what you want, so that's great!
import Foundation
struct OffsetTransition {
let instant: Date
let offsetBefore: Int
let offsetAfter: Int
}
var date = Date.distantPast
let timeZone = TimeZone(identifier: "Some Time Zone ID")!
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime]
var transitions = [OffsetTransition]()
while date < Date() {
guard let instant = timeZone.nextDaylightSavingTimeTransition(after: date) else {
break
}
let offsetBefore = timeZone.secondsFromGMT(for: instant.addingTimeInterval(-1))
let offsetAfter = timeZone.secondsFromGMT(for: instant)
if offsetBefore == offsetAfter { continue }
transitions.append(.init(instant: instant, offsetBefore: offsetBefore, offsetAfter: offsetAfter))
print("Transition at \(dateFormatter.string(from: instant)) from \(offsetBefore) to \(offsetAfter)")
date = instant
}
Note the if offsetBefore == offsetAfter { continue } check. I added this check because without it, it will sometimes produce transitions with offsetBefore == offsetAfter. This could be an indication that I am doing something wrong, but this check seems to fix the problem...
The output of this almost matches the output of a similar code using Java's getTransitions. However, there is one other slight problem that I found with this solution. For some timezones, such as Europe/London, the transition from the local mean time to the standardised offset is missing, but for other timezones (e.g. Asia/Hong_Kong), it exists. For example, a similar code using getTransitions in Java would produce a "Transition at 1847-12-01T00:01:15Z from -75 to 0" for Europe/London, but the Swift code's output does not include this line. This is not too big of a problem for me though.

Swift Widget: getTimeline with completion handler

I have a iOS Widget that I am trying to update every 5 or 15 minutes.
I am new to widgets and do not understand how to loop the timeline with an async call.
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
let price: Double
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
networkManager.fetchData { price in
var entries: [SimpleEntry] = []
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration, price: price)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
I have a completion handler that passes in the price from the async api call.
networkManager.fetchData { price in
}
It looks like your networkManager.fetchData is providing only one price value at a time.
In such cases, when only one entry is provided at a time then the following should be enough:
func getTimeline(for configuration: ConfigurationIntent,
in context: Context,
completion: #escaping (Timeline<Entry>) -> ()) {
networkManager.fetchData { (price) in
let currentDate = Date()
//create the entry for the given price
let entry = SimpleEntry(date: currentDate,
configuration: configuration,
price: price)
/*
If you can predict price values then you need the loop for multiple entries.
However in your case, it seems this one entry is sufficient
*/
let entries = [entry]
//next reload date; 15mins in this case
let reloadDate = Calendar.current.date(byAdding: .minute,
value: 15,
to: currentDate)!
//your timeline with one entry that will refresh after given date
let timeline = Timeline(entries: entries, policy: .after(reloadDate))
completion(timeline)
}
}
By specifying after(_ date:) as the TimelineReloadPolicy, iOS will refresh the widget after the given date.
The timeline’s refresh policy specifies the earliest date for WidgetKit to request a new timeline from the provider. The default refresh policy, .atEnd, tells WidgetKit to request a new timeline after the last date in the array of timeline entries you provide. However, you can use .afterDate to indicate a different date either earlier or later than the default date. Specify an earlier date if you know there’s a point in time before the end of your timeline entries that may alter the timeline.
Ref: Apple Documentation on Timeline
This solution was to address the core issue, so finally I would like to add a disclaimer that reloading every 15mins might be overkill. Hence tweak your logic to specify the TimelineReloadPolicy intelligently. If you can predict values & create a timeline of multiple entries with fewer network calls then great! If not then... well... best of luck :)
Important
Plan ahead if your widget makes requests to a server when it reloads, and uses afterDate() with a specific date in timeline entries. WidgetKit tries to respect the date you specify, which may cause a significant increase in server load when multiple devices reload your widget at around the same time.
Ref: Keeping a Widget Up To Date
More Read:
Apple Documentation on TimelineProvider

Count current streak in swift

I would like to count how many continuous days user has used the app.
It updates the label depending on the streaks and if user has not used the app for a day the number goes back to zero.
How can I achieve this? I have searched but could not find any source.
For this you have a few things to take into consideration:
When to report last usage?
Your app idea may include the need to perform some actions before
considering a complete usage. For example, after loading or presenting something on the screen, after retrieving data and performing some actions, etc.
Just by intention of opening the app. The only intention is for the user to hit your app´s icon to launch the app, nevermind if he set it to a close state before even passing your loading screen.
This can be a bit unpredictable
When sending the app to background.
Important to notice that iOS can kill your process anytime after your
app is sent to background, so better to do it right after user´s
action.
Also, the user could not open your app again in a while.
You can subscribe to background capabilities for letting your app be active for a while longer while transitioning to suspended/close state if you are going to save data out of the iPhone.
The function you are looking for is applicationDidEnterBackground(_:)
Strong Points of this approach
You get last time that your app was actually used.
For more on the application life cycle and how to handle it correctly, please visit apple documentation about this topic
Do I need this information to be available between installs & Where to save ?
If you care about this counter to be stable and remains intact between installs you can not save it in any local database or NSUserDefaults. In this case you should implement some kind of online storage, via user creation & handling in your servers or the use of iCloud alternatives.
If your information is sensitive (let's say that you are going to give some money like reward to your user for opening your app 1000 times) then you can not store it in NSUserDefaults, as it is not encripted and can be modified.
What to save in order to count days in a row?
Simplicity is king when dealing with stored data and there are many ways to achieve this specific task.
I would go with:
Storing the first date (ignoring time if you are dealing with calendar days, but including it if you are handling 24hours lapses as your day instead)
Storing last visit date (same considerations apply).
You could save complete timestamp in order to be able of change your mind later ;-)
In my app I would do the maths then with current date data (now = NSDate()) before making any changes.
If timelapse between now and last visit date is bigger than
a "Day", then update first visit date with now.
Save now data into last visit date storage.
Your counter will always be the difference in "Days" between now and first visit date.
Summing Up
If your data is not sensitive store it in NSUserDefaults, otherwise and if this can affect your income store it somewhere else. If it's sensitive but you don't care if your user lose the counter, save it in a local DB (CoreData, Realm, etc)
Best time (as of my consideration) for storing new data will be when an intention of closure (included suspended state and incoming calls) is notified to your app.
You can save this data in many ways, one that give you some space for maneuvering is saving just last visit and date of first visit of the row and then do the maths. Of course, updating as needed and explained before.
private extension Sequence where Element == Date {
var streaks: Int {
let oneDayInSeconds: Double = 60*60*24
let days = self.compactMap { Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: $0) }
let uniq = Set(days).sorted(by: >)
var count = 0
guard var lastStreak = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date()) else { return count }
for date in uniq {
guard date > lastStreak.advanced(by: -oneDayInSeconds - 1) else { break }
count += 1
lastStreak = date
}
return count
}
}
Simply save the last date in your UserDefault or CoreData, then on the next date, use timeIntervalSinceDate to get the timeInterval, divide to 3600, if that value is more than 24, reset the number
I didn't checked but this idea will work, simply store current date (DD-MM-YYYY) into Array or list & save it using any Data Persistence Options. Before adding date (Whenever launching app - you can use didFinishLauchingOptions) into list check whether it had already or not.To showing continuous days use the array count of saved list.
To reset Zero just check last date is Available or not.
This is my implementation for keeping track of a login streak and if that streak was rewarded yet:
class func checkForStreak() -> Int {
let lastLogin = UserDefaults.standard.string(forKey: "lastLogin")
guard let lastLogin = lastLogin else {
UserDefaults.standard.set(1, forKey: "loginStreak")
UserDefaults.standard.set(Date().toString(), forKey: "lastLogin")
return 1
}
let format = DateFormatter()
format.dateFormat = "YYYY-MM-DD"
guard let lastLoginDate = format.date(from: lastLogin) else {
return 0
}
guard let modifiedDate = Calendar.current.date(byAdding: .day, value: 1, to: lastLoginDate) else {return 0}
if lastLoginDate.isToday {
//login on same day
return UserDefaults.standard.integer(forKey: "loginStreak")
} else if modifiedDate.isToday {
//streak is extended
var streak = UserDefaults.standard.integer(forKey: "loginStreak")
streak += 1
UserDefaults.standard.set(streak, forKey: "loginStreak")
UserDefaults.standard.set(false, forKey: "streakRewarded")
return streak
} else {
//streak is broken
UserDefaults.standard.set(1, forKey: "loginStreak")
return 1
}
}
you'll want to set:
UserDefaults.standard.set(true, forKey: "streakRewarded")
after rewarding the streak. I call this function in my viewDidLoad of the root view controller.
func streakCount() {
if USERDEFAULTS.value(forKey: "lastLogin") == nil{
USERDEFAULTS.set(1, forKey: "loginStreak")
USERDEFAULTS.set(Date().toString(), forKey: "lastLogin")
return
}
let lastLogin = USERDEFAULTS.value(forKey: "lastLogin") as! String
let format = DateFormatter()
format.dateFormat = "yyyy-MM-dd"
guard let lastLoginDate = format.date(from: lastLogin) else {
return
}
guard let modifiedDate = Calendar.current.date(byAdding: .day, value: 1, to: lastLoginDate) else {
return
}
if modifiedDate.isToday {
//streak is extended
var streak = USERDEFAULTS.integer(forKey: "loginStreak")
streak += 1
USERDEFAULTS.set(streak, forKey: "loginStreak")
USERDEFAULTS.set(Date().toString(), forKey: "lastLogin")
} else {
//streak is broken
let streak = USERDEFAULTS.integer(forKey: "loginStreak")
if USERDEFAULTS.value(forKey: "longestStreak") != nil{
let longestStreak = USERDEFAULTS.integer(forKey: "longestStreak")
let streak = USERDEFAULTS.integer(forKey: "loginStreak")
if streak > longestStreak{
USERDEFAULTS.set(streak, forKey: "longestStreak")
}
}
else{
USERDEFAULTS.set(streak, forKey: "longestStreak")
}
USERDEFAULTS.set(0, forKey: "loginStreak")
}
}

Resources