适用于 iOS 的每小时自复位闹钟

Posted

技术标签:

【中文标题】适用于 iOS 的每小时自复位闹钟【英文标题】:Hourly self-resetting alarm for iOS 【发布时间】:2016-09-26 00:35:52 【问题描述】:

我对这个问题做了很多研究,并在 Objective-C 中编写了几个替代方案,但似乎没有任何效果。我的问题是:这在 ios 中是否可行?

具体来说,我希望用户能够在每个小时的开头设置“每小时钟声”,收到通知,然后即使用户没有响应,应用程序也会重新启动计时器。我知道我不能使用 NSTimer,因为 Apple 对在后台运行的计时器有限制。如果用户没有回复UILocalNotification,我已经尝试过UILocalNotification 和“看门狗”计时器来重新启动每小时计时器。但是,如果应用程序在后台,我在 appdelegate.m 中导致每小时计时器重置的代码不会执行,直到用户响应 UILocalNotification。当应用程序在后台时,“看门狗”计时器不会触发。

用户还可以将“铃声”设置为在整点后的 15 分钟、30 分钟和 45 分钟响起。

Sciral HabiTimer 等应用程序如何做到这一点?代码sn-ps如下。

AppDelegate.m

    UILocalNotification *localNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
    if (localNotification) 
        // Set icon badge number to zero
        application.applicationIconBadgeNumber = 0;
        HourlyChimeTableViewController *controller = [[HourlyChimeTableViewController alloc]init];
        [controller stopTimer];
        [controller startTimer];
        NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
        NSInteger interval = [storage integerForKey:@"intervalToWatch"];
        [controller setLocalNotificationExpirationTime:interval];
    

    return YES;


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

    UIApplicationState state = [application applicationState];
    if (state == UIApplicationStateActive) 
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Reminder"
                                                        message:notification.alertBody
                                                       delegate:self cancelButtonTitle:@"OK"
                                              otherButtonTitles:nil];
        [alert show];
    
    HourlyChimeTableViewController *controller = [[HourlyChimeTableViewController alloc]init];
    [controller stopTimer];
    [controller startTimer];

    NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
    NSString *received = @"YES";
    [storage setObject:received forKey:@"notificationWasReceived"];
    [storage synchronize];

    NSInteger intervalToWatch = [storage integerForKey:@"intervalToWatch"];
    [controller setLocalNotificationExpirationTime:intervalToWatch];

    // if a timer was started, stop it.
    NSTimer *timer = controller.expirationTimer;
    if(timer)
        [timer invalidate];

    // Set icon badge number to zero
    application.applicationIconBadgeNumber = 0;


- (void)applicationDidEnterBackground:(UIApplication *)application 
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    NSLog(@"application did enter background");
    HourlyChimeTableViewController *controller = [[HourlyChimeTableViewController alloc]init];
    [controller stopTimer];


- (void)applicationWillEnterForeground:(UIApplication *)application 
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.


- (void)applicationDidBecomeActive:(UIApplication *)application 
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    HourlyChimeTableViewController *controller = [[HourlyChimeTableViewController alloc]init];
    [controller startTimer];


- (void)applicationWillTerminate:(UIApplication *)application 
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.


@end

HourlyChimeTableViewController:

-(void)setLocalNotificationExpirationTime:(NSInteger)intervalToWatch
    NSDate *today = [[NSDate alloc] init];
    NSLog(@"Today: %@", today);
    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDateComponents *components= [calendar components:(NSCalendarUnitMinute) | (NSCalendarUnitSecond) fromDate: today];
    NSInteger minute = [components minute];
    NSInteger second = [components second];
    NSDateComponents *offsetComponents = [[NSDateComponents alloc] init];

    switch(intervalToWatch)
        case 0:
            NSInteger difference;
            //minute =13;
           // NSInteger difference = 10;
            [offsetComponents setHour:0];
            [offsetComponents setMinute:0];
            if(minute >= 0 && minute < 15)
                difference = 900 - minute*60 - second;//seconds left to quarter past
                NSLog(@"Minute: %ld Second: %ld", (long)minute, (long)second);
            else
                if(minute >= 15 && minute < 30)
                    difference = 1800 - minute*60 - second;// seconds to half past
                    NSLog(@"Minute: %ld Second: %ld", (long)minute, (long)second);
                else
                    if (minute >= 30 && minute < 45)// seconds to quarter to
                        difference = 2700 - minute*60 - second;
                    else// seconds to the hour
                        difference = 3600 - minute*60 - second;
                        NSLog(@"Minute: %ld Second: %ld", (long)minute, (long)second);
                    
                
            
        //    difference = 60;
            [offsetComponents setSecond:difference];
            NSDate *fireDate = [calendar dateByAddingComponents:offsetComponents
                                                          toDate:today options:0];
            self.expirationInterval = difference + 5;
            UILocalNotification *notification =   [self startLocalNotification:fireDate];
            self.localNotification = notification;

            [self startNotificationTimer:self.expirationInterval];
            break;

-(UILocalNotification *)startLocalNotification:(NSDate *)fireDate
  //  [[UIApplication sharedApplication] cancelAllLocalNotifications];
    UILocalNotification *notification = [[UILocalNotification alloc]init];
    notification.fireDate = fireDate;
    NSLog(@"firedate %@", fireDate);
    notification.alertBody =@"Timer Expired!";
    notification.alertTitle = @"TimeChime Alert";
    notification.timeZone = [NSTimeZone defaultTimeZone];
    notification.soundName = UILocalNotificationDefaultSoundName;

    self.notificationWasReceived = @"NO";
    NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
    [storage setObject:self.notificationWasReceived forKey:@"notificationWasReceived"];
    [storage synchronize];
   // notification.repeatInterval = NSCalendarUnitMinute;

    [[UIApplication sharedApplication]scheduleLocalNotification:notification];
    return notification;



-(void)startNotificationTimer:(NSInteger)seconds
    NSTimer *notificationTimer = [NSTimer scheduledTimerWithTimeInterval:seconds target:self selector:@selector(notificationTimerDidExpire:) userInfo:nil repeats:YES];
    self.expirationTimer = notificationTimer;
    NSLog(@"started notification timer for %ld", (long)seconds);


-(void)notificationTimerDidExpire:(NSTimer *)notification
    NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
    self.notificationWasReceived = [storage objectForKey:@"notificationWasReceived"];
    NSLog(@"notification timer expired and notificationWasReceived =%@", self.notificationWasReceived);
    if(self.localNotification)
    
        [[UIApplication sharedApplication]cancelLocalNotification:self.localNotification];

        NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
        NSInteger intervalToWatch = [storage integerForKey:@"intervalToWatch"];

        // and reschedule the notification.
        [self setLocalNotificationExpirationTime:intervalToWatch];
    

我已经通过安排多个重复通知解决了这个问题。这是代码(还为 iOS8 添加了自定义操作)。请注意,iOS10 的本地通知调度方式完全不同):

    //
//  AppDelegate.m
//  Hourly Chime2
//
//  Created by Nelson Capes on 9/20/16.
//  Copyright © 2016 Nelson Capes. All rights reserved.
//

#import "AppDelegate.h"
#import <AVFoundation/AVFoundation.h>
#import "HourlyChimeTableViewController.h"
@interface AppDelegate ()

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
    // Override point for customization after application launch.

    NSError *sessionError = nil;
    NSError *activationError = nil;
    [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback error:&sessionError];
    [[AVAudioSession sharedInstance] setActive: YES error: &activationError];

    // ask the user to allow local notifications
    if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)])

        [application registerUserNotificationSettings:[UIUserNotificationSettings
                                                       settingsForTypes:UIUserNotificationTypeAlert categories:nil]];
    
    UILocalNotification *localNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
    if (localNotification) 
        // Set icon badge number to zero
        application.applicationIconBadgeNumber = 0;
        HourlyChimeTableViewController *controller = [[HourlyChimeTableViewController alloc]init];
        [controller stopTimer];
        [controller startTimer];
        [[UIApplication sharedApplication]cancelLocalNotification:localNotification];
        NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
        NSInteger intervalToWatch = [storage integerForKey:@"intervalToWatch"];
        NSMutableArray *array = [storage objectForKey:@"timerArray"];

        // BOOL YES means to schedule a set of local notifications; NO means don't schedule.
        [controller setLocalNotificationExpirationTime:intervalToWatch : NO];



        // Set icon badge number to zero
        application.applicationIconBadgeNumber = 0;

    
    // define a notification action
    UIMutableUserNotificationAction *acceptAction = [[UIMutableUserNotificationAction alloc]init];
    acceptAction.identifier =@"ACCEPT_IDENTIFIER";
    acceptAction.title = @"Continue";
    acceptAction.activationMode = UIUserNotificationActivationModeBackground;
    acceptAction.destructive = NO;
    acceptAction.authenticationRequired = NO;

    UIMutableUserNotificationAction *declineAction = [[UIMutableUserNotificationAction alloc]init];
    declineAction.identifier =@"DECLINE_IDENTIFIER";
    declineAction.title = @"Stop";
    declineAction.activationMode = UIUserNotificationActivationModeBackground;
    declineAction.destructive = NO;
    declineAction.authenticationRequired = NO;

    UIMutableUserNotificationCategory *inviteCategory = [[UIMutableUserNotificationCategory alloc]init];
    inviteCategory.identifier = @"INVITE_CATEGORY";
    [inviteCategory setActions:@[acceptAction, declineAction] forContext:UIUserNotificationActionContextDefault];

    NSSet *categories = [NSSet setWithObjects:inviteCategory, nil];
    if([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)])
        UIUserNotificationSettings *settings = [UIUserNotificationSettings
                                                settingsForTypes:UIUserNotificationTypeAlert categories:categories];
        [[UIApplication sharedApplication]registerUserNotificationSettings:settings];
    

    return YES;


// determine whether the user will allow local notifications.
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings;

    if ([[UIApplication sharedApplication] respondsToSelector:@selector(currentUserNotificationSettings)]) // Check it's iOS 8 and above
        NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
        UIUserNotificationSettings *grantedSettings = [[UIApplication sharedApplication] currentUserNotificationSettings];

        if (grantedSettings.types == UIUserNotificationTypeNone) 
            NSLog(@"No permission granted");
            [storage setBool:NO forKey:@"permission granted"];
        
        else if (grantedSettings.types & UIUserNotificationTypeSound & UIUserNotificationTypeAlert )
            NSLog(@"Sound and alert permissions ");
            [storage setBool:YES forKey:@"permission granted"];
            [storage setBool:YES forKey:@"sound permission granted"];
            [storage setBool:YES forKey:@"alert permission granted"];
        
        else if (grantedSettings.types  & UIUserNotificationTypeAlert)
            NSLog(@"Alert Permission Granted");
            [storage setBool:YES forKey:@"permission granted"];
            [storage setBool:YES forKey:@"alert permission granted"];
            [storage setBool:NO forKey:@"sound permission granted"];
        
        [storage synchronize];
    


-(void)application:(UIApplication *)application handleActionWithIdentifier:(nullable NSString *)identifier forLocalNotification:(nonnull UILocalNotification *)notification completionHandler:(nonnull void (^)())completionHandler
    if ([identifier isEqualToString:@"ACCEPT_IDENTIFIER"])
        [self handleAcceptActionWithNotification:notification];
    else
        if ([identifier isEqualToString:@"DECLINE_IDENTIFIER"])
            [self handleDeclineActionWithNotification:notification];
        
    
    completionHandler();

-(void)handleAcceptActionWithNotification:notification
    NSLog(@"accept action received");
    HourlyChimeTableViewController *controller = [[HourlyChimeTableViewController alloc]init];
    [controller stopTimer];
    [controller startTimer];
    [[UIApplication sharedApplication]cancelLocalNotification:notification];
    NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
    NSInteger intervalToWatch = [storage integerForKey:@"intervalToWatch"];
    // BOOL YES means to schedule a set of local notifications; NO means don't schedule.
    [controller setLocalNotificationExpirationTime:intervalToWatch : NO];

-(void)handleDeclineActionWithNotification:notification
    NSLog(@"decline Action received");
    HourlyChimeTableViewController *controller = [[HourlyChimeTableViewController alloc]init];
    [controller stopTimer];


- (void)applicationWillResignActive:(UIApplication *)application 
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.


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

    UIApplicationState state = [application applicationState];
    if (state == UIApplicationStateActive) 
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Reminder"
                                                        message:notification.alertBody
                                                       delegate:self cancelButtonTitle:@"OK"
                                              otherButtonTitles:nil];
        [alert show];
    
  //  [[UIApplication sharedApplication]cancelAllLocalNotifications];
    NSLog(@"app delegate: notification received");
    HourlyChimeTableViewController *controller = [[HourlyChimeTableViewController alloc]init];
    [controller stopTimer];
    [controller startTimer];

    [[UIApplication sharedApplication]cancelLocalNotification:notification];
    NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
    NSInteger intervalToWatch = [storage integerForKey:@"intervalToWatch"];
   // NSMutableArray *array = [storage objectForKey:@"timerArray"];

    // BOOL YES means to schedule a set of local notifications; NO means don't schedule.
    [controller setLocalNotificationExpirationTime:intervalToWatch : NO];



    // Set icon badge number to zero
  //  application.applicationIconBadgeNumber = 0;


- (void)applicationDidEnterBackground:(UIApplication *)application 
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    NSLog(@"application did enter background");
    HourlyChimeTableViewController *controller = [[HourlyChimeTableViewController alloc]init];
   // [controller stopTimer];



- (void)applicationWillEnterForeground:(UIApplication *)application 
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    NSLog(@"application did enter foreground");



- (void)applicationDidBecomeActive:(UIApplication *)application 
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.a
    NSLog(@"app delegate: applicationDidBecomeActive");
    HourlyChimeTableViewController *controller = [[HourlyChimeTableViewController alloc]init];
    [controller startTimer];
    if ([[UIApplication sharedApplication] respondsToSelector:@selector(currentUserNotificationSettings)]) // Check it's iOS 8 and above
        UIUserNotificationSettings *grantedSettings = [[UIApplication sharedApplication] currentUserNotificationSettings];

        if (grantedSettings.types == UIUserNotificationTypeNone) 
            NSLog(@"No permission granted");
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"You have not granted permission for Alerts"
                                                            message:@"We can't let you know when a timer expires"
                                                           delegate:self cancelButtonTitle:@"OK"
                                                  otherButtonTitles:nil];
            [alert show];
        
        else if (grantedSettings.types & UIUserNotificationTypeSound & UIUserNotificationTypeAlert )
            NSLog(@"Sound and alert permissions ");
        
        else if (grantedSettings.types  & UIUserNotificationTypeAlert)
            NSLog(@"Alert Permission Granted");
        else
            NSLog(@"No permissions ");
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"You have not granted permission for Alerts"
                                                        message:@"We can't let you know when a timer expires"
                                                       delegate:self cancelButtonTitle:@"OK"
                                              otherButtonTitles:nil];
        [alert show];
        
    


- (void)applicationWillTerminate:(UIApplication *)application 
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.



@end

以下代码展示了如何安排本地通知:

-(void)setLocalNotificationExpirationTime:(NSInteger)intervalToWatch :(BOOL)option

    // BOOL YES means to schedule a set of local notifications; NO means don't schedule.

    NSDate *today = [[NSDate alloc] init];
    NSLog(@"Today: %@", today);
    NSCalendar *calendar = [NSCalendar currentCalendar];
    NSDateComponents *components= [calendar components:(NSCalendarUnitMinute) | (NSCalendarUnitSecond) fromDate: today];
    NSInteger minute = [components minute];
    NSInteger second = [components second];
    NSDateComponents *offsetComponents = [[NSDateComponents alloc] init];

    // intervalToWatch is an NSInteger set by the UI to determine what kind of notification to set.
    // 1 = every 15 minutes; 2 = every half hour; 3 = every hour.
    // we calculate the seconds left until the next interval, then schedule a local notification with the seconds left.
    switch(intervalToWatch)
        case 0:// notification every 15 minutes
            NSInteger difference;
            [offsetComponents setHour:0];
            [offsetComponents setMinute:0];
            if(minute >= 0 && minute < 15)
                difference = 900 - minute*60 - second;//seconds left to quarter past
                // NSLog(@"Minute: %ld Second: %ld", (long)minute, (long)second);
            else
                if(minute >= 15 && minute < 30)
                    difference = 1800 - minute*60 - second;// seconds to half past
                    // NSLog(@"Minute: %ld Second: %ld", (long)minute, (long)second);
                else
                    if (minute >= 30 && minute < 45)// seconds to quarter to
                        difference = 2700 - minute*60 - second;
                    else// seconds to the hour
                        difference = 3600 - minute*60 - second;
                        //   NSLog(@"Minute: %ld Second: %ld", (long)minute, (long)second);
                    
                
            
                // schedule repeating local notifications every 15 minutes.
                if(option == YES)
                    [offsetComponents setSecond:difference];

                    NSDate *fireDate = [calendar dateByAddingComponents:offsetComponents
                                                                 toDate:today options:0];

                    UILocalNotification *notification =   [self startLocalNotification:fireDate];
                    self.localNotification = notification;// at the next 15 minute interval.


                    [offsetComponents setSecond:difference + 900];
                    fireDate = [calendar dateByAddingComponents:offsetComponents
                                                         toDate:today options:0];

                    UILocalNotification *notification1 = [self startLocalNotification:fireDate];// at next 30 minutes
                    [self.timerArray addObject:notification1];

                    [offsetComponents setSecond:difference + 1800];
                    fireDate = [calendar dateByAddingComponents:offsetComponents
                                                         toDate:today options:0];

                    UILocalNotification *notification2 = [self startLocalNotification:fireDate];// at next 45 minutes
                    [self.timerArray addObject:notification2];

                    [offsetComponents setSecond:difference + 2700];
                    fireDate = [calendar dateByAddingComponents:offsetComponents
                                                         toDate:today options:0];

                    UILocalNotification *notification3 = [self startLocalNotification:fireDate];// at next hour
                    [self.timerArray addObject:notification3];

                
            
            break;

        case 1:// notification every 30 minutes at half past the hour.
            NSInteger difference;
            [offsetComponents setHour:0];
            [offsetComponents setMinute:0];
            if(minute >= 0 && minute < 30)
                difference = 1800 - minute*60 - second;// seconds to half past
            else
                difference = 3600 - minute*60 - second;// seconds to the hour
            
            if(option == YES)
                [offsetComponents setSecond:difference];

                NSDate *fireDate = [calendar dateByAddingComponents:offsetComponents
                                                             toDate:today options:0];

                UILocalNotification *notification =   [self startLocalNotification:fireDate];
                self.localNotification = notification;// at the next 30 minute interval.


                [offsetComponents setSecond:difference + 1800];
                fireDate = [calendar dateByAddingComponents:offsetComponents
                                                     toDate:today options:0];
                UILocalNotification *notification2 = [self startLocalNotification:fireDate];// at next 45 minutes
                [self.timerArray addObject:notification2];
            
            break;
        
        case 2:// notification every hour on the hour
            minute = 58;
            NSInteger difference = 3600 - minute*60 - second;
            if(option == YES)
                [offsetComponents setSecond:difference];

                NSDate *fireDate = [calendar dateByAddingComponents:offsetComponents
                                                             toDate:today options:0];

                UILocalNotification *notification =   [self startLocalNotification:fireDate];
                self.localNotification = notification;// at the next 60 minute interval.
            
            break;
        

        


    

-(UILocalNotification *)startLocalNotification:(NSDate *)fireDate

  //  [[UIApplication sharedApplication] cancelAllLocalNotifications];

    UILocalNotification *notification = [[UILocalNotification alloc]init];
    NSUserDefaults *storage = [NSUserDefaults standardUserDefaults];
    notification.fireDate = fireDate;
    NSLog(@"firedate %@", fireDate);
   notification.alertBody =@"Timer Expired!";
   notification.alertTitle = @"TimeChime Alert";
    notification.timeZone = [NSTimeZone defaultTimeZone];
    BOOL sound = [storage boolForKey:@"sound permission granted"];
    if(sound)
        notification.soundName = UILocalNotificationDefaultSoundName;
    notification.repeatInterval = NSCalendarUnitHour;
    notification.category = @"INVITE_CATEGORY";

    [[UIApplication sharedApplication]scheduleLocalNotification:notification];

    return notification;


【问题讨论】:

计算您现在的第一次触发时间并指定repeatIntervalNSCalendarUnitHour 你试过在日历中设置一些隐藏事件吗? 【参考方案1】:

经过大量测试,我对自己问题的回答是“不可能”。一旦应用程序开始等待本地通知,它就会被阻止。除了编写一些多线程代码(我从未做过)之外,没有办法做任何事情,只能让用户响应(即,当应用程序被阻止时,没有看门狗定时器会关闭,例如,当它在后台)。

不过,我确实找到了解决问题的方法,我编辑了这篇文章以反映这一点。

【讨论】:

注意:我编辑了我的原始帖子以显示解决方案。

以上是关于适用于 iOS 的每小时自复位闹钟的主要内容,如果未能解决你的问题,请参考以下文章

STM32中怎样让自己指定部分程序不受系统复位的影响?

单片机复位后高电平输出问题(收集别人的)

自锁开关信号如何转换为1s内的自复位开关信号

NO---22 H5在ios端微信浏览器中,input事件触发后页面不复位的问题

计算机复位后CPU的第一指令执行地址

时钟与复位