iOS Swift function isDateInToday not working on some phones - ios

Our app requires that the user carries out a daily task. If he/she completes the task, then at midnight a new task is released. If they don't, then come midnight they will continue seeing the previous task until they do.
I'm saving the current date in User Defaults and I'm using isDateInToday to heck if the date has changed:
func checkIfDayChanged() -> Bool {
if let date = UserDefaults.standard.object(forKey: "currentDate") as? Date {
var calendar = Calendar.current
calendar.timeZone = TimeZone.current
let isSameDay = calendar.isDateInToday(date)
if (isSameDay) {
return false
}
}
return true
}
In the majority of cases this works fine. However, there is a small group of users that are stuck with the same task even though they completed it the previous day. For some reason the app is not recognizing that the day is a new one.
Could this be a setting on the phone? Or do I need to change this code? Why wouldn't this be working in only a few devices?
UPDATE:
This is what happens when the daily task (a lesson) is completed:
func onComplete() {
let latestLesson = UserDefaults.standard.integer(forKey: "currentLesson")
if String(latestLesson) == lesson?.lessonNumber {
if (UserDefaults.standard.bool(forKey: "dayLessonCompleted") == false) {
UserDefaults.standard.set(true, forKey: "dayLessonCompleted")
lesson?.completed = true
}
}
}
Every day, the following function runs:
private func checkForMoreLessons() {
// Check the date
let newDay = checkIfDayChanged()
// If the day changed, check if user has watched the previous lesson
if newDay {
if (UserDefaults.standard.bool(forKey: "dayLessonCompleted") == true) {
// Save the new day
UserDefaults.standard.set(Date(), forKey:"currentDate")
// Add 1 to the current date
let currentLesson = UserDefaults.standard.integer(forKey: "currentLesson")
UserDefaults.standard.set(currentLesson + 1, forKey: "currentLesson")
UserDefaults.standard.set(false, forKey: "dayLessonCompleted")
}
}
}

Related

field value in firestore document increments by wrong number due to print statement repeating 4 times

So my goal is to always have a proper increment value when a ticket is purchased. I'm having a strange bug where when I update a field value by incrementing it, it increments by the wrong number the first time every time I run a fresh new simulation due to the repeats in print statements, but if I was to update the value again in the same fresh simulation, it will increment by the proper value every time after that.
The value I use to increment is a label text which is dependent on a UIStepper value.
Here is the complete function I use to update and increment this value:
func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, handler completion: #escaping (PKPaymentAuthorizationResult) -> Void) {
guard let count = guestNumberCount.text else { return }
guard let labelAsANumber = Int64(count) else { return }
guard let user = Auth.auth().currentUser else { return }
let dfmatter = DateFormatter()
dfmatter.dateFormat = "EEEE, MMMM d yyyy, h:mm a"
let dateFromString = dfmatter.date(from: actualDateOfEvent.text ?? "")
let dateStamp: TimeInterval = dateFromString!.timeIntervalSince1970
let dateSt: Int = Int(dateStamp)
getStudentID { (studid) in
if let id = studid {
self.db.document("student_users/\(user.uid)/events_bought/\(self.navigationItem.title!)").setData(["event_name": self.navigationItem.title!, "event_date": self.actualDateOfEvent.text ?? "", "event_cost": self.actualCostOfEvent.text ?? "", "for_grades": self.gradeOfEvent , "school_id": id, "expiresAt":dateSt, "isEventPurchased": true, "time_purchased": Date()], merge: true) { (error) in
if let error = error {
print("There was an error trying to add purchase info to the database: \(error)")
} else {
print("The purchase info was successfully stored to the database!")
}
}
}
}
getDocumentIDOfSelectedEvent { (eventidentification) in
if let eventid = eventidentification {
self.getSchoolDocumentID { (docID) in
if let doc = docID {
self.db.document("school_users/\(doc)/events/\(eventid)/extraEventInfo/TicketCount").updateData(["ticketsPurchased": FieldValue.increment(Int64(labelAsANumber))]) { (error) in
if let error = error {
print("There was an error updating the number of tickets: \(error)")
} else {
print("Number of tickets purchased succesfully updated!")
print(labelAsANumber)
}
}
}
}
}
}
performSegue(withIdentifier: Constants.Segues.fromTicketFormToPurchaseDetails, sender: self)
completion(PKPaymentAuthorizationResult(status: .success, errors: []))
}
I can't really figure out any issues within the function, neither within my whole project as well in regards to this situation other than the print statement. In the print(labelAsANumber) statement, it prints the correct value of the label text (stepper value), but it prints it four times, which is causing the issue. How can I prevent this repetition of print statements and overall incorrect numeric incrementing?

Unwrap optionals of the type Date() from CoreData?

I have created a NSManagedObject sReminderDate: Date(). This is stored in the coreData. The value for this comes from the date and time set by the user from the datePicker.
When the viewcontroller is loaded, this value is compared with the currentDate and time and if the currentTime > sReminderDate then a label says "Time's Up". The issue is that when the user loads the viewcontroller for the first time, the time and date is not set the sReminderDate is 'null'. Hence, it's crashing my app because this is null and I am using it for comparison.
I am having a hard time unwrapping it with if-let and guard statements. Help would be appreciated!
let currentDate = NSDate() //Current date and time stored whenever viewcontroller is loaded
if ((currentDate as! Date ) >= (editnotes?.sSelectedDate)! && (editnotes?.sReminderState == true)) //Comparison -> sSelectedDate is the date stored in coreData by the user when scheduling task
{
editnotes?.sReminderDate = "Time is up"
reminderMsg.text = editnotes?.sReminderDate
}
Look what you wanna to achieve is to separate code when you don't have this values and when you got them and can compare, i.e. implement your logic
I'd make something like that:
guard let selectedDate = editnotes?.sSelectedDate,
let needsToRemind = editnotes?.sReminderState else {
// implement logic when you DO NOT have values
return
}
// Everything is cool and you have values => can compare
if selectedDate < Date() && needsToRemind {
editnotes?.sSelectedDate = "Time's up"
// Possibly editnotes?.sReminderDate better to unwrap somehow
// if-let, guard-let, ??
reminderMsg.text = editnotes?.sReminderDate
}
Code is much cleaner and more representative
You can use comma (,) as AND operator in swift.
let currentDate = Date()
if let editnotes = editnotes, currentDate >= editnotes.sSelectedDate!, editnotes.sReminderState == true {
editnotes?.sReminderDate = "Time is up"
reminderMsg.text = editnotes?.sReminderDate
}
Updated ans
let currentDate = Date()
if let selectedDate = editnotes?.sSelectedDate, let reminderState = editnotes?.sReminderState, currentDate >= selectedDate, reminderState == true {
editnotes?.sReminderDate = "Time is up" reminderMsg.text = editnotes?.sReminderDate
}

SKStore​Review​Controller, How to use it in a correct way?

I have seen some answer but not satisfied with them and got some idea, but don't know how to use it properly, so that it will execute in proper way, though i think it should be used in App delegates didFinishLaunching, but i wanted to be sure before implement it in Live app without any hustle.
SKStore​Review​Controller is only work for ios 10.3 what i read, could anybody explain with little bit of code in swift and objective c.
UPDATE:
Actually I'm confused about calling the method request​Review(), Where do i need to call this method? in rootViewController's viewDidLoad or in appDelegate's didFinishlaunching ?
Thanks.
SKStoreReviewController is available in iOS 10.3 and later.
According to APPLE's Documents:
You can ask users to rate or review your app while they're using it,
without sending them to the App Store.You determine the points in the
user experience at which it makes sense to call the API and the system
takes care of the rest.
Inorder to display Rate/Review inside the app, you have to add StoreKitframework.
Please find the Sample code for both language:
Objective C:
#import <StoreKit/StoreKit.h>
- (void)DisplayReviewController {
if([SKStoreReviewController class]){
[SKStoreReviewController requestReview] ;
}
}
since xCode 9 you can do:
#import <StoreKit/StoreKit.h>
- (void)DisplayReviewController {
if (#available(iOS 10.3, *)) {
[SKStoreReviewController requestReview];
}
}
Swift:
import StoreKit
func DisplayReviewController {
if #available( iOS 10.3,*){
SKStoreReviewController.requestReview()
}
}
Update: Ask for a rating only after the user has demonstrated engagement with your app
For Swift,
import StoreKit
Add below code to request when you want to ask.
if #available(iOS 10.3, *) {
SKStoreReviewController.requestReview()
}
For Objective C,
1-) Added StoreKit framework from Link Binary With Library
2-) Added framework
#import <StoreKit/StoreKit.h>
3-) Added below code where you want to call App-Review pop-up. In this case, i added in viewDidLoad.
- (void)viewDidLoad {
[super viewDidLoad];
[SKStoreReviewController requestReview];
}
4-) You should be aware of below explain from Apple, When you test in debug mode
When you call this method while your app is still in development mode, a rating/review request view is always displayed so that you can test the user interface and experience. However, this method has no effect when you call it in an app that you distribute using TestFlight.
I think directly calling the below is not an good idea
SKStoreReviewController.requestReview()
It can be done like whenever user opens your app the multiple of 10(10,20,30,...100) then you can show for review
so first of all you need to create a file that will be responsible for everything like saving your application open count in user defaults , retrieving application open count, and showing requestReview()
kindly have a look at the following code snippet
import Foundation
import StoreKit
class SpsRateManager {
private static let instance = SpsRateManager()
var shareinstance: SpsRateManager{
return .instance
}
static func incrementAppOpenedCount() { // called from appdelegate didfinishLaunchingWithOptions:
let userdefault = UserDefaults.standard
let savedvalue = userdefault.integer(forKey: Configuration.APPLICATIONOPENCOUNTSTATUS)
if savedvalue == 0 {
print("Not saved ")
userdefault.set(1, forKey: Configuration.APPLICATIONOPENCOUNTSTATUS)
}
else{
userdefault.set(savedvalue+1, forKey: Configuration.APPLICATIONOPENCOUNTSTATUS)
}
}
static func checkAppopencountandProvideReview(){
let userdefault = UserDefaults.standard
let appopencountvalue = userdefault.integer(forKey: Configuration.APPLICATIONOPENCOUNTSTATUS)
if appopencountvalue % 10 == 0 {
print("its been 10 times so ask for review ")
SpsRateManager().requestReview()
}
else{
print("not enough open count dont show ")
}
}
fileprivate func requestReview() {
if #available(iOS 10.3, *) {
SKStoreReviewController.requestReview()
} else {
// Fallback on earlier versions
// Try any other 3rd party or manual method here.
}
}
}
Adding onto korat's great answer above...
If your supporting a legacy Objective-C app and you want to call DisplayReviewController after a few app opens then do the following:
In your class AppDelegate.m add this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
int count = [[NSUserDefaults standardUserDefaults] integerForKey:#"LaunchCount"];
if(count < 0) count = 0;
[[NSUserDefaults standardUserDefaults] setInteger:count+1 forKey:#"LaunchCount"];
}
//The application was in background and become active
- (void)applicationWillEnterForeground:(UIApplication *)application {
int count = [[NSUserDefaults standardUserDefaults] integerForKey:#"LaunchCount"];
if(count < 0) count = 0;
[[NSUserDefaults standardUserDefaults] setInteger:count+1 forKey:#"LaunchCount"];
}
and in the controller you want to call the function:
- (void)applicationDidBecomeActive {
if ([[NSUserDefaults standardUserDefaults] integerForKey:#"LaunchCount"] == 5) {
[self DisplayReviewController];
}
}
I think you may implement a method to count when they run the app and store it in UserDefaults, then call requestReview() if the count number is 5 or 10 or something like that (it depends on you), by this way you have more chance of getting a good review.
Here's a utility function I am developing for my own use case that might help a lot of other people. (Feel free to roast and improve/correct my code :D). I am working on a speech practice app and I want to ask for a rating after the user has done a few recordings. I will add the main function and then other helper functions used below it. The brief logic is, you can request a review 3 times a year, so if 1 year has passed, I reset the ask count to 0. Also, the review request won't be presented for each ask. So I have an upper limit of 30 asks before I don't allow the app to attempt review requests anymore. This won't be taken into consideration if the app version has changed, as you can again ask for a review for the new app version.
/// Requests review from user based on certain conditions.
/// 1. Should have recorderd at least 3 recordings (if you want to force attept a review ask don't pass any parameter)
/// 2. Has not already asked for a review today
/// 3. A probabitly of 50% if will ask today
/// 4. If review has not been asked more than 30 times in the same year for the current version
/// - Parameter numberOfRecordings: If the number of recordings is greater than 3 then a review will be asked.
func askForReview(numberOfRecordings: Int = 5) {
let defaults = UserDefaults.standard
let lastAskedReviewAt = defaults.double(forKey: lastAskedReviewAtKey)
let dateStringForLastReviewAsk = getDateString(from: lastAskedReviewAt)
let dateForLastReviewAsk = getDate(from: dateStringForLastReviewAsk) ?? Date(timeIntervalSince1970: 0)
let askedReviewToday = Calendar.current.isDateInToday(dateForLastReviewAsk)
var appReviewRequestsCount = defaults.integer(forKey: appReviewRequestsCountKey)
if Date().localDate().years(from: dateForLastReviewAsk) >= 1 {
defaults.setValue(0, forKey: appReviewRequestsCountKey)
appReviewRequestsCount = 0
}
var isAskingReviewForSameVersion = false
if let currentlyInstalledVersion = getInstalledVersionNumber(), let lastReviewAskedForVersion = defaults.string(forKey: lastReviewAskedForVersionKey) {
if currentlyInstalledVersion == lastReviewAskedForVersion {
isAskingReviewForSameVersion = true
} else {
appReviewRequestsCount = 0
defaults.setValue(0, forKey: appReviewRequestsCountKey)
}
}
let askingReviewTooManyTimes = appReviewRequestsCount >= 30 && isAskingReviewForSameVersion
let totalRecordingsTillDateCount = defaults.integer(forKey: totalRecordingsTillDateCountKey)
let localNumberOfRecordings = max(numberOfRecordings, totalRecordingsTillDateCount)
if localNumberOfRecordings > 3 && Bool.random() && !askedReviewToday && !askingReviewTooManyTimes {
SKStoreReviewController.requestReview()
defaults.setValue(Date().timeIntervalSince1970, forKey: lastAskedReviewAtKey)
if let versionNumber = getInstalledVersionNumber() {
defaults.setValue(versionNumber, forKey: lastReviewAskedForVersionKey)
}
defaults.setValue(appReviewRequestsCount + 1, forKey: appReviewRequestsCountKey)
}
}
Dictionary Keys:
let lastAskedReviewAtKey = "LastAskedReviewAt"
let appReviewRequestsCountKey = "AppReviewRequestsCount"
let lastReviewAskedForVersionKey = "AskedReviewForVersion"
let appVersionNumberKey = "CFBundleShortVersionString"
Helper Functions:
/// Get a string representation in current local time for a timestamp
/// - Parameter timestamp: Timestamp to be converted to date string
/// - Returns: A date string from passed timestamp in dd MMM yyy format
func getDateString(from timestamp: Double) -> String {
let dateFormatter = getDateFormatter()
let date = Date(timeIntervalSince1970: timestamp)
let dateString = dateFormatter.string(from: date)
return dateString
}
/// Get a date from a string of date format dd MMM yyyy.
/// - Parameter dateString: Date string formated as dd MMM yyyy
/// - Returns: A date object by parsing date in dd MMM yyy format
func getDate(from dateString: String) -> Date? {
// print("Date String: ", dateString)
let dateFormatter = getDateFormatter()
return dateFormatter.date(from: dateString) ?? nil
}
//Ref: https://stackoverflow.com/questions/27182023/getting-the-difference-between-two-dates-months-days-hours-minutes-seconds-in
extension Date {
/// Returns the amount of years from another date
func years(from date: Date) -> Int {
return Calendar.current.dateComponents([.year], from: date, to: self).year ?? 0
}
/// Returns the amount of months from another date
func months(from date: Date) -> Int {
return Calendar.current.dateComponents([.month], from: date, to: self).month ?? 0
}
/// Returns the amount of weeks from another date
func weeks(from date: Date) -> Int {
return Calendar.current.dateComponents([.weekOfMonth], from: date, to: self).weekOfMonth ?? 0
}
/// Returns the amount of days from another date
func days(from date: Date) -> Int {
return Calendar.current.dateComponents([.day], from: date, to: self).day ?? 0
}
/// Returns the amount of hours from another date
func hours(from date: Date) -> Int {
return Calendar.current.dateComponents([.hour], from: date, to: self).hour ?? 0
}
/// Returns the amount of minutes from another date
func minutes(from date: Date) -> Int {
return Calendar.current.dateComponents([.minute], from: date, to: self).minute ?? 0
}
/// Returns the amount of seconds from another date
func seconds(from date: Date) -> Int {
return Calendar.current.dateComponents([.second], from: date, to: self).second ?? 0
}
/// Returns the a custom time interval description from another date
func offset(from date: Date) -> String {
if years(from: date) > 0 { return "\(years(from: date))y" }
if months(from: date) > 0 { return "\(months(from: date))M" }
if weeks(from: date) > 0 { return "\(weeks(from: date))w" }
if days(from: date) > 0 { return "\(days(from: date))d" }
if hours(from: date) > 0 { return "\(hours(from: date))h" }
if minutes(from: date) > 0 { return "\(minutes(from: date))m" }
if seconds(from: date) > 0 { return "\(seconds(from: date))s" }
return ""
}
}
//Ref: https://stackoverflow.com/questions/28404154/swift-get-local-date-and-time
extension Date {
func localDate() -> Date {
let nowUTC = Date()
let timeZoneOffset = Double(TimeZone.current.secondsFromGMT(for: nowUTC))
guard let localDate = Calendar.current.date(byAdding: .second, value: Int(timeZoneOffset), to: nowUTC) else {return Date()}
return localDate
}
}
func getInstalledVersionNumber() -> String? {
guard let infoDictionary = Bundle.main.infoDictionary, let currentVersionNumber = infoDictionary[appVersionNumberKey] as? String else { return nil}
return currentVersionNumber
}

Grand Central Dispatch in Swift only works the first run

I got a method which works like a refresher which uses the GCD pattern as shown below:
func getStepsForTheWeek() {
let concurrentQueue : dispatch_queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(concurrentQueue, {
// Create an array of Days.
var days = [Day]()
dispatch_sync(concurrentQueue, {
print("first")
for day in 0...7 {
let date = self.getDate(day)
// Get the date the day after that day.
let endDate = self.getDateDayAfter(date)
// Create a Day.
var day = Day(date: date)
self.pedometer.queryPedometerDataFromDate(date, toDate: endDate, withHandler: {numberOfSteps, error in
print("fetching")
if error != nil {
print("There was an error requesting data from the pedometer: \(error)")
} else {
day.steps = numberOfSteps!.numberOfSteps as Int
days.append(day)
}
})
}
})
dispatch_sync(dispatch_get_main_queue(), {
print("second")
self.historyViewController.days = days
self.historyViewController.reloadHistory()
})
})
}
When the app starts the method works as it is intended to.
But when the app is in the background and when I'm going back to the app I got this Observer which calls the method again to refresh its content.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "appBecomeActive", name: UIApplicationWillEnterForegroundNotification, object: nil )
But everytime I do this the second code-block is running before the first one.
Any help?
i think you should do the UI update in the completion handler of the pedometer query
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {
self.pedometer.queryPedometerDataFromDate(date, toDate: endDate, withHandler: { numberOfSteps, error in
if error != nil {
print("There was an error requesting data from the pedometer: \(error)")
} else {
let numberOfStepsThisDay = numberOfSteps?.numberOfSteps as! Int
day.steps = numberOfStepsThisDay
days.append(day)
}
dispatch_async(dispatch_get_main_queue(), {
self.historyViewController.days = self.days
self.historyViewController.reloadHistory()
})
})
})
you should always update the UI in the completion handler of the method if it has one, since you dont know whether it could be asynchronous or not (usually would be asynchronous if using a completion handler), you can probably drop the outer dispatch_async if there is no other code in it besides the pedometer query
I solve it.
In the query I check if the array of days is filled (e.g size of 8).
When filled the reload should be done. I also deleted all unnecessary sync-tasks. Much cleaner now. It was simpler than I thought.
func getStepsForTheWeek() {
// Create an array of Days.
var days = [Day]()
print("first")
// Fetch the total steps per day for 8 days (0 - 7).
for day in 0...7 {
// Get days date from today.
let date = self.getDate(day)
// Get the date the day after that day.
let endDate = self.getDateDayAfter(date)
// Create a Day.
var day = Day(date: date)
// Query the Pedometer for the total steps of that day.
self.pedometer.queryPedometerDataFromDate(date, toDate: endDate) {
(data, error) -> Void in
if(error == nil){
print("fetching")
day.steps = data!.numberOfSteps as Int
days.append(day)
if(days.count == 8){
dispatch_async(dispatch_get_main_queue(), { () -> Void in
print("finished")
self.historyViewController.days = days
self.historyViewController.reloadHistory()
})
}
}
}
}
}
Thanks to Fonix and gnasher729.

Deleting reminders from calendar in Swift

I use the following function to retrieve my calendar:
func retrieveCalendar() -> EKCalendar? {
appDelegate = UIApplication.sharedApplication().delegate
as? AppDelegate
var myCalendar: EKCalendar?
let calendars = appDelegate!.eventStore!.calendarsForEntityType(EKEntityTypeReminder) as! [EKCalendar]
let filteredCalendars = calendars.filter {$0.title == "MedicalCalendar"}
if filteredCalendars.isEmpty {
println("could not find reminder calendar 'MedicalCalendar'")
return nil
} else {
myCalendar = filteredCalendars[0]
return myCalendar!
}
}
However, anytime I add new events to the calendar I'd like to check if they already exist there. I figured out that the easiest approach would be to delete all reminders and load new ones again. I tried:
self.retrieveCalendar()?.reset()
But it does not work. How can I remove reminders from calendar?(either one at a time or all of them at once)
To check reminders you have to call the method fetchRemindersMatchingPredicate() in conjunction with predicateForRemindersInCalendars or predicateForIncompleteRemindersWithDueDateStarting:ending:calendars: or predicateForCompletedRemindersWithCompletionDateStarting:ending:calendars:
For example if you want to delete all expired reminders in the past until now, use something like this
assumed properties:
var calendar : EKCalendar // current calendar
let eventStore : EKEventStore // current event store
code
func removeExpiredReminders() {
let pastPredicate = eventStore.predicateForIncompleteRemindersWithDueDateStarting(nil, ending:NSDate(), calendars:[calendar])
eventStore.fetchRemindersMatchingPredicate(pastPredicate) { foundReminders in
let remindersToDelete = !foundReminders.isEmpty
for reminder in foundReminders as! [EKReminder] {
self.eventStore.removeReminder(reminder, commit: false, error: nil)
}
if remindersToDelete {
self.eventStore.commit(nil)
}
}
}
in the loop you can check for further conditions

Resources