從0到1思考與實現iOS-Widget

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

基本的展開收起、本App本體交互

然后再展示幾個效果不錯的 Widget app


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

demo 地址在此!歡迎star

比心

一、Widget總覽

  • Widget 是 iOS8 推出第一版,在iOS 10 進行大幅度的優化
  • Widget可以讓用戶更快地訪問到其感興趣的內容,官方的說法是用來呈現功能比較簡單的,交互性不強的東西,在不打擾或者中斷用戶使用當前應用的前提下完成自己的功能點.對于這個說法,國內的開發者表示呵呵,因為幾乎所有的 Widget都綁定了對應的點擊事件

二、Widget代碼實現

  • 因為 Widget 屬于單獨的進程,因此需要再新建一個target:File -> New ->target


  • 初次構建 UI 時,運行 Widget 后會發現,Widget左側距離屏幕左側始終有一段距離,導致效果不佳,可以通過下面的代理方法消除間距

// 取消widget默認的inset,讓應用靠左
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets {
    return UIEdgeInsetsZero;
}
  • Widget 的收起、展開 則是通過這個代理方法:
/**
 activeDisplayMode有以下兩種
     NCWidgetDisplayModeCompact, // 收起模式
     NCWidgetDisplayModeExpanded, // 展開模式
 */
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
    if(activeDisplayMode == NCWidgetDisplayModeCompact) {
        // 尺寸只設置高度即可,因為寬度是固定的,設置了也不會有效果
        self.preferredContentSize = CGSizeMake(0, 110);
    } else {
        self.preferredContentSize = CGSizeMake(0, 310);
    }
}
  • 在設置 UI 的過程中,若想使用本體 Target 中的類:


    在對應類的 Target Membership 勾選 Widget 即可
  • 如果想使用Pod 管理的第三方庫,那么只需要以下三步就可以愉快地玩耍了(比如我想使用 Masonry 布局)
    1、 在podfile文件中



    2、 按照如圖所示配置configurations



    3、 最后分別配置兩個 Target 的 link Binanry

當然有些第三方包含 source 文件的可能還需要別的操作,最簡單粗暴的方式就是-->拖進去!

  • 使用圖片也是必不可少,然而 imageNamed: 和 imageWithContentsOfFile: 兩種方式加載都不行,即使設置了文件的 target 為 Widget Extension,后來在其target 內部建立一個 .xcassets 文件即可加載圖片


  • 然而在 Widget Extension 里面新建類又出現了如下報錯


    • 造成這個的原因是新建的時候默認是 C header,而且沒有指向對應的target,按照下圖所示修改一下type,選一下target,再次編譯就木有問題了
  • 如果需要網絡請求,記住在 Extension 的plist文件中添加App Transport Security Settings 屬性
  • 在開發過程中,那么怎么一直有個“Hello World”顯示,最后看了一下原來是 Storyboard 加載,去 Storyboard 文件刪除對應 label 即可
  • 如果你的項目中要求純代碼
    • 刪除 Storyboard 文件和plist 對應鍵值對
    • 添加 NSExtensionPrincipalClass 字段并設置為 TodayViewController



三、與 App 本體交互

與本體 app 進行交互之前,要明白的一個概念是:Widget 與 app 本身 是兩個target,appId 也是獨立的,因此 Widget 與本體 app 是通過 app group 進行交互

1、設置群組關系

在 本體 App 的 target > Capabilities添加 container 標識符

這個寫好之后,再去擴展的target做相同的操作,標識符一定要一樣!!
切換 target 的方法在這里
  • 報錯信息:[_NCWidgetExtensionContext openURL:completionHandler:]_block_invoke failed: Error Domain=NSOSStatusErrorDomain Code=-50 "(null) 如果報這個錯說明 urlScheme有問題,沒有標準對應,比如下劃線識別等
2、設置 scheme 進行交互
  • 設置 app 的 scheme 標識符


    在plist 文件內添加以下鍵值對
  • 然后!就可以在 Widget 對應的點擊事件里面

// 掃一掃按鈕的點擊事件
- (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 相關事件
- (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"]) {
            // 進入到掃一掃頁面
            [self.rootVC transferToRichScanVC];
        } else if ([action isEqualToString:@"web"]) {
            // 進入到 web 活動頁
            [self.rootVC transferToWebVCWithUrlString:@"webTest"];
        } 
    }
    return  YES;
}
  • 數據共享:widget項目必然經常要和主項目共享數據,可以通過NSUserDefault,注意和平時用有些不同,創建UserDefault的時候,要指定groupid。上代碼:
// widget項目里取數據
+ (NSString*)widgetStringForKey:(NSString*)defaultName {
     NSUserDefaults*shared = [[NSUserDefaultsalloc] initWithSuiteName:@"group.com.widgetTest"];
     return[shared stringForKey:defaultName];
}

// 主項目里存數據
+ (void)widgetSetObject:(id)value forKey:(NSString*)defaultName {
    NSUserDefaults*shared = [[NSUserDefaultsalloc] initWithSuiteName:@"group.com.widgetTest"];
    [shared setObject:value forKey:defaultName];
    [shared synchronize];
}

#warning 涉及到大量數據交互也可以使用 NSFileManager 進行數據共享

在demo中,實現了從Widget入口 點擊未讀消息后,下次不再展示該未讀消息項



四、關于刷新時機

  • Widget 自身的更新機制,是進入到 Widget 頁面后(iOS 10 左滑,之前是下拉),先執行 viewDidLoad 方法,然后是 viewWillAppear 方法,但是經測驗,Widget 頁面在屏幕消失超過兩秒后(手機沒有停留在 Widget 頁面 或者 停留在別的app 的Widget頁面,自己的沒顯示)
  • 由于以上特性,更新代碼最好寫在 viewWillAppear 方法里面,對于更新時效性特別強的,比如天氣類 app,這種最好就是 在該方法里面添加一個 NSTimer 定時進行刷新,在 viewWillDisAppear 方法中 進行 取消NSTimer invalidate定時更新即可
  • 知乎、得到 app的 Widget,只要走 viewDidLoad 方法就會閃一下(如下圖),因為每次Widget加載請求的數據后會進行替換造成的。這里可以做個緩存優化,判斷如果請求來的數據和當前數據內容一致,那么就不進行刷新列表操作
    不信你看

五、關于 iOS8 適配

  • iOS8、9是老式的下拉刷新,并沒有折疊和展開功能,默認的Widget高度為self.preferredContentSize設置的高度
  • iOS8 默認的背景是黑色磨砂效果,iOS10默認的背景色是白色磨砂效果。因此在控件顏色上做下適配
iOS8效果圖
  • iOS8下所有組件默認右移30pt

六、其他注意點

  1. 當程序內存不足時,蘋果優先會殺死擴展,因此需要注意內存的管理。

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

  3. 3D touch 對應的也有Widget!?答案是 YES!,只要設置了3D touch,Widget的第一欄就會自動顯示。但是如果有多個widget的話,還需要在 info.plist 指定相應的main target!

Extension 證書配置指南
官網說明
一直很心儀的app --> Things 關于widget的介紹
幾個精致的 Widget app
在模擬器上進行3D touch 測試


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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容