在iOS開發中,Widget(小組件)的功能屬于雞肋的功能,因為大多數的人都很少去使用,或者很多人都不知道還有這功能。不過在項目完成時可以添加上去,增加項目的bigger。本文會詳細介紹小組件的開發以及開發中會遇到的坑,如果大家又遇到新的坑,可以在下面留言,大家一起交流。
聲明:本文是使用Xcode11.0beta6版本+iOS13.1beta2版本作為展示用例。但是項目創建是使用Xcode10.3創建的,這是因為害怕部分用戶下載Demo后無法在Xcode10中運行。現在使用Xcode的11創建APP會多一個SceneDelegate,在Xcode10中無法識別,也會簡單的說明一下當前Demo找Xcode10與Xcode11的簡單區別。所展示的支付寶版本為10.1.72,今日頭條版本為7.4.0。(避免今后兩款APP更新以后與本文展示的不一致)
1.什么是小組件
小組件即如圖的下面兩種呈現方式:第一種是在最左頁(俗稱第0頁)或者是往下滑的時候會出現的頁面,展示如下:
這個需要用戶去點擊編輯才會顯示出來。
第二種展示方式是長按當前APP后會出來(也就是3DTouch),展示如下:
這種方式只要長按就會觸發,不需要用戶去添加。
常見的小組件都是這兩種方式顯示,一種是支付寶的那種固定樣式的,點擊某一個功能后就跳轉到APP的某個頁面去。另外一種是今日頭條的動態顯示的,每次都回去請求數據然后刷新顯示。接下來兩種方式的開發都會進行講解。
2.靜態小組件的開發(類似支付寶的樣式)
對于想做成類似支付寶的點擊小組件的某個按鈕就跳轉的指定頁面的是最簡單的。有一些其他文章上來就說需要去添加APP Groups的對于這種類型的來說是不必要的,這個不涉及到數據共享的東西,就簡單的跳轉,完全沒必要去進行創建APP Groups。下面說明詳細步驟:
完成主項目后,點擊如下兩個步驟完成widget的創建:
選擇Today Extension:
填寫項目名稱即可完成創建。接下來在TodayViewController的viewDidLoad里面進行按鈕的創建:
CGFloat width = [UIScreen mainScreen].bounds.size.width;
//NCWidgetDisplayModeCompact 不需要折疊樣式,整個完整顯示
//NCWidgetDisplayModeExpanded 有折疊按鈕,點擊可以顯示更多
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeCompact;
//設置整個小組件的寬高
self.preferredContentSize = CGSizeMake(width, 130);
NSArray *titleArray = @[
@"掃一掃",
@"掃一掃",
@"掃一掃",
@"掃一掃",
];
NSArray *imageArray = @[
@"widget_scan",
@"widget_scan",
@"widget_scan",
@"widget_scan",
];
CGFloat btnWidth = 60;
CGFloat btnHeight = 60;
CGFloat margin = (width - titleArray.count * btnWidth) / (titleArray.count + 1);
CGFloat y = 20;
for (NSInteger i = 0; i < titleArray.count; i ++) {
CGFloat x = margin + i * (btnWidth + margin);
UIButton *btn = [[UIButton alloc] init];
btn.frame = CGRectMake(x, y, btnWidth, btnHeight);
[btn setImage:[UIImage imageNamed:imageArray[i]] forState:UIControlStateNormal];
[btn setTitle:titleArray[i] forState:UIControlStateNormal];
btn.tag = i;
[btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
運行后效果如下圖:這是為什么呢?沒有寫hello world確顯示出來了。這是因為系統默認是用小組件的MainInterface.storyboard進行顯示的,如果你喜歡使用storyboard直接在里面布局就行了,但是我一般都用代碼開發,所以需要去當前小組件的文件下的info.plist刪除 NSExtensionMainStoryboard 選項,增加 NSExtensionPrincipalClass,value為類的名字TodayViewController即可用純代碼進行布局。
配置完成后再次運行。
現在能看到文字顯示出來了,但是圖片沒有,這是因為在放進去圖片的時候,沒有勾選在widget項目里面也能共,所以當前項目找不到圖片,只需要去勾選就可以在當前文件使用即可。
如果想引入主項目里面的其他文件,也和圖片一樣的勾選,只是在上面那里導入是不行的,必須進行勾選才能使用,比如我需要導入一個分類文件:
勾選圖片以后就能看到圖片了。
demo有不完美的地方,按鈕的圖片和title不能對齊,不過不影響demo的演示:)
布局完成能顯示以后,接下來進行點擊按鈕跳轉,由于需要跳轉到主項目的某個頁面,所以需要在主項目中用到URL Types。強烈建議設置成包名的形式,否則和其他APP一樣的schemes可能會跳轉他們的APP去
ok,配置完成后,只需要在widget的按鈕里面實現點擊事件然后在主項目的AppDelegate的openURL里面實現點擊以后的判讀即可。
widget的點擊事件如下:
- (void)btnDidClick:(UIButton *)btn{
NSInteger index = btn.tag;
//因為我是需要跳轉到某個頁面,所以我直接傳遞跳轉頁面的類名
NSString *vcString = @"";
if (index == 0) {
vcString = @"OneViewController";
}else if (index == 1){
vcString = @"OneViewController";
}else if (index == 2){
vcString = @"ViewController";
}else if (index == 3){
vcString = @"TwoViewController";
}
[self.extensionContext openURL:[NSURL URLWithString:[NSString stringWithFormat:@"com.widgetDemo://%@",vcString]] completionHandler:nil];
}
AppDelegate的openURL如下:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{
[self appCallbackWithOpenUrl:url];
return YES;
}
- (void)appCallbackWithOpenUrl:(NSURL *)url{
// 針對url進行不同的操作
NSString *URLString = [NSString stringWithFormat:@"%@",url];
NSArray *array = [URLString componentsSeparatedByString:@"://"];
NSString *vcString = @"";
//這里最好進行判斷URL的開頭是不是設置的schemes,如果用到其他的第三方SDK,有可能是他們的URL
if (array.count >= 2) {
vcString = array[1];
}
//這里可以先判斷是否登錄或者其他邏輯在進行下面的操作
if ([vcString isEqualToString:@"OneViewController"]) {
UINavigationController *nav = ((UITabBarController*)self.window.rootViewController).selectedViewController;
UIViewController *VC = [[NSClassFromString(vcString) alloc] init];
[nav pushViewController:VC animated:YES];
}else if ([vcString isEqualToString:@"ViewController"]){
UINavigationController *nav = ((UITabBarController*)self.window.rootViewController).selectedViewController;
UIViewController *VC = [[NSClassFromString(vcString) alloc] init];
[nav pushViewController:VC animated:YES];
}else if ([vcString isEqualToString:@"TwoViewController"]){
UITabBarController *tabBarVC = (UITabBarController *)self.window.rootViewController;
tabBarVC.selectedIndex = 1;
}
}
一個固定點擊跳轉的小組件就這樣開發完成,很簡單的。如果不需要數據共享,根本不需要其他文章說的先去配置APP Groups,就如此簡單即可。
3.動態小組件的開發(類似今日頭條的樣式)
做了靜態的小組件以后,動態的也不難,類似于今日頭條的小組件,會有數據的共享,以及數據的請求。但是有以下注意點:
3.1主項目框架的使用
如果項目中用CocoaPods導入了第三方框架,比如Masonry和AFNetworking。在小組件的項目中有可能是能直接導入頭文件的,但是實際使用卻是不行的。這時候需要在podfile文件中加入小組件的target然后在pod install,這樣在小組件的項目中才能使用。這樣以后才能在小組件的項目里面使用第三方框架。
3.2數據共享
如果需要將主APP的某些數據共享給小組件使用,這時候就需要進行數據共享,使用到APP Groups,本次以Xcoede11截圖。在Xcode10中,直接把開關打開添加即可。因為我直接在Xcode里面登錄了賬號,所以不需要去網頁上進行證書那些的配置即可,如果是用其他方式的,需要去網頁上進行相關的設置。然后就可以在需要存儲的地方進行數據的存儲了,不過現在只支持兩種存儲,一個是NSUserDefaults,一個是NSFileManager。
NSUserDefaults存儲數據
NSUserDefaults *sharedData = [[NSUserDefaults alloc] initWithSuiteName:GroupID];
[sharedData setValue:@"測試存儲" forKey:Name];
[sharedData synchronize];
NSFileManager 存儲數據
NSError *error = nil;
NSURL *containUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:GroupID];
containUrl = [containUrl URLByAppendingPathComponent:Data];
NSString *text = @"NSFileManager 存儲數據";
BOOL result = [text writeToURL:containUrl atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (result){
NSLog(@"save success");
}else {
NSLog(@"error:%@", error);
}
在這個方法你可以獲取到時候折疊
// 展開/折疊監聽
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize{
}
至此,兩種方式創建的小組件都完成。
如果大家還遇到什么其他問題可以在下面留言,大家一起交流交流。
最后附上Demo地址
)