iOS:應用程式擴充套件開發之Today擴充套件(Today Extesnsion)
一、簡介
iOS應用程式擴充套件是蘋果在iOS8推出的一個新特性,可以將自定義的功能和內容擴充套件到應用程式之外,在之後又經過不斷地優化和更新,已經成為開發中不可或缺的功能之一。擴充套件也是一個Target專案,它執行在主機應用程式上,可以與主機應用程式實現資源共享,和宿主應用程式的Target專案是彼此獨立的。系統提供的擴充套件有很多,Toady擴充套件就是其中之一,也被成為應用程式外掛,它的作用是將今日發生的簡單訊息展示在系統的外掛介面上。Toady擴充套件模板名稱為Today Extension。圖1是建立Today擴充套件,圖2是擴充套件顯示在外掛介面上(可以通過點選Edit來新增或者移除擴充套件)。
二、建立
按照上圖1的方式建立一個Today Extension的Target後,系統會預設幫我們生成一個TodayViewController控制器類、MainInterface.storyBoard故事板、plist序列化檔案,檔案結構圖如下:
上圖中紅色圈內和箭頭指向的配置就是系統通過MainInterface.storyBoard幫我們實現了一個基本的Toady外掛UI佈局,執行後可以直接顯示在外掛介面上。可是,有的時候開發者並不想使用系統的故事板來構建UI,系統支援自定義的,我們只需要修改plist配置即可。具體的配置是這樣的:
[1] 將NSExtensionMainStoryboard欄位刪除;
[2] 新增NSExtensionPrincipalClass欄位,修改value為控制器的類名。
[3] 在TodayViewController中的ViewDidLoad中設定preferredContentSize屬性大小,用來調整widget介面UI的尺寸。
配置如下圖所示:
//設定尺寸 self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 300);
三、分析
TodayViewController類比較簡單,就是一個VC類,它實現了系統提供的一個擴充套件協議<NCWidgetProviding>,可以在協議方法中實現對擴充套件的更新和狀態監控。
協議如下,都是可選的,開發者根據需要進行重寫。
//協議 @protocol NCWidgetProviding <NSObject> @optional //當資料更新時呼叫的方法,系統會定期更新擴充套件 - (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult result))completionHandler; //監聽顯示模式(寬鬆型、緊奏型)和尺寸的改變,其中寬鬆和緊湊表示的是展開和摺疊狀態, iOS10開始才能使用 - (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize __API_AVAILABLE(ios(10.0)); //設定擴充套件UI邊距,注意:使用StoryBoard時,若要所見即所得,則這個方法中需要返回UIEdgeInsetsZero; (iOS10 and later 不會再被呼叫,棄用了) - (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets __API_DEPRECATED("This method will not be called on widgets linked against iOS versions 10.0 and later.", ios(8.0, 10.0)); @end
//擴充套件,都是iOS10開始才能使用 @interface NSExtensionContext (NCWidgetAdditions) //設定widget摺疊或展開狀態 @property (nonatomic, assign) NCWidgetDisplayMode widgetLargestAvailableDisplayMode __API_AVAILABLE(ios(10.0)); //只讀,widget狀態 @property (nonatomic, assign, readonly) NCWidgetDisplayMode widgetActiveDisplayMode __API_AVAILABLE(ios(10.0)); //獲取widget不同狀態的尺寸 - (CGSize)widgetMaximumSizeForDisplayMode:(NCWidgetDisplayMode)displayMode __API_AVAILABLE(ios(10.0)); @end
四、互動
Today擴充套件是寄宿於主機應用程式上的, TodayViewController又是一個UIViewController類,系統支援Today擴充套件對UIViewController進行切換。也就是說,蘋果在考慮提供給開發者在對UIViewController中新增各種展示控制元件這種便利的同時,也相應的提供給開發者通過Today擴充套件的widget從主機應用程式啟用並開啟宿主應用程式的機會。不過這個操作必須通過設定並調起scheme來實現。步驟如下:
[1] 配置宿主應用程式的scheme;
[2] 使用擴充套件的openURL開啟宿主應用程式。
互動如下:
//擴充套件通過scheme開啟主宿主應用程式 [self.extensionContext openURL:[NSURL URLWithString:@"MainApp://"] completionHandler:nil];
五、資料
既然Today擴充套件能與宿主應用程式進行互動,那麼肯定就存在資料通訊的問題了。擴充套件與宿主目錄應用程式位於不同的目錄結構中,預設情況下,擴充套件與宿主應用程式的資料並不共享,程式碼也不能複用。例如在宿主目錄應用程式中可能有網路請求、資料持久化儲存等結構框架,在擴充套件中不可以直接使用,擴充套件需要提供自己的網路請求框架、資料持久化框架等。這些問題蘋果都提供瞭解決方法,可以通過建立靜態庫的方式實現程式碼共享,通過APP Group和Scheme跳轉實現資料共享。這裡主要講一下資料共享。注意:擴充套件和宿主應用程式的素材檔案也是互相獨立的,必須將擴充套件中的素材新增到擴充套件Target。
方式一:通過配置scheme跳轉來實現資料共享。可以將傳遞的資料配置到URL中,然後宿主應用程式通過AppDeleagte的代理方法application:openURL:options:獲取資料,不過這個資料傳遞只能是單方向的。
//開啟主應用程式 -(void)openMainApp { //共享資料 NSString *schemeFormat = @"MainApp://action=openCarema?name=xiayuanquan"; [self.extensionContext openURL:[NSURL URLWithString:schemeFormat] completionHandler:nil]; }
-(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options { //從URL獲取共享資料,擷取資料 NSLog(@"---------url = %@---------",url); return YES; }
方式二:給擴充套件的Target和宿主應用程式的Target專案都開啟APP Group,兩者配置相同的appgroupIndentifier標識,分別生成字尾名為.entitlements檔案。然後對於小資料推薦使用偏好進行雙向傳遞共享資料,如圖所示。
//共享資料 //使用偏好設定 NSUserDefaults *defalut = [[NSUserDefaults alloc] initWithSuiteName:@"group.xiayuanquan"]; [defalut setObject:@"xiayuanquan" forKey:@"name"];
//從偏好設定獲取共享資料 NSUserDefaults *defalut = [[NSUserDefaults alloc] initWithSuiteName:@"group.xiayuanquan"]; NSString *name1 = [defalut objectForKey:@"name"]; NSLog(@"1------------name1=%@",name1);
方式三:配置跟方式二一樣,不過雙向傳遞共享資料使用檔案目錄來實現。
//共享資料 //方式二:使用共享目錄 NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *baseURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.xiayuanquan"]; NSURL *filePath = [baseURL URLByAppendingPathComponent:@"file"]; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:@"xiayuanquan" requiringSecureCoding:NO error:nil]; [data writeToURL:filePath atomically:YES];
//從共享目錄獲取共享資料 NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *baseURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.xiayuanquan"]; NSURL *filePath = [baseURL URLByAppendingPathComponent:@"file"]; NSData *data = [NSData dataWithContentsOfURL:filePath]; NSLog(@"2------------data=%@",data);
六、適配
從iOS10開始,蘋果提供了NCWidgetDisplayMode展示模式,通過設定該模式來支援對widget進行摺疊和展開。在這裡,preferredContentSize就用到了。這個是用來設定widget的尺寸的。蘋果對widget的尺寸有自己的標準,width為maxSize.width,height取值範圍[110, maxSize.height]。這個maxSize可以在擴充套件協議<NCWidgetProviding>的協議方法也即widgetActiveDisplayModeDidChange:withMaximumSize中獲取:,可以發現每一種機型maxSize不一樣。
// 6s模擬器下: // NCWidgetDisplayModeCompact模式下:{359.000000, 110.000000} // NCWidgetDisplayModeExpanded模式下:{359.000000, 528.000000} // 8 plus模擬器下: // NCWidgetDisplayModeCompact模式下:{304.000000, 110.000000} // NCWidgetDisplayModeExpanded模式下:{304.000000, 616.000000}
摺疊狀態:widget的高為110,此時設定preferredContentSize無效;
展開狀態:widget的高為開發者設定的preferredContentSize.height,但是如果preferredContentSize.height>maxSize.height,此時取值為maxSize.height。
適配iOS10,預設支援展開,設定如下:
//設定widget預設為可以展開,此時處於摺疊狀態 #ifdef __IPHONE_10_0 //適配iOS10 self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded; #endif
七、範例
【去掉MainInterface.storyBoard,採用純程式碼實現】
1、宿主應用程式AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. //儲存共享資料 //方式二:使用偏好設定 NSUserDefaults *defalut = [[NSUserDefaults alloc] initWithSuiteName:@"group.xiayuanquan"]; [defalut setObject:@"xiayuanquan" forKey:@"name"]; //儲存共享資料 //方式三:使用共享目錄 NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *baseURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.xiayuanquan"]; NSURL *filePath = [baseURL URLByAppendingPathComponent:@"file"]; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:@"xiayuanquan" requiringSecureCoding:NO error:nil]; [data writeToURL:filePath atomically:YES]; return YES; } -(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options { //方式一:從URL獲取共享資料,例如引數 NSLog(@"---------url = %@---------",url); return YES; }
2、Widget擴充套件TodayViewController
// // TodayViewController.m // TodayExtension // Created by 夏遠全 on 2019/11/19. // #import "TodayViewController.h" #import <NotificationCenter/NotificationCenter.h> @interface TodayViewController () <NCWidgetProviding> @end @implementation TodayViewController - (void)viewDidLoad { [super viewDidLoad]; [self config]; [self createUI]; [self fecthData]; } //配置 -(void)config { self.view.backgroundColor = [UIColor lightGrayColor]; //widget背景色為灰色 self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 300); //widget尺寸大小, 寬度實際取maxSize,width,高度[110, maxSize.height] //設定widget預設為可以展開,此時處於摺疊狀態 #ifdef __IPHONE_10_0 //適配iOS10 self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded; #endif } //建立UI -(void)createUI { CGFloat width = self.view.frame.size.width; CGFloat btnWidth = 100; UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake((width-btnWidth)/2, 0, btnWidth, 40)]; button.backgroundColor = [UIColor greenColor]; [button setTitle:@"OpenAPP" forState:UIControlStateNormal]; [button setTitleColor:[UIColor redColor] forState:UIControlStateNormal]; [button addTarget:self action:@selector(openMainApp) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; } //開啟主應用程式 -(void)openMainApp { //傳遞共享資料 //方式一:引數傳遞 NSString *schemeFormat = @"MainApp://action=openCarema?name=xiayuanquan"; [self.extensionContext openURL:[NSURL URLWithString:schemeFormat] completionHandler:nil]; } //獲取共享資料 -(void)fecthData { //方式二:從偏好設定獲取共享資料 NSUserDefaults *defalut = [[NSUserDefaults alloc] initWithSuiteName:@"group.xiayuanquan"]; NSString *name1 = [defalut objectForKey:@"name"]; NSLog(@"1------------name1=%@",name1); //方式三:從共享目錄獲取共享資料 NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *baseURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.xiayuanquan"]; NSURL *filePath = [baseURL URLByAppendingPathComponent:@"file"]; NSData *data = [NSData dataWithContentsOfURL:filePath]; NSLog(@"2------------data=%@",data); } //當資料更新時呼叫的方法,系統會定期更新擴充套件 - (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler { //獲取共享的資料,根據判斷回撥對應的block //NCUpdateResultNewData, //NCUpdateResultNoData, //NCUpdateResultFailed completionHandler(NCUpdateResultNoData); } //監聽顯示模式(寬鬆型、緊奏型)和尺寸的改變 //NCWidgetDisplayModeCompact : 摺疊 //NCWidgetDisplayModeExpanded : 展開 - (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize { //maxSize: //雖說是最大的Size,但蘋果還是把Widget的高度範圍限制在了[110 ~ maxSize]之間 //如果設定高度小於110,那麼default = 110; //如果設定高度大於開發者設定的preferredContentSize.Heiget,那麼default = maxSize; //摺疊狀態下,蘋果將高度固定為110,這個時候設定preferredContentSize屬性無效。 NSLog(@"width = %lf-------height = %lf",maxSize.width,maxSize.height); //可以更改狀態 if (activeDisplayMode == NCWidgetDisplayModeExpanded) { self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 300); } else{ self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 100); } } //設定擴充套件UI邊距,注意:使用StoryBoard時,若要所見即所得,則這個方法中需要返回UIEdgeInsetsZero; (iOS10 and later 不會再被呼叫) //- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets { // return UIEdgeInsetsZero; //} @end
3、列印和gif
2019-11-20 16:22:31.074596+0800 TodayExtension[29668:1132736] 1------------name1=xiayuanquan 2019-11-20 16:22:31.234435+0800 TodayExtension[29668:1132736] 2------------data={length = 149, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00000068 } 2019-11-20 16:22:31.234970+0800 TodayExtension[29668:1132736] maxSize.width = 359.000000-------maxSize.height = 110.000000 //摺疊 2019-11-20 16:22:38.117764+0800 TodayExtension[29668:1132736] maxSize.width = 359.000000-------maxSize.height = 528.000000 //展開
&n