Schedule multiple, daily events with NSTimer?
Asked Answered
S

3

1

I have a schedule cache stored in an NSDictionary.

For the example below, I have a schedule time of January 13, 20120 2:00PM and January 13, 2012 2:05PM. How can I add both of these to a queue to fire on their own?

Build the Schedule Cache:

-(void) buildScheduleCache
{  
    NSCalendarDate *now = [NSCalendarDate calendarDate];

    NSFileManager *manager = [[NSFileManager defaultManager] autorelease];
    path = @"/var/mobile/Library/MobileProfiles/Custom Profiles";
    theProfiles = [manager directoryContentsAtPath:path];

    myPrimaryinfo = [[NSMutableArray arrayWithCapacity:6] retain];
    keys = [NSArray arrayWithObjects:@"Profile",@"MPSYear",@"MPSMonth",@"MPSDay",@"MPSHour",@"MPSMinute",nil];

    for (NSString *profile in theProfiles) 
    {
        plistDict = [[[NSMutableDictionary alloc] initWithContentsOfFile:[NSString stringWithFormat:@"%@/%@",path,profile]] autorelease];

        [myPrimaryinfo addObject:[NSDictionary dictionaryWithObjects:
                                  [NSArray arrayWithObjects:
                                   [NSString stringWithFormat:@"%@",profile], 
                                   [NSString stringWithFormat:@"%@",[plistDict objectForKey:@"MPSYear"]], 
                                   [NSString stringWithFormat:@"%@",[plistDict objectForKey:@"MPSMonth"]], 
                                   [NSString stringWithFormat:@"%@",[plistDict objectForKey:@"MPSDay"]], 
                                   [NSString stringWithFormat:@"%@",[plistDict objectForKey:@"MPSHour"]], 
                                   [NSString stringWithFormat:@"%@",[plistDict objectForKey:@"MPSMinute"]],
                                   nil]forKeys:keys]];

        profileSched =
        [NSCalendarDate dateWithYear:[plistDict objectForKey:@"MPSYear"]
                               month:[plistDict objectForKey:@"MPSMonth"]
                                 day:[plistDict objectForKey:@"MPSDay"]
                                hour:[plistDict objectForKey:@"MPSHour"]
                              minute:[plistDict objectForKey:@"MPSMinute"]
                              second:01
                            timeZone:[now timeZone]];

        [self rescheduleTimer];
    }

    NSString *testPath = @"/var/mobile/Library/MobileProfiles/Schedules.plist";
    [myPrimaryinfo writeToFile:testPath atomically:YES];
}

Schedule The Event:

-(void) scheduleProfiles
{
    NSFileManager *manager = [[NSFileManager defaultManager] autorelease];
    path = @"/var/mobile/Library/WrightsCS/MobileProfiles/Custom Profiles";
    theProfiles = [manager contentsOfDirectoryAtPath:path error:nil];

    for (NSString *profile in theProfiles) 
    {
        NSMutableDictionary * plistDict = [[[NSMutableDictionary alloc] initWithContentsOfFile:[NSString stringWithFormat:@"%@/%@",path,profile]] autorelease];

        profileSched =
        [NSCalendarDate dateWithYear:[[plistDict objectForKey:@"MPSYear"] intValue]
                               month:[[plistDict objectForKey:@"MPSMonth"] intValue]
                                 day:[[plistDict objectForKey:@"MPSDay"] intValue]
                                hour:[[plistDict objectForKey:@"MPSHour"] intValue]
                              minute:[[plistDict objectForKey:@"MPSMinute"] intValue]
                              second:01
                            timeZone:[NSTimeZone localTimeZone]];


            NSLog(@"DATE: %@      SCHEDULE: %@      PROFILE: %@",[NSDate date],profileSched,profile);
        if([NSDate date] < profileSched)
        {
            NSLog(@"IGNORING PROFILE: %@     WITH SCHEDULE: %@",profile,profileSched);
        }else{
            //Create the timer from the Cached Array
            schedTimer = [[NSTimer alloc] initWithFireDate:profileSched //[NSDate dateWithTimeIntervalSinceNow: 10]
                                                  interval:0.1f
                                                    target:self
                                                  selector:@selector(fireCustomProfile:)
                                                  userInfo:profile
                                                   repeats:NO];//[[plistDict objectForKey:@"MPSRepeat"] boolValue]];

            MLogString(@"Scheduling Profile: %@",profile);
            [[NSRunLoop currentRunLoop] addTimer:schedTimer forMode:NSDefaultRunLoopMode];
        }
    }
}

Fire the Event:

-(void)fireCustomProfile:(NSTimer *)timer
{   
    if([[NSDate date] earlierDate:[schedTimer fireDate]])
    {
        NSLog(@"Ignoring Profile: %@",[schedTimer userInfo]);
        return;
    }

    notify_post("com.wrightscs.MobileProfiles.setCustomProfile");
}

Example Event:

<array>
    <dict>
        <key>MPSDay</key>
        <string>13</string>
        <key>MPSHour</key>
        <string>21</string>
        <key>MPSMinute</key>
        <string>15</string>
        <key>MPSMonth</key>
        <string>1</string>
        <key>MPSYear</key>
        <string>2012</string>
        <key>Profile</key>
        <string>Event 1</string>
        <key>Repeat</key>
        <true/>
    </dict>
</array>
<array>
    <dict>
        <key>MPSDay</key>
        <string>13</string>
        <key>MPSHour</key>
        <string>21</string>
        <key>MPSMinute</key>
        <string>20</string>
        <key>MPSMonth</key>
        <string>1</string>
        <key>MPSYear</key>
        <string>2012</string>
        <key>Profile</key>
        <string>Event 2</string>
        <key>Repeat</key>
        <true/>
    </dict>
</array>
Supporting answered 13/4, 2010 at 20:33 Comment(2)
The part "daily" makes me think "not a timer." Can you really guarantee your app is never terminated or sent to the background?Madi
Not an issue for this project, the schedule cache is rebuilt often.Supporting
P
3

You can use UILocalNotification instead of NSTimer. The limitations are that there is no actions in your application without your interaction, but there is triggered a notification even if the app is shutted down.

Apple documentation about UILocalNotifications:

Instances of UILocalNotification represent notifications that an application can schedule for presentation to its users at specific dates and times. The operating system is responsible for delivering the notification at the proper time; the application does not have to be running for this to happen.

...

When the system delivers a local notification, several things can happen, depending on the application state and the type of notification. If the application is not frontmost and visible, the system displays the alert message, badges the application, and plays a sound—whatever is specified in the notification. If the notification is an alert and the user taps the action button (or, if the device is locked, drags open the action slider), the application is launched. In the application:didFinishLaunchingWithOptions: method the application delegate can obtain the UILocalNotification object from the passed-in options dictionary by using the UIApplicationLaunchOptionsLocalNotificationKey key. The delegate can inspect the properties of the notification and, if the notification includes custom data in its userInfo dictionary, it can access that data and process it accordingly. On the other hand, if the local notification only badges the application icon, and the user in response launches the application, the application:didFinishLaunchingWithOptions: method is invoked, but no UILocalNotification object is included in the options dictionary.

If the application is foremost and visible when the system delivers the notification, no alert is shown, no icon is badged, and no sound is played. However, the application:didReceiveLocalNotification: is called if the application delegate implements it. The UILocalNotification instance is passed into this method, and the delegate can check its properties or access any custom data from the userInfo dictionary.

You can found a good tutorial here where the main idea is like this:

The notification setup:

Class cls = NSClassFromString(@"UILocalNotification");
if (cls != nil) {
    UILocalNotification *notif = [[cls alloc] init];
    notif.fireDate = [datePicker date];
    notif.timeZone = [NSTimeZone defaultTimeZone];

    notif.alertBody = TEXT;
    notif.alertAction = LAUNCH_BUTTON;
    notif.soundName = UILocalNotificationDefaultSoundName;
    notif.applicationIconBadgeNumber = NOTIFICATIONS_REMAINING;

    NSDictionary *userDict = [NSDictionary dictionaryWithObject:CUSTOM_INFO 
                                            forKey:kRemindMeNotificationDataKey];
    notif.userInfo = userDict;

    [[UIApplication sharedApplication] scheduleLocalNotification:notif];
    [notif release];
}

Handling the nofitication when the application is not running (in the applicaction delegate):

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

    Class cls = NSClassFromString(@"UILocalNotification");
    if (cls) {
        UILocalNotification *notification = [launchOptions objectForKey:
                        UIApplicationLaunchOptionsLocalNotificationKey];

        if (notification) {
            NSString *reminderText = [notification.userInfo 
                        objectForKey:kRemindMeNotificationDataKey];
           [viewController showReminder:reminderText];
        }
    }

    application.applicationIconBadgeNumber = NOTIFICATIONS_REMAINING_LESS_ONE;

    [window addSubview:viewController.view];
    [window makeKeyAndVisible];

    return YES;
}

Application in the foreground (in the applicaction delegate):

- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {

    UIApplicationState state = [application applicationState];
    if (state == UIApplicationStateInactive) {
        // Application was in the background when notification
        // was delivered.
    }
}
Postcard answered 19/1, 2012 at 1:8 Comment(0)
G
4

Are you sure a timer is what you want? Remember, a timer is only active when your app is active. It does not work when the app is inactive. If you're trying to get your app to do something at any time in the distant future, a timer will not really solve your problem because every time you quit the app, all its timers die.

Assuming that (1) you do just want to set timers for events while the app is active and (2) you always have an unknown number of events in the queue, then your biggest problem will be sorting out how to target an arbitrary number of timers for an arbitrary number of events.

Fortunately, timers can pass any arbitrary object in their userInfo property so you want to wrap every event in an object and pass that object to the timer. Then the timer fired method can extract the event and act on it.

Something like this:

// assume a data model with event objects with the attributes and methods shown

- (void) setTimerForEvent:(EventClass *) anEvent{
    [NSTimer timerWithTimeInterval:[anEvent eventTimeFromNow] 
                            target:self 
                          selector:@selector(fireEvent:) 
                          userInfo:anEvent 
                           repeats:NO];

}//------------------------------------setTimerForEvent:------------------------------------

- (void)fireEvent:(NSTimer*)theTimer{
    [self handleEvent:(EventClass *)theTimer.userInfo]; 
    [theTimer invalidate];
}//------------------------------------fireEvent:------------------------------------

The -[EventClass eventTimeFromNow] should return a NSTimerInterval of the seconds remaining between the time the method is called and time of the event.

Goltz answered 16/4, 2010 at 17:44 Comment(2)
Technically this is for a dylib that is constantly running in the background (non-AppStore) and I need the timer to grab each date in my arrays and set a timer (or whatever) for every date to fire on the date.Supporting
This approach will work for what you want. There is not time limit on on timers. In principle, you can start a timer to fire years down the line if the system keeps up that long. You just need to wrap the dates in a class so they can be passed by the timer. You could pass the raw dates but then you might know what to do with them. A more descriptive Event type class would be better.Goltz
P
3

You can use UILocalNotification instead of NSTimer. The limitations are that there is no actions in your application without your interaction, but there is triggered a notification even if the app is shutted down.

Apple documentation about UILocalNotifications:

Instances of UILocalNotification represent notifications that an application can schedule for presentation to its users at specific dates and times. The operating system is responsible for delivering the notification at the proper time; the application does not have to be running for this to happen.

...

When the system delivers a local notification, several things can happen, depending on the application state and the type of notification. If the application is not frontmost and visible, the system displays the alert message, badges the application, and plays a sound—whatever is specified in the notification. If the notification is an alert and the user taps the action button (or, if the device is locked, drags open the action slider), the application is launched. In the application:didFinishLaunchingWithOptions: method the application delegate can obtain the UILocalNotification object from the passed-in options dictionary by using the UIApplicationLaunchOptionsLocalNotificationKey key. The delegate can inspect the properties of the notification and, if the notification includes custom data in its userInfo dictionary, it can access that data and process it accordingly. On the other hand, if the local notification only badges the application icon, and the user in response launches the application, the application:didFinishLaunchingWithOptions: method is invoked, but no UILocalNotification object is included in the options dictionary.

If the application is foremost and visible when the system delivers the notification, no alert is shown, no icon is badged, and no sound is played. However, the application:didReceiveLocalNotification: is called if the application delegate implements it. The UILocalNotification instance is passed into this method, and the delegate can check its properties or access any custom data from the userInfo dictionary.

You can found a good tutorial here where the main idea is like this:

The notification setup:

Class cls = NSClassFromString(@"UILocalNotification");
if (cls != nil) {
    UILocalNotification *notif = [[cls alloc] init];
    notif.fireDate = [datePicker date];
    notif.timeZone = [NSTimeZone defaultTimeZone];

    notif.alertBody = TEXT;
    notif.alertAction = LAUNCH_BUTTON;
    notif.soundName = UILocalNotificationDefaultSoundName;
    notif.applicationIconBadgeNumber = NOTIFICATIONS_REMAINING;

    NSDictionary *userDict = [NSDictionary dictionaryWithObject:CUSTOM_INFO 
                                            forKey:kRemindMeNotificationDataKey];
    notif.userInfo = userDict;

    [[UIApplication sharedApplication] scheduleLocalNotification:notif];
    [notif release];
}

Handling the nofitication when the application is not running (in the applicaction delegate):

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

    Class cls = NSClassFromString(@"UILocalNotification");
    if (cls) {
        UILocalNotification *notification = [launchOptions objectForKey:
                        UIApplicationLaunchOptionsLocalNotificationKey];

        if (notification) {
            NSString *reminderText = [notification.userInfo 
                        objectForKey:kRemindMeNotificationDataKey];
           [viewController showReminder:reminderText];
        }
    }

    application.applicationIconBadgeNumber = NOTIFICATIONS_REMAINING_LESS_ONE;

    [window addSubview:viewController.view];
    [window makeKeyAndVisible];

    return YES;
}

Application in the foreground (in the applicaction delegate):

- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {

    UIApplicationState state = [application applicationState];
    if (state == UIApplicationStateInactive) {
        // Application was in the background when notification
        // was delivered.
    }
}
Postcard answered 19/1, 2012 at 1:8 Comment(0)
U
0

You'll need a timer for each event. Maybe don't assign the timer to the timer variable. You can just create these timers and release them in the theFireEvent. You'll need to change that selector to @selector(theFireEvent) though, and make sure the method has the signature:

- (void)timerFireMethod:(NSTimer*)theTimer

Utica answered 13/4, 2010 at 20:55 Comment(2)
well, what if I have an unknown number of schedules to be queued? how can I create a new timer for each?Supporting
Well your call to rescheduleTimer is within the loop that reads from the plist for profiles (if I'm understanding your code correctly), so it should get called once per profile it reads and create a timer each time. Also, I think you're currently leaking memory, as you're re-allocing new timers into the timer variable, but never releasing them. You want those timers to hang around until they fire, after which you want to release them. So it probably makes sense to put a release in the timer event.Utica

© 2022 - 2024 — McMap. All rights reserved.