iOS Parse (Reduce API Requests) Perform function once a week every week on the same day

92 Views Asked by At

Using Parse, I'm trying to reduce the amount of network calls to parse in an effort to limit my API requests of nonessential data. To do this I thought it would be best to call a function only once a week on the same day and upload any differences from the previous week to the installation class (for syncing push notification channels in my circumstance)

So how do you call a function once a week every week on a particular day? And what do you if the day is already passed? So say you want something to happen every Thursday, but a user doesn't open the app until Sunday after that Thursday you were supposed to sync data?

2

There are 2 best solutions below

3
On BEST ANSWER

In practice, I find that a fixed interval makes more sense in more cases than a calendar milestone. With that, and very little NSDate logic, I can have my model guarantee that its data is at most N seconds out of date.

To do this, I have the model singleton keep track only of the date of the last update:

// initialize this to [NSDate distantPast]
@property(nonatomic,strong) NSDate *lastUpdate;

The interface also provides an asynch update method, like:

- (void)updateWithCompletion:(void (^)(BOOL, NSError *))completion;

I override the synthesized getter/setter of lastUpdate to wrap persistence:

// user defaults in this case, but there are several ways to persist a date
- (NSDate *)lastUpdate {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    return [defaults valueForKey:@"lastUpdate"];
}

- (void)setLastUpdate:(NSDate *)lastUpdate {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setValue:lastUpdate forKey:@"lastUpdate"];
    [defaults synchronize];
}

Finally, the asynch update opaquely decides whether the current data is good enough, or whether we should invoke the parse.com api...

- (void)updateWithCompletion:(void (^)(BOOL, NSError *))completion {

    NSTimeInterval sinceLastUpdate = -[self.lastUpdate timeIntervalSinceNow];
    NSTimeInterval updatePeriod = [self updatePeriod];
    if (sinceLastUpdate < updatePeriod) {
        // our current data is new enough, so
        return completion(YES, nil);
    } else {
        // our current data is stale, so call parse.com to update...
        [...inBackgroundWithBlock:(NSArray *objects, NSError *error) {
            if (!error) {
                // finish the update here and persist the objects, then...
                self.lastUpdate = [NSDate date];
                completion(YES, nil);
            } else {
                completion(NO, error);
            }
        }];
    }
}

The method updatePeriod answers whatever NSTimeInterval the app thinks is an acceptable age of the data. Usually, I get this from parse.config at some reasonably high frequency (like daily). This way, I can tune the frequency of model updates as I see fit for clients in the wild.

So with very little NSDate logic, I use this to keep the clients "up-to-date-enough", where even the "enough" part can be dynamically decided.

EDIT - we can still remain concise and set our model expiration to be a calendar day. I'd do that as follows:

- (NSDate *)lastSaturday {

    NSDate *now = [NSDate date];
    NSCalendar *calendar = [NSCalendar currentCalendar];

    NSDate *startOfWeek;
    [calendar rangeOfUnit:NSCalendarUnitWeekOfMonth startDate:&startOfWeek interval:NULL forDate:now];

    NSDateComponents *components = [[NSDateComponents alloc] init];
    [components setDay:-1];
    return [calendar dateByAddingComponents:components toDate:startOfWeek options:0];
}

Now, instead of updating when the interval expires, update if the last update was before last Saturday (or whatever weekday, you want... adjust by adjusting setDay:-n)

// change the date condition in updateWithCompletion
if (self.lastUpdate == [self.lastUpdate earlierDate:[self lastSaturday]) {
    // do the update
} else {
    // no need to update
}
0
On

The first thing you want to do is check if today is the day you want the data to sync. So for instance, I want every Thursday a function to be called to sync an array of objects to the 'channels' column.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    if ([self isTodayThursday]) {

    } else {

    }
}

- (BOOL) isTodayThursday {
    NSDateFormatter *nameOfDayOfWeekFormatter = [[NSDateFormatter alloc] init];
    [nameOfDayOfWeekFormatter setDateFormat:@"EEEE"];

    if ([[nameOfDayOfWeekFormatter stringFromDate:[NSDate date]] isEqualToString:@"Thursday"]) {
    return YES;
    }
    return NO;
}

Simple, here we are checking if the name of the day of the week is Thursday. Now we want to make sure we aren't stacking syncs and ensure we have or have not synced already today, if it is a Thursday. We do that by validating against an object in NSUserDefaults :

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    if ([self isTodayThursday]) {
        if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"lastSynced"] isEqualToString:[[self dateFormatter] stringFromDate:[NSDate date]]]) {
            //So we check against a custom date formatter that we want of the last sync date we performed, and if it's already been synced today, then do nothing
        } else {
            //if not, sync plist (or whatever we want) to channels array
            [self syncPlistToParse];
        }
    } else {

    }
}

- (NSDateFormatter *)dateFormatter {

    NSDateFormatter *formatedDate = [[NSDateFormatter alloc] init];
    [formatedDate setDateFormat:@"yyyyMMdd"];
    NSLog(@"Today is %@", [formatedDate stringFromDate:[NSDate date]]);
    return formatedDate;
}

Sync plist to PFInstallation channels array

-(void)syncPlistToParse {
    //First obtain whatever data you have from where ever you store it for the week. I chose to save all the data in a plist until the next sync date to reduce API calls.
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *path = [paths objectAtIndex:0];
    NSString *plistPath = [path stringByAppendingPathComponent:@"AlertSubscriptions.plist"];
    NSMutableArray *array = [[NSMutableArray alloc] initWithContentsOfFile:plistPath];

    PFInstallation *currentInstallation = [PFInstallation currentInstallation];
    [currentInstallation addUniqueObjectsFromArray:array forKey:@"channels"];
    [currentInstallation saveInBackgroundWithBlock:^(BOOL success, NSError *error) {
      if (success) {
        //if successful save lastSynced date to NSUserDefaults to today
        [[NSUserDefaults standardUserDefaults] setObject:[[self dateFormatter] stringFromDate:[NSDate date]] forKey:@"lastSynced"];
      } else {
       //It wasn't a successful save so you can do whatever you want on an unsuccessful save, but I chose to use `saveEventually` as a back-up for connectivity issues etc
        PFInstallation *currentInstallation = [PFInstallation currentInstallation];
        [currentInstallation addUniqueObjectsFromArray:array forKey:@"channels"];
        [currentInstallation saveEventually];
      }
  }];
}

So if today is NOT Thursday how do we determine if it's before or after the lastSynced date and how do we determine how to set the next sync date?

Lets use these dates as an example:

                  June 2015
---------------------------------------------
|SUN | MON | TUES | WED | THURS | FRI | SAT |
---------------------------------------------
|    |     |      |     |  18   | 19  | 20  |
---------------------------------------------
| 21 |  22 |  23  |  24 |  25   | 26  | 27  |
---------------------------------------------
| 28 |  29 |  30  |  1  |  02   | 03  | 04  |
---------------------------------------------

So lets say the last synced Thursday was 18th of June and the user doesn't open their app until 26 of June (Friday) we need to validate against that. We already checked if today was a Thursday and we know it's not since it's Friday the 26th. So there are two descrepencies we need to do checks and balances on, most import, we need to check if that Friday was within the same week of the current sync cycle (the 19th Friday), but in this case since the current week cycle already passed because we didn't open it up until past the next cycle (which was supposed to be the 25th of June)

    if ([self isTodayThursday]) {
        if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"lastSynced"] isEqualToString:[[self dateFormatter] stringFromDate:[NSDate date]]]) {
            //So we check against a custom date formatter that we want of the last sync date we performed, and if it's already been synced today, then do nothing
        } else {
            //if not, sync plist (or whatever we want) to channels array
            [self syncPlistToParse];
        }
    } else {
        //Not Thursday. So we need to check if its within the same week of the sync cycle or passed that date
        int lastSyncDate = [[self lastSyncDate] intValue];
        NSLog(@"lastSyncDate int %d", lastSyncDate);
        int nextSyncDate = [[self nextSyncDate] intValue];
        NSLog(@"nextSyncDate int %d", nextSyncDate);

        if (lastSyncDate < nextSyncDate) {
            NSLog(@"next sync date is this coming thursday");
        } else {
            // if not before thursday (already passed Thursday), sync and save today as lastSynced
            [self syncPlistToParse];
        }
    }
}

- (NSString *)lastSyncDate {
    NSString *date = [[NSUserDefaults standardUserDefaults] objectForKey:@"lastSynced"];
    return date;
}

- (NSString *)nextSyncDate {

    NSCalendar *cal = [NSCalendar currentCalendar];
    NSDate *lastSync = [[self dateFormatter] dateFromString:[self lastSyncDate]];
    NSDateComponents *components = [cal components:(NSCalendarUnitDay) fromDate:lastSync];
    [components setDay: +[self daysUntilNearestThursday]];
    NSDate *nextSync = [cal dateByAddingComponents:components toDate:lastSync options:0];    
    return [[self dateFormatter] stringFromDate:nextSync];
}

-(int)daysUntilNearestThursday {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"EEEE"];
    //Note the day of the week is including today thats why it's always +1
    if ([[dateFormatter stringFromDate:[NSDate date]] isEqualToString:@"Monday"]) {
      return 4;
    }
    if ([[dateFormatter stringFromDate:[NSDate date]] isEqualToString:@"Tuesday"]) {
      return 3;
    }
    if ([[dateFormatter stringFromDate:[NSDate date]] isEqualToString:@"Wednesday"]) {
      return 2;
    }
    if ([[dateFormatter stringFromDate:[NSDate date]] isEqualToString:@"Friday"]) {
      return 7;
    }
    if ([[dateFormatter stringFromDate:[NSDate date]] isEqualToString:@"Saturday"]) {
      return 6;
    }
    if ([[dateFormatter stringFromDate:[NSDate date]] isEqualToString:@"Sunday"]) {
      return 5;
    }
    return 0;
}

NOTE It doesn't matter what the nextSyncDate is because we always check if it's Thursday first and then act accordingly

This is my personal opinion, someone asked me about this and this is what I came up with. I feel like theres an easier solution out there that accurately resets the nextSync to the nearest Thursday instead of [components setDay: +[self daysUntilNearestThursday]]; So by all means, please add input/suggestions and comments to make this a better opportunity for future question seekers. I think Parse is fantastic, but at the same time it's a business, and they have to act accordingly, but certain things like API requests for everything ever for a scaleable app (2,000+ users) is kind of bad practice if you intend on keeping developers. This is an alternative I thought of to help my friend out, it needs work, not saying its the only way, just the way I thought would work for their project. Please, don't be shy, add edits or corrections.