[iOS Dev] 建立一個有通知中心 widget 的 app (Notification Center, Today Extension Widget)

2015-12-08

Point.1

首先建立一個 Project
File > New > Project…
接著選擇 iOS > Application > Single View Application

接著是 widget 的部份
File > New > Target…
選擇 iOS > Application Extension > Today Extension
這裡設定的 Product Name 預設會是顯示在 widget title 的文字 (之後還可以更改)
建立完成後 Xcode 會建立一個 new scheme 並問你要不要 Activate
點選 Activate 即可

到這裡已經完成了一個簡單的 widget
執行後可以在通知中心裡看到剛剛建立的內容
以及預設的內容 :
Hello World

Point.2

因為預設的 widget 有使用 storyboard
我比較習慣寫純程式碼
所以這邊將設定改掉
找到剛剛建立的 widget 目錄下 會有一個 info.plist
在 NSExtension 裡面
可以找到一個 NSExtensionMainStoryboard 將這行刪除
接著在裡面增加一筆 NSExtensionPrincipalClass 並設成 TodayViewController (即建立 widget 後預設的 ViewController)

Point.3

widget 跟 主 app 其實可以說是兩個獨立的 app
所以需要有讓他們兩個溝通的工具
即 App Groups

首先點選主 app 的 target
接著點選 Capabilities 頁籤
找到 App Groups 並開啟 (轉成 ON )
接著新增一個 group
這邊先設定成 group.fastUrlSharedDefaults
新增後記得打勾

然後切換到 widget 的 target
並在如上述同樣的位置
打勾 group.fastUrlSharedDefaults

Point.3.1

App Groups 設定完成後 測試看看 先是使用 NSUserDefaults
▼ 在主 app 的 ViewController.m 中加入

    NSString *valueToSave = @"https://www.apple.com";
    NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.fastUrlSharedDefaults"];
    [sharedDefaults setObject:valueToSave forKey:@"url"];
    [sharedDefaults synchronize];

▼ 在 widget 的 TodayViewController.m 加入

    NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.fastUrlSharedDefaults"];
    NSString *fastUrl = [sharedDefaults stringForKey:@"url"];

即可在 widget 中取得主 app 中設定的值

Point.3.2

如果有其他不是純文字的檔案需要溝通
則是需要使用 NSFileManager
▼ 在各別的 ViewController.m 中加入

    // 像是需要使用 SQLite 的檔案 myDB.db
    NSURL *appGroupDirectoryPath = [[NSFileManager defaultManager] 
    containerURLForSecurityApplicationGroupIdentifier:@"group.fastUrlSharedDefaults"];
    NSURL *dataBaseURL = [appGroupDirectoryPath URLByAppendingPathComponent:@"myDB.db"];

   // 或是圖檔也是一樣的情況
    NSURL *containerURL = [[NSFileManager defaultManager] 
    containerURLForSecurityApplicationGroupIdentifier:@"group.fastUrlSharedDefaults"];
    containerURL = [containerURL URLByAppendingPathComponent:@"pic.png"];
    UIImage *contents=[[UIImage alloc]initWithData:[NSData dataWithContentsOfURL:containerURL]];

即可獲得相同的檔案 後續則依照個別需求進行處理

▼▼
這邊有一個小範例 Fast Url 只有一個功能
在主 app 設定一個 url 則在通知中心會顯示這個 url
點擊後會在 safari 中開啟這個 url
原始碼放在 github

一些片段的小技巧

▼ widget 內容四周有不明的(?)空白空間 消除這些空白要在 widget 的 TodayViewController.m 中加入這個 method

-(UIEdgeInsets) widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets {
    return UIEdgeInsetsZero;
}

▼ 設定 widget 的高度, 在 widget 的 TodayViewController.m 中

- (void)viewDidLoad {
    [super viewDidLoad];

    // 寬度是固定的 不會有效果 所以這邊 x 填 0 即可, 高度就看需要多少 
    self.preferredContentSize = CGSizeMake(0, 100);
}

▼ 修改 widget 顯示的名稱
在 info.plist 中找到 Bundle display name
即可修改 widget title 顯示的名稱

▼ widget 中 使用 openURL

    NSURL *url = [NSURL URLWithString:@"https://www.apple.com"];

    // 在 widget 中 無法使用這個方法 (會報錯誤)
    [[UIApplication sharedApplication] openURL:url];
    
    // 而是要使用這個方法
    [self.extensionContext openURL:url completionHandler:^(BOOL success) {
        NSLog(@"fun=%s after completion. success=%d", __func__, success);
    }];
https://developer.apple.com/library/prerelease/ios/documentation/Foundation/Reference/NSExtensionContext_Class/#//apple_ref/occ/instm/NSExtensionContext/openURL:completionHandler:

根據 Apple 的說法 這個方法只能使用在開啟主 app 的功能
如果要開啟其他的 app 則在上架審查時 可能會有額外的檢查 以確保符合規則

主 app 中仍然要建立一個 URL Scheme 才能從 widget 回去主 app
建立方式如下網址
http://stackoverflow.com/questions/8201724/how-to-register-a-custom-app-opening-url-scheme-with-xcode-4

▼ widget 標題要使用多國語系的話 參考下面這篇文章
Localize info.plist 多國語系 InfoPlist.strings

▼ 要佈署上 App store 或是 Ad Hoc 時
記得主 app 跟 widget 要分別建立 Provisioning Profiles
因為要把他們想成是兩個獨立且不同的 app
所以同理 Bundle identifier 也不一樣
但 widget 的必須是主 app 的 id 再接一節 id
e.q.
主 app id : tw.hsin.demo
widget id : tw.hsin.demo.abcde ( abcde 可以自己修改 但前面就要跟主 app 一樣)

ref:
https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/NotificationCenter.html
https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/ExtensionCreation.html
http://code.tutsplus.com/tutorials/ios-8-creating-a-today-widget--cms-22379
http://tapadoo.com/2014/sharing-nsuserdefaults-between-your-app-and-a-today-extension-on-ios-8/
http://onevcat.com/2014/08/notification-today-widget/
http://www.jianshu.com/p/0efd62ee033a
http://stackoverflow.com/questions/25876171/ios-8-create-sqlite-database-for-app-group