1、簡介ios
iOS應用程序擴展是蘋果在iOS8推出的一個新特性,能夠將自定義的功能和內容擴展到應用程序以外,在以後又通過不斷地優化和更新,已經成爲開發中不可或缺的功能之一。擴展也是一個Target項目,它運行在主機應用程序上,能夠與主機應用程序實現資源共享,和宿主應用程序的Target項目是彼此獨立的。系統提供的擴展有不少,Toady擴展就是其中之一,也被成爲應用程序插件,它的做用是將今日發生的簡單消息展現在系統的插件界面上。Toady擴展模板名稱爲Today Extension。圖1是建立Today擴展,圖2是擴展顯示在插件界面上(能夠經過點擊Edit來添加或者移除擴展)。
網絡
2、建立app
按照上圖1的方式建立一個Today Extension的Target後,系統會默認幫咱們生成一個TodayViewController控制器類、MainInterface.storyBoard故事板、plist序列化文件,文件結構圖以下:框架
上圖中紅色圈內和箭頭指向的配置就是系統經過MainInterface.storyBoard幫咱們實現了一個基本的Toady插件UI佈局,運行後能夠直接顯示在插件界面上。但是,有的時候開發者並不想使用系統的故事板來構建UI,系統支持自定義的,咱們只須要修改plist配置便可。具體的配置是這樣的:ide
[1] 將NSExtensionMainStoryboard字段刪除;佈局
[2] 添加NSExtensionPrincipalClass字典,修改value爲主控制器的類名。 優化
[3] 在TodayViewController中的ViewDidLoad中設置preferredContentSize屬性大小,用來調整widget界面UI的尺寸。ui
配置以下圖所示:atom
3、分析url
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
4、交互
Today擴展是寄宿於主機應用程序上的, TodayViewController又是一個UIViewController類,系統支持Today擴展對UIViewController進行切換。也就是說,蘋果在考慮提供給開發者在對UIViewController中添加各類展現控件這種便利的同時,也相應的提供給開發者經過Today擴展的widget從主機應用程序激活並打開宿主應用程序的機會。不過這個操做必須經過設置並調起scheme來實現。步驟以下:
[1] 配置宿主應用程序的scheme;
[2] 使用擴展的openURL打開宿主應用程序。
交互以下:
//擴展經過scheme打開主宿主應用程序 [self.extensionContext openURL:[NSURL URLWithString:@"MainApp://"] completionHandler:nil];
5、數據
既然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);
6、適配
從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
7、範例
【去掉MainInterface.storyBoard,採用純代碼實現】
一、宿主應用程序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; }
二、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
三、打印和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 //展開