読者です 読者をやめる 読者になる 読者になる

Local Notification(通知センター)で自分用ユーティリティアプリ

2012.3.30追記

先日iOS5.1にアップデートしたら、通知センターから各項目へジャンプできなくなってしまいました。
「通知以外の用途に使ってはいけない」という規約ができたり、同等機能のものがリジェクトされたりという話は知っていたのですが、まさかこういう形で禁止されるとは。
というわけで、下記の方法は最新OSには対応していないのですが、一応残しておきます。↓

----------

つくるもの

通知センターによく使うアプリを登録・起動するユーティリティアプリ
「通知センターからWiFi設定とかにさくっと飛べたら便利だよね!」という思いつきから。

完成図

オンオフを切り替えて

登録

起動

操作の流れ

  1. App Listから動作を追加したいアプリ名を選択
  2. Action Listから動作のオンオフを切り替え
  3. Registerボタンを押して通知センターに登録

使用感

なかなか。さっとメニューを下ろしてアプリを切り替えることができるので、けっこう重宝しています。
通知センターのリスト並び替えで一番上にしておくと、なおよし。

問題点

  • リストを作る作業をどう自動化させるか

今は自分の使いそうなアプリだけピックアップして手作業で作ってますが、ここをなんとかクリアしたらストアに出せますよね。

  • 通知をセットしたときにこのアプリが起動していると、強制的にセットしたアプリに飛ばされる

飛ぶ→戻るを数回繰り返すことになっちゃいます。仕様と割り切れる気もしますが、なんとかできるといいなーと。

開発の流れ

  • 用意するもの

App List、Action List用のplist(xml)

  • topic
    • plistを書く
    • plistを読み込む→Tableに表示
    • AppList/ActionListの表示
    • 通知センター登録・起動
  • plistを書く

App/Actionのリストは、後で変更しやすいように外部化してます。
URLスキームは下記で検索しつつ。
URL Schemes | handleOpenURL, Shared Interapp Communication on iOS

appList.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">

<array>
  <string>TweetBot</string>
  <string>settings</string>
  <string>mail</string>
</array>

</plist>

schemes.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">

<array>
    <dict>
    <key>app_name</key>
    <string>Tweetbot</string>
    <key>url_scheme</key>
    <string>tweetbot://</string>
    <key>category</key>
    <string>TweetBot</string>
  </dict>
  <dict>
    <key>app_name</key>
    <string>Tweetbot_post</string>
    <key>url_scheme</key>
    <string>tweetbot:///post?</string>
    <key>category</key>
    <string>TweetBot</string>
  </dict>
  <dict>
    <key>app_name</key>
    <string>settings_wifi</string>
    <key>url_scheme</key>
    <string>prefs:root=WIFI</string>
    <key>category</key>
    <string>settings</string>
  </dict>
  <dict>
    <key>app_name</key>
    <string>settings_sounds</string>
    <key>url_scheme</key>
    <string>prefs:root=Sounds</string>
    <key>category</key>
    <string>settings</string>
  </dict>
  <dict>
    <key>app_name</key>
    <string>e-mail</string>
    <key>url_scheme</key>
    <string>mailto:</string>
    <key>category</key>
    <string>mail</string>
  </dict>
  <dict>
    <key>app_name</key>
    <string>sms</string>
    <key>url_scheme</key>
    <string>sms:</string>
    <key>category</key>
    <string>mail</string>
  </dict>
</array>
</plist>

こんな感じで。

  • plistを読み込む→Tableに表示

初回起動時に一括で読み込んでNSUserDefaultに保存、Table生成時にそこから値を取り出して表示させるようにしてみました。

AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    
    // 初回起動判定
    if([ud stringForKey:@"init"] == NULL) {
        isFirstLaunch = YES;
        [ud setValue:@"YES" forKey:@"init"];
        
        [self udInit];
    }
    
    return YES;
}

- (void)udInit {
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    
    //plist読み込み
    NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"schemes" ofType:@"plist"];
    NSArray *schemes = [[NSMutableArray alloc] initWithContentsOfFile:path];
    
    NSString *listPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"appList" ofType:@"plist"];
    NSArray *lists = [[NSMutableArray alloc] initWithContentsOfFile:listPath];
    
    NSMutableArray *categories = [NSMutableArray array];
    NSMutableArray *apps = [NSMutableArray array];
    NSMutableArray *urls = [NSMutableArray array];
    NSMutableArray *enables = [NSMutableArray array];
    
    // plistの中身を配列に追加
    for(NSDictionary *dic in schemes) {
        [urls addObject:[dic objectForKey:@"url_scheme"]];
        [apps addObject:[dic objectForKey:@"app_name"]];
        [enables addObject:@"NO"];
        
        [categories addObject:[dic objectForKey:@"category"]];
    }
    
    //userdefaultに配列をセット
    [ud setValue:urls forKey:@"url_scheme"];
    [ud setValue:apps forKey:@"app_name"];
    [ud setValue:categories forKey:@"category"];
    [ud setValue:enables forKey:@"enable"];
    
    [ud setValue:lists forKey:@"app_list"];
}
  • AppList/ActionListの表示

素数と名前をUserDefaultsから取り出して返すようにします。

AppList.m

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    
    // appListの要素数を返す
    return [[ud objectForKey:@"app_list"] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }
    
    // セルの番目に対応したApp名を返す
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    NSArray *appList = [ud objectForKey:@"app_list"];
    
    cell.textLabel.text = [appList objectAtIndex:indexPath.row];
    
    return cell;
}

ActionListは選ばれたカテゴリ名の要素数と機能のオンオフをセットしておく必要があるので、ViewDidLoad内でUserDefaultsから取り出して配列に入れてます。
cellのイニシャライズ時にUISwitchをaddして、スライダが切り替わったときにchangedSwitchValueメソッドを呼ぶように設定。
AppListに戻る(viewWillDisappear)際に、変更された値をUserDefaultsに戻します。

ActionList.m

NSMutableArray *hitItems;
NSMutableArray *switchStatus;
NSMutableArray *appearRows;

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    hitItems = [NSMutableArray array];
    switchStatus = [NSMutableArray array];
    appearRows = [NSMutableArray array];
    
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    NSArray *categories = [ud objectForKey:@"category"];
    NSArray *enables = [ud objectForKey:@"enable"];
    
    for (int i=0; i<[categories count]; i++) {
        if([[categories objectAtIndex:i] isEqualToString:[ud objectForKey:@"selectedItem"]]) {
            [hitItems addObject:[NSString stringWithFormat:@"%d", i]];
            [switchStatus addObject:[enables objectAtIndex:i]];
            [appearRows addObject:[NSString stringWithFormat:@"%d", i]];
        }
    }
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    
    // statusをuserDefaultsにセット
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    NSMutableArray *enables = [ud objectForKey:@"enable"];
    NSMutableArray *ens = [NSMutableArray arrayWithArray:enables];
    
    for(int i=0; i<[appearRows count]; i++) { 
        [ens replaceObjectAtIndex:[[appearRows objectAtIndex:i] intValue] withObject:[switchStatus objectAtIndex:i]];
    }
    
    [ud setValue:ens forKey:@"enable"];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{

    return [hitItems count];
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    return [ud valueForKey:@"selectedItem"];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }
    
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    
    cell.textLabel.text = [[ud objectForKey:@"app_name"] objectAtIndex: [[hitItems objectAtIndex:indexPath.row] intValue]];
    cell.accessoryType = UITableViewCellAccessoryNone;
    
    UISwitch *sButton = [[UISwitch alloc] initWithFrame:CGRectMake(235, 8, 79, 27)];
    [sButton setTag:indexPath.row];
    [sButton addTarget:self action:@selector(changedSwitchValue:) forControlEvents:UIControlEventValueChanged];
    
    [sButton setOn:[[switchStatus objectAtIndex:indexPath.row] boolValue]];
    
    [cell addSubview:sButton];
    
    return cell;
}

- (void)changedSwitchValue:(UISwitch*)sw {
    
    if(sw.on){
        [switchStatus replaceObjectAtIndex:sw.tag withObject:@"YES"];
    }
    else {
        [switchStatus replaceObjectAtIndex:sw.tag withObject:@"NO"];
    }
}
  • 通知センター登録・起動

AppListにあるRegisterボタンをトリガにして、switchがオンになっているものを5秒後に通知センターに登録します。
UILocalNotificationのuserInfoプロパティは、NSDictionaryの形で好きな値を保存しておくことができます。
これを利用して、通知センターに表示される内容と呼ぶスキームを紐付け。
通知センターからその項目をタップするとAppDelegateのdidReceiveLocalNotificationが呼ばれて、目的のアプリに飛びます。
すぐ登録してしまうと強制的に登録したアプリに飛ばされてしまうので(問題点2)
5秒後に設定→ホームボタンを押してもらうアラートを出してます。


AppList.m

- (IBAction)pushRegister:(id)sender {
    
    // はじめに通知をクリア
    [[UIApplication sharedApplication] cancelAllLocalNotifications];
    
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    
    NSMutableArray *enables = [ud valueForKey:@"enable"];
    NSMutableArray *aps = [ud valueForKey:@"app_name"];
    NSMutableArray *scs = [ud valueForKey:@"url_scheme"];
    
    NSMutableArray *apps = [NSMutableArray arrayWithArray:aps];
    NSMutableArray *schemes = [NSMutableArray arrayWithArray:scs];
    
    for(int i=0; i<[enables count]; i++) {
        if([[enables objectAtIndex:i] boolValue]) {
            UILocalNotification *localPush = [[UILocalNotification alloc] init];
            
            // time zone
            localPush.timeZone = [NSTimeZone defaultTimeZone];
            
            // timing
            localPush.fireDate = [NSDate dateWithTimeIntervalSinceNow:5];
            
            // message
            localPush.alertBody = [apps objectAtIndex:i];
            
            // info
            localPush.userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
                                  [schemes objectAtIndex:i], @"scheme", 
                                  nil];
            
            // badge
            localPush.applicationIconBadgeNumber = 0;
            
            // register
            [[UIApplication sharedApplication] scheduleLocalNotification:localPush];
        }
    }
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"done" message:@"登録しました。ホームボタンを押してください" delegate:self cancelButtonTitle:nil otherButtonTitles:@"ok", nil];
    [alert show];
}

AppDelegate.m

- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
    // アプリケーションに飛ばす処理
    if(notification) {
        NSString *itemName = [notification.userInfo objectForKey:@"scheme"];
        
        [UIApplication sharedApplication].applicationIconBadgeNumber = notification.applicationIconBadgeNumber-1;
        //NSLog(@"%@", itemName);
        
        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:itemName]];
    }   
}

コードの核はこんなところ。難しいことしてないです。
UIのことをほとんど考えずに、こういうものがぱっと作れてしまうのは楽しい。
storyboardの登場のお陰で「とりあえず作ってみる」が格段にやりやすくなった印象があります。