iOS開發------Widget(Today Extension)外掛程式化開發

來源:互聯網
上載者:User

標籤:

iOS10.0發布啦(貌似過去有點時間了吧 - -),在宏觀帶給我們使用體驗的提升之外,更多的是帶給iOS開發人員一定的欣喜。因為我們又要學習新東西來適配10啦。


博文所說的Widget(以下稱之為拓展應用)並不是iOS10系統新推出的外掛程式化應用(其實早在iOS8上就已經出現啦,只不過樓主是在iOS10發布之後才算真正的關注它,實在是慚愧呀)。iOS10之前它僅僅是存在於通知那一欄中,至於多隱蔽我就不說了吧。但在iOS10之後獲得重生,地位獲得了巨大的提升,從這點也不難看出蘋果增加了對它的重視。儘管公司的App沒有適配Widget,但作為一個“後知後覺”的iOS開發人員,注意到了但不研究一下就說不過去了吧?

為了避免真實情況與博文的圖不太符合,這裡聲明一下:樓主用的IDE是最新版的Xcode8.0(沒辦法,還是迫不及待的進行了升級0.0),可能會與其他版本的Xcode介面不太一樣

博文中的所有代碼:https://github.com/RITL/WidgetDemo(如果有用請star支援一下,感謝)

預覽圖

這裡附上Widget Demo中完成後的預覽圖: 這裡會稍有不同,如果使用Xcode7及之前版本IDE編譯的應用(後面稱作宿主應用),那麼找到Widget的方法1;如果是Xcode8編譯的宿主應用,那麼可以直接通過3D Touch喚起Widget,當然通過第一種也是可以的。不過兩者本質是一樣的。

   


建立Widget Extension

1.首先建立一個新的Target: New->Target,Xcode8 會出現如下介面,選擇Today Extension,命名為WidgetExtension:

2、建立完畢,則會出現如下檔案夾,名字什麼的不是問題,一般建立好的名字都為TodayViewController,我只不過是改了改名字而已O(∩_∩)O

3、這裡囉嗦一句,雖然作為應用的拓展,但這兩個應用是“獨立”存在的,你也可以認為這拓展應用與宿主應用是兩個完全獨立的應用,這也就是說明在開發過程中會出現一些共用的問題,不過共用問題下面博文會有介紹。在此之前,對於拓展應用,我們也是要去開發人員申請APP ID以及開發,發布認證的。

由於樓主只是為了學習,用了Xcode8的Automatically manager signing,它的作用是自動產生id以及認證。

作用細說一點就是:如果開發Team沒有相應的APP ID,那麼Xcode會自動產生APP ID; 如果沒有建立相應的認證,那麼它會自動建立認證 (當然,正常開發過程中,還是建議手動去建立ID以及配置認證吧)

4、認證都配置完畢,運行,添加Widget,就可以看到咱們的項目已經具備了Widget的拓展功能,預設的是MainInterface.storyboard上的內容啦:(我改了改Label上的字,O(∩_∩)O)


布局方式interface builder or coding

如果牽扯到UI繪製的方式,這裡只需要調整一點東西即可。Demo中樓主選用的是使用storyboard完成快速布局,當然,如果開發人員習慣使用代碼來完成布局,依舊是可以的。需要對拓展應用的info.plist檔案做如下操作:

使用interface builder

這個是預設的,如果修改了預設的storyboard,只需要將NSExtensionMainStoryboard的value修改成相應的storyboard名字即可

使用coding

首先將NSExtensionMainStoryboard欄位刪除,添加NSExtensionPrincipalClass字典,value為主控制器的類名即可。

使用這個方法不要忘記在todayViewController的ViewDidLoad中設定preferredContentSize屬性調整大小。


資料共用

很多的時候我們需要Widget與宿主應用共用一些資料,想到資料共用,如果是單一的APP,我們的方法是很多的,比如單例,檔案等形式,但由於拓展與宿主應用是兩個完全獨立的App,並且iOS應用基於沙箱的形式,所以一般的共用資料方法都是實現不了資料共用,這裡就需要使用App Groups。

App Groups

1、首先需要在開發人員網站註冊一個App Groups

2、在 宿主應用 以及 拓展應用 中將App Groups開啟,選中需要共用資料的group


兩種共用資料的方式


使用UserDefaults共用資料

NSUserDefaults大家應該都是非常熟悉的了,通常用法就是

//擷取UserDefaults的單例對象,完成對應用內相關資料的持久化儲存[NSUserDefaults standardUserDefaults];

正像之前所說,由於沙箱機制,拓展應用是不允許訪問宿主應用的沙箱路徑的,因此上述用法是不對的,需要搭配app group完成執行個體化UserDefaults,使用UserDefaults類進行資料共用樓主封裝為RITL_ShareDataDefaultsManager

通過groups執行個體化UserDefaults對象的代碼如下:

//組名private static let groupIdentifier : String = "group.com.yue.WidgetTest"/// 獲得userDefualt對象private class func __userDefault() -> UserDefaults{    return UserDefaults(suiteName: RITL_ShareDataDefaultsManager.groupIdentifier)!}

儲存資料方法如下,至於為什麼會有open關鍵詞(與public作用是一樣的,只不過開發文檔中新的API貌似都改為open了),因為樓主在Demo中將該檔案分離出來了,需要實現”不同命名空間”代碼共用,所以Swift預設的Internal範圍就顯得許可權不足了,至於如何分離下面會提及:

//存放資料的索引值private static let defaultKey : String = "com.yue.WidgetTest.value"/// 儲存資料open class func saveData(_ value : String){    //儲存資料    __userDefault().set(value, forKey: RITL_ShareDataDefaultsManager.defaultKey)    __userDefault().synchronize()}

擷取資料的方法與儲存資料很像:

/// 擷取資料open class func getData() -> String!{    //如果值為nil,表示沒有存過值,返回預設的值    let value = (__userDefault().value(forKey: RITL_ShareDataDefaultsManager.defaultKey))    __userDefault().synchronize()    guard value == nil else {        return value as! String    }    return ""}

因為是通過檔案來產生,所以必須要在必要的時候對儲存的資料進行刪除,如下:

/// 清除資料open class func clearData(){    __userDefault().removeSuite(named: RITL_ShareDataDefaultsManager.groupIdentifier)    __userDefault().synchronize()}


使用FileManager共用資料

第二種方法說本質的與第一種是一樣的,因為他們都是通過在本地建立檔案完成資料的共用,該功能的Demo中封裝成了RITL_ShareDataFileManager

與第一種不同的就是,它不但要執行個體化對象,還需要獲得儲存資料的路徑,如下:

//組名private static let groupIdentifier : String = "group.com.yue.WidgetTest"//儲存的路徑private static let dataSavePathFile : String = "Library/Caches/widgetTest"/// 獲得儲存的路徑private class func __fileManagerSavePath() -> URL{    //獲得當前的組的路徑    var url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: RITL_ShareDataFileManager.groupIdentifier)    //返回拼接完畢的路徑    url?.appendPathComponent(RITL_ShareDataFileManager.dataSavePathFile)    return url!}

儲存資料的方法,因為在Swift中有的方法是throw異常的,所以寫法稍有不同,如下:

/// 儲存資料open class func saveData(_ value:String) -> Bool{    //進行儲存    do {        try value.write(to: __fileManagerSavePath(), atomically: true, encoding: String.Encoding.utf8)    } catch _ as NSError {//出錯        return false    }    return true}

擷取資料的方法只是讀取存放的資料即可,當然Demo中存的是字串,方法實現如下:

/// 擷取資料open class func getData() -> String{    //用於接收資料    var value : String    do {//讀取資料        try value = String(contentsOf: __fileManagerSavePath())    } catch _ as NSError {        return ""//有誤輸出Null 字元串    }    return value}

必要時候不要忘記清除資料:

/// 清除資料open class func clearData() -> Bool{//其實不太規範,應該先判斷是否存在該檔案,再進行刪除    do {//開始刪除        try FileManager.default.removeItem(at: __fileManagerSavePath())    } catch _ as NSError{        return false    }    return true}


代碼共用

這裡為什麼會有代碼共用呢,如果上面兩個儲存的類寫在了宿主應用目錄下,那麼宿主應用使用是沒有問題的,but,這個時候拓展應用是擷取不到這兩個類的,當然,如果每個應用裡都寫一套不就可以了,雖然這樣也能解決問題,但我很難用完美解決問題來形容他,因為這樣不僅會出現命名,不好維護等眾多問題,嚴重的時候還會帶來很多問題,話不多說,如何共用代碼呢?

使用Framework

這個問題在iOS8之後能夠完美的用framework來解決,(如果有人問iOS7怎麼辦?請面壁3秒鐘,Widget不是iOS8才對我們開放的麼0.0)

1、與建立拓展一樣,New->Target,選擇Cocoa Touch Framework來建立framework,Demo中命名隨便了一點,起名為RITLKit

2、將需要共用程式碼從源項目的編譯源中刪除,添加到RITLKit中

3、將建立的framework都要連結到 宿主專案 以及 拓展應用 的Linked Frameworks and Libraries中,不要忘了,都要添加,不然可能會出現找不到檔案的問題

4、這裡提示一下,如果上面的步驟完成,但是在拓展中還是提示找不到檔案,那麼還需要做一個步驟,就是將我們的framework添加到拓展應用中Allow app extension API only選中即可,將如下:

5、以上步驟完畢之後,應該就可以在拓展以及宿主應用中實現代碼共用了,但還有一點:

如果是ObjC項目,匯入Objc的檔案,只需使用#import"XX.h"匯入即可,但是如果framework中含有Swift檔案,使用#import "Project-Swift.h"是匯入不進去項目的,可以使用@import RILTKit; 對建立的framework編譯的檔案進行匯入,就可以使用Swift檔案了,這件事在Demo中也已經實現。


Extension與宿主應用互動

通過點擊Widget上的按鈕來開啟宿主應用並實現響應操作也是一種重要的互動手段,如何?呢?

1、首先我們需要在宿主應用的Target->Info->URL Types中添加url Schemes

2、通過Widget來開啟宿主應用,Demo中點擊Widget中的按鈕跳轉至不同的介面,通過Widget開啟宿主應用的操作如下:

/// 開啟我的App- (void)openMyApplication:(NSString *)title{    NSURL * url = [NSURL URLWithString:[NSString stringWithFormat:@"WidgetDemoOpenViewController://%@",title]];   [self.extensionContext openURL:url completionHandler:^(BOOL success) {}];}

3、宿主App通過AppDelegate中的響應openUrl的代理方法,接收資訊並發出通知來響應全域:

/// -(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{    if ([url.scheme isEqualToString:@"WidgetDemoOpenViewController"])    {        NSLog(@"host = %@",url.host);        //發送通知        [[NSNotificationCenter defaultCenter] postNotificationName:@"ExtenicationNotification" object:url.host];    }    return false;}

4、宿主應用中響應通知的控制器接收通知即可,比如Demo中是首頁進行跳轉:

//添加獲得拓展開啟基礎應用的通知[[NSNotificationCenter defaultCenter] addObserverForName:@"ExtenicationNotification" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {    //獲得類型    NSString * type = note.object;    [weakSelf presentTextController:type];}];


NCWidgetProviding協議

如何仔細看,其實Widget的控制器與其他的控制器是沒有區別的,只不過它履行了一個叫做”NCWidgetProviding”的協議。協議方法不多,在iOS10中新增了一個,廢棄了一個,如下:

// 這個就不用多說了吧,沒有很難得單詞哦0.0typedef NS_ENUM(NSUInteger, NCUpdateResult) {    NCUpdateResultNewData,    NCUpdateResultNoData,    NCUpdateResultFailed} NS_ENUM_AVAILABLE_IOS(8_0);/* 該方法是用來告知Widget控制器是否需要更新的一個協議方法 */- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult result))completionHandler;

比如Demo中為了避免重複重新整理做了如下操作:

- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {    // Perform any setup necessary in order to update the view.    // If an error is encountered, use NCUpdateResultFailed    // If there‘s no update required, use NCUpdateResultNoData    // If there‘s an update, use NCUpdateResultNewData    //獲得資料    NSString * newValue = [RITL_ShareDataDefaultsManager getData];    if ([newValue isEqualToString:self.textLabel.text])//表明沒有更新    {        completionHandler(NCUpdateResultNoData);    }    else//需要重新整理    {        completionHandler(NCUpdateResultNewData);    }}
// iOS10 版本之後將不會再被喚起// 用來設定Widget控制器邊框間距的方法,如果出現偏差,可以調整此方法的傳回值進行操作- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets NS_DEPRECATED_IOS(8_0, 10_0, "This method will not be called on widgets linked against iOS versions 10.0 and later.");// iOS10 新增的方法// 用來設定Widget是展開還是摺疊狀態的方法,可以設定相關的preferredContentSizes屬性修改大小- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize NS_AVAILABLE_IOS(10_0);


儲存資料的時機

這個看具體的需求,比如Demo中就是選擇在宿主應用將要失去Active狀態的時候進行資料的儲存,實現如下:

    //獲得失去前台的監聽    [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {//進行資料的儲存        //儲存當前的資料#ifdef RITL_ShareDataType_UserDefaults        //第一種儲存資料        [RITL_ShareDataDefaultsManager saveData:weakSelf.mainTextField.text];#else        //第二種儲存資料        [RITL_ShareDataFileManager saveData:weakSelf.mainTextField.text];#endif    }];


Widget無法展開摺疊問題

2016-09-24補充

之前丟了一點,也有小夥伴們問,就是說按照上面的方式來開發外掛程式,不能摺疊的問題.

解決方案:

//在TodayViewController的ViewDidLoad裡面需要設定最大展示的類型#ifdef __IPHONE_10_0 //因為是iOS10才有的,還請記得適配    //如果需要摺疊    self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;#endif

更多的歡迎下載Github代碼一起鑽研~3Q

感謝一下博文對我的協助,感謝
iOS開發之widget實現
WWDC 2014 Session筆記 - iOS 通知中樞擴充製作入門

iOS開發------Widget(Today Extension)外掛程式化開發

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.