1. 程式人生 > >iOS:應用程式擴充套件開發之Today擴充套件(Today Extesnsion)

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