講述之前首先看下demo效果圖:


然后再展示幾個(gè)效果不錯(cuò)的 Widget app







一、Widget總覽
- Widget 是 iOS8 推出第一版,在iOS 10 進(jìn)行大幅度的優(yōu)化
- Widget可以讓用戶(hù)更快地訪問(wèn)到其感興趣的內(nèi)容,官方的說(shuō)法是用來(lái)呈現(xiàn)功能比較簡(jiǎn)單的,交互性不強(qiáng)的東西,在不打擾或者中斷用戶(hù)使用當(dāng)前應(yīng)用的前提下完成自己的功能點(diǎn).對(duì)于這個(gè)說(shuō)法,國(guó)內(nèi)的開(kāi)發(fā)者表示呵呵,因?yàn)閹缀跛械?Widget都綁定了對(duì)應(yīng)的點(diǎn)擊事件
二、Widget代碼實(shí)現(xiàn)
-
因?yàn)?Widget 屬于單獨(dú)的進(jìn)程,因此需要再新建一個(gè)target:File -> New ->target
初次構(gòu)建 UI 時(shí),運(yùn)行 Widget 后會(huì)發(fā)現(xiàn),Widget左側(cè)距離屏幕左側(cè)始終有一段距離,導(dǎo)致效果不佳,可以通過(guò)下面的代理方法消除間距
// 取消widget默認(rèn)的inset,讓?xiě)?yīng)用靠左
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets {
return UIEdgeInsetsZero;
}
- Widget 的收起、展開(kāi) 則是通過(guò)這個(gè)代理方法:
/**
activeDisplayMode有以下兩種
NCWidgetDisplayModeCompact, // 收起模式
NCWidgetDisplayModeExpanded, // 展開(kāi)模式
*/
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
if(activeDisplayMode == NCWidgetDisplayModeCompact) {
// 尺寸只設(shè)置高度即可,因?yàn)閷挾仁枪潭ǖ?,設(shè)置了也不會(huì)有效果
self.preferredContentSize = CGSizeMake(0, 110);
} else {
self.preferredContentSize = CGSizeMake(0, 310);
}
}
-
在設(shè)置 UI 的過(guò)程中,若想使用本體 Target 中的類(lèi):
在對(duì)應(yīng)類(lèi)的 Target Membership 勾選 Widget 即可 -
如果想使用Pod 管理的第三方庫(kù),那么只需要以下三步就可以愉快地玩耍了(比如我想使用 Masonry 布局)
1、 在podfile文件中
2、 按照如圖所示配置configurations
3、 最后分別配置兩個(gè) Target 的 link Binanry
當(dāng)然有些第三方包含 source 文件的可能還需要?jiǎng)e的操作,最簡(jiǎn)單粗暴的方式就是-->拖進(jìn)去!
-
使用圖片也是必不可少,然而 imageNamed: 和 imageWithContentsOfFile: 兩種方式加載都不行,即使設(shè)置了文件的 target 為 Widget Extension,后來(lái)在其target 內(nèi)部建立一個(gè) .xcassets 文件即可加載圖片
-
然而在 Widget Extension 里面新建類(lèi)又出現(xiàn)了如下報(bào)錯(cuò)
- 造成這個(gè)的原因是新建的時(shí)候默認(rèn)是 C header,而且沒(méi)有指向?qū)?yīng)的target,按照下圖所示修改一下type,選一下target,再次編譯就木有問(wèn)題了

- 如果需要網(wǎng)絡(luò)請(qǐng)求,記住在 Extension 的plist文件中添加App Transport Security Settings 屬性
- 在開(kāi)發(fā)過(guò)程中,那么怎么一直有個(gè)“Hello World”顯示,最后看了一下原來(lái)是 Storyboard 加載,去 Storyboard 文件刪除對(duì)應(yīng) label 即可
-
如果你的項(xiàng)目中要求純代碼
- 刪除 Storyboard 文件和plist 對(duì)應(yīng)鍵值對(duì)
-
添加 NSExtensionPrincipalClass 字段并設(shè)置為 TodayViewController
三、與 App 本體交互
與本體 app 進(jìn)行交互之前,要明白的一個(gè)概念是:Widget 與 app 本身 是兩個(gè)target,appId 也是獨(dú)立的,因此 Widget 與本體 app 是通過(guò) app group 進(jìn)行交互
1、設(shè)置群組關(guān)系
在 本體 App 的 target > Capabilities添加 container 標(biāo)識(shí)符


- 報(bào)錯(cuò)信息:[_NCWidgetExtensionContext openURL:completionHandler:]_block_invoke failed: Error Domain=NSOSStatusErrorDomain Code=-50 "(null) 如果報(bào)這個(gè)錯(cuò)說(shuō)明 urlScheme有問(wèn)題,沒(méi)有標(biāo)準(zhǔn)對(duì)應(yīng),比如下劃線識(shí)別等
2、設(shè)置 scheme 進(jìn)行交互
-
設(shè)置 app 的 scheme 標(biāo)識(shí)符
在plist 文件內(nèi)添加以下鍵值對(duì) 然后!就可以在 Widget 對(duì)應(yīng)的點(diǎn)擊事件里面
// 掃一掃按鈕的點(diǎn)擊事件
- (void)scanBtnTapped:(UIButton *)sender {
[self.extensionContext openURL:[NSURL URLWithString:@"wpfWidgetTest://action=richScan"] completionHandler:^(BOOL success) {
NSLog(@"scanBtnTapped open url result:%d",success);
}];
}
- 在 app 本體的 AppDelegate 方法里面
// 處理 Widget 相關(guān)事件
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
NSString* prefix = @"wpfWidgetTest://action=";
NSString *urlString = [url absoluteString];
if ([urlString rangeOfString:prefix].location != NSNotFound) {
NSString *action = [urlString substringFromIndex:prefix.length];
if ([action isEqualToString:@"richScan"]) {
// 進(jìn)入到掃一掃頁(yè)面
[self.rootVC transferToRichScanVC];
} else if ([action isEqualToString:@"web"]) {
// 進(jìn)入到 web 活動(dòng)頁(yè)
[self.rootVC transferToWebVCWithUrlString:@"webTest"];
}
}
return YES;
}
- 數(shù)據(jù)共享:widget項(xiàng)目必然經(jīng)常要和主項(xiàng)目共享數(shù)據(jù),可以通過(guò)NSUserDefault,注意和平時(shí)用有些不同,創(chuàng)建UserDefault的時(shí)候,要指定groupid。上代碼:
// widget項(xiàng)目里取數(shù)據(jù)
+ (NSString*)widgetStringForKey:(NSString*)defaultName {
NSUserDefaults*shared = [[NSUserDefaultsalloc] initWithSuiteName:@"group.com.widgetTest"];
return[shared stringForKey:defaultName];
}
// 主項(xiàng)目里存數(shù)據(jù)
+ (void)widgetSetObject:(id)value forKey:(NSString*)defaultName {
NSUserDefaults*shared = [[NSUserDefaultsalloc] initWithSuiteName:@"group.com.widgetTest"];
[shared setObject:value forKey:defaultName];
[shared synchronize];
}
#warning 涉及到大量數(shù)據(jù)交互也可以使用 NSFileManager 進(jìn)行數(shù)據(jù)共享
在demo中,實(shí)現(xiàn)了從Widget入口 點(diǎn)擊未讀消息后,下次不再展示該未讀消息項(xiàng)

四、關(guān)于刷新時(shí)機(jī)
- Widget 自身的更新機(jī)制,是進(jìn)入到 Widget 頁(yè)面后(iOS 10 左滑,之前是下拉),先執(zhí)行 viewDidLoad 方法,然后是 viewWillAppear 方法,但是經(jīng)測(cè)驗(yàn),Widget 頁(yè)面在屏幕消失超過(guò)兩秒后(手機(jī)沒(méi)有停留在 Widget 頁(yè)面 或者 停留在別的app 的Widget頁(yè)面,自己的沒(méi)顯示)
- 由于以上特性,更新代碼最好寫(xiě)在 viewWillAppear 方法里面,對(duì)于更新時(shí)效性特別強(qiáng)的,比如天氣類(lèi) app,這種最好就是 在該方法里面添加一個(gè) NSTimer 定時(shí)進(jìn)行刷新,在 viewWillDisAppear 方法中 進(jìn)行 取消NSTimer invalidate定時(shí)更新即可
- 知乎、得到 app的 Widget,只要走 viewDidLoad 方法就會(huì)閃一下(如下圖),因?yàn)槊看蜽idget加載請(qǐng)求的數(shù)據(jù)后會(huì)進(jìn)行替換造成的。這里可以做個(gè)緩存優(yōu)化,判斷如果請(qǐng)求來(lái)的數(shù)據(jù)和當(dāng)前數(shù)據(jù)內(nèi)容一致,那么就不進(jìn)行刷新列表操作
不信你看
五、關(guān)于 iOS8 適配
- iOS8、9是老式的下拉刷新,并沒(méi)有折疊和展開(kāi)功能,默認(rèn)的Widget高度為self.preferredContentSize設(shè)置的高度
- iOS8 默認(rèn)的背景是黑色磨砂效果,iOS10默認(rèn)的背景色是白色磨砂效果。因此在控件顏色上做下適配

- iOS8下所有組件默認(rèn)右移30pt
六、其他注意點(diǎn)
當(dāng)程序內(nèi)存不足時(shí),蘋(píng)果優(yōu)先會(huì)殺死擴(kuò)展,因此需要注意內(nèi)存的管理。
在配置team是賬號(hào)需要一致(免費(fèi)賬號(hào)不行,需要付費(fèi)的賬號(hào)),上傳包的時(shí)候一定注意選擇 Product -> Archive -> ** 選擇 distribution 模式!**
-
3D touch 對(duì)應(yīng)的也有Widget!?答案是 YES!,只要設(shè)置了3D touch,Widget的第一欄就會(huì)自動(dòng)顯示。但是如果有多個(gè)widget的話,還需要在 info.plist 指定相應(yīng)的main target!
Extension 證書(shū)配置指南
官網(wǎng)說(shuō)明
一直很心儀的app --> Things 關(guān)于widget的介紹
幾個(gè)精致的 Widget app
在模擬器上進(jìn)行3D touch 測(cè)試

再次附上 demo Github 地址,歡迎star










