從0到1思考與實(shí)現(xiàn)iOS-Widget

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

基本的展開(kāi)收起、本App本體交互

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


毒物 && Keep
ESPN
PCalc
Musixmatch
Fantastical 2
Carrot Weather

demo 地址在此!歡迎star

比心

一、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í)符

這個(gè)寫(xiě)好之后,再去擴(kuò)展的target做相同的操作,標(biāo)識(shí)符一定要一樣??!
切換 target 的方法在這里
  • 報(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效果圖
  • iOS8下所有組件默認(rèn)右移30pt

六、其他注意點(diǎn)

  1. 當(dāng)程序內(nèi)存不足時(shí),蘋(píng)果優(yōu)先會(huì)殺死擴(kuò)展,因此需要注意內(nèi)存的管理。

  2. 在配置team是賬號(hào)需要一致(免費(fèi)賬號(hào)不行,需要付費(fèi)的賬號(hào)),上傳包的時(shí)候一定注意選擇 Product -> Archive -> ** 選擇 distribution 模式!**

  3. 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容