講述之前首先看下demo效果圖:
然后再展示幾個(gè)效果不錯(cuò)的 Widget app
一、Widget總覽
- Widget 是 iOS8 推出第一版坎匿,在iOS 10 進(jìn)行大幅度的優(yōu)化
- Widget可以讓用戶更快地訪問到其感興趣的內(nèi)容绢淀,官方的說法是用來呈現(xiàn)功能比較簡(jiǎn)單的,交互性不強(qiáng)的東西丢早,在不打擾或者中斷用戶使用當(dāng)前應(yīng)用的前提下完成自己的功能點(diǎn).對(duì)于這個(gè)說法姨裸,國(guó)內(nè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)致效果不佳售淡,可以通過下面的代理方法消除間距
// 取消widget默認(rèn)的inset,讓應(yīng)用靠左
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets {
return UIEdgeInsetsZero;
}
- Widget 的收起慷垮、展開 則是通過這個(gè)代理方法:
/**
activeDisplayMode有以下兩種
NCWidgetDisplayModeCompact, // 收起模式
NCWidgetDisplayModeExpanded, // 展開模式
*/
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
if(activeDisplayMode == NCWidgetDisplayModeCompact) {
// 尺寸只設(shè)置高度即可揖闸,因?yàn)閷挾仁枪潭ǖ模O(shè)置了也不會(huì)有效果
self.preferredContentSize = CGSizeMake(0, 110);
} else {
self.preferredContentSize = CGSizeMake(0, 310);
}
}
-
在設(shè)置 UI 的過程中料身,若想使用本體 Target 中的類:
在對(duì)應(yīng)類的 Target Membership 勾選 Widget 即可 -
如果想使用Pod 管理的第三方庫汤纸,那么只需要以下三步就可以愉快地玩耍了(比如我想使用 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,后來在其target 內(nèi)部建立一個(gè) .xcassets 文件即可加載圖片
-
然而在 Widget Extension 里面新建類又出現(xiàn)了如下報(bào)錯(cuò)
- 造成這個(gè)的原因是新建的時(shí)候默認(rèn)是 C header令蛉,而且沒有指向?qū)?yīng)的target聚霜,按照下圖所示修改一下type,選一下target言询,再次編譯就木有問題了
- 如果需要網(wǎng)絡(luò)請(qǐng)求俯萎,記住在 Extension 的plist文件中添加App Transport Security Settings 屬性
- 在開發(fā)過程中,那么怎么一直有個(gè)“Hello World”顯示运杭,最后看了一下原來是 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 是通過 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ò)說明 urlScheme有問題,沒有標(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)入到掃一掃頁面
[self.rootVC transferToRichScanVC];
} else if ([action isEqualToString:@"web"]) {
// 進(jìn)入到 web 活動(dòng)頁
[self.rootVC transferToWebVCWithUrlString:@"webTest"];
}
}
return YES;
}
- 數(shù)據(jù)共享:widget項(xiàng)目必然經(jīng)常要和主項(xiàng)目共享數(shù)據(jù)车伞,可以通過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 頁面后(iOS 10 左滑鳄哭,之前是下拉)要糊,先執(zhí)行 viewDidLoad 方法,然后是 viewWillAppear 方法窃诉,但是經(jīng)測(cè)驗(yàn)杨耙,Widget 頁面在屏幕消失超過兩秒后(手機(jī)沒有停留在 Widget 頁面 或者 停留在別的app 的Widget頁面赤套,自己的沒顯示)
- 由于以上特性,更新代碼最好寫在 viewWillAppear 方法里面珊膜,對(duì)于更新時(shí)效性特別強(qiáng)的容握,比如天氣類 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)求來的數(shù)據(jù)和當(dāng)前數(shù)據(jù)內(nèi)容一致塑陵,那么就不進(jìn)行刷新列表操作
不信你看
五感憾、關(guān)于 iOS8 適配
- iOS8、9是老式的下拉刷新令花,并沒有折疊和展開功能阻桅,默認(rèn)的Widget高度為self.preferredContentSize設(shè)置的高度
- iOS8 默認(rèn)的背景是黑色磨砂效果,iOS10默認(rèn)的背景色是白色磨砂效果兼都。因此在控件顏色上做下適配
- iOS8下所有組件默認(rèn)右移30pt
六嫂沉、其他注意點(diǎn)
當(dāng)程序內(nèi)存不足時(shí),蘋果優(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 證書配置指南
官網(wǎng)說明
一直很心儀的app --> Things 關(guān)于widget的介紹
幾個(gè)精致的 Widget app
在模擬器上進(jìn)行3D touch 測(cè)試
再次附上 demo Github 地址,歡迎star