iOS10.0發(fā)布啦(貌似過去有點時間了吧 - -),在宏觀帶給我們使用體驗的提升之外,更多的是帶給iOS開發(fā)者一定的欣喜。
因為我們又要學(xué)習(xí)新東西來適配10啦。
博文所說的Widget(以下稱之為拓展應(yīng)用)并不是iOS10系統(tǒng)新推出的插件化應(yīng)用(其實早在iOS8上就已經(jīng)出現(xiàn)啦,只不過樓主是在iOS10發(fā)布之后才算真正的關(guān)注它疏之,實在是慚愧呀)。iOS10之前它僅僅是存在于通知那一欄中暇咆,至于多隱蔽我就不說了吧锋爪。但在iOS10之后獲得重生,地位獲得了巨大的提升爸业,從這點也不難看出蘋果增加了對它的重視其骄。盡管公司的App沒有適配Widget,但作為一個“后知后覺”的iOS開發(fā)者扯旷,注意到了但不研究一下就說不過去了吧拯爽?
為了避免真實情況與博文的圖不太符合,這里聲明一下:樓主用的IDE是最新版的Xcode8.0(沒辦法钧忽,還是迫不及待的進行了升級0.0)毯炮,可能會與其他版本的Xcode界面不太一樣
博文中的所有代碼:https://github.com/RITL/WidgetDemo(如果有用請star支持一下逼肯,感謝)
預(yù)覽圖
這里附上Widget Demo中完成后的預(yù)覽圖: 這里會稍有不同,如果使用Xcode7及之前版本IDE編譯的應(yīng)用(后面稱作宿主應(yīng)用)桃煎,那么找到Widget的方法如圖1篮幢;如果是Xcode8編譯的宿主應(yīng)用,那么可以直接通過3D Touch喚起Widget为迈,當(dāng)然通過第一種也是可以的三椿。不過兩者本質(zhì)是一樣的。
</img>
</img>
創(chuàng)建Widget Extension
1.首先創(chuàng)建一個新的Target: New->Target葫辐,Xcode8 會出現(xiàn)如下界面搜锰,選擇Today Extension,命名為WidgetExtension:
</img>
2耿战、創(chuàng)建完畢蛋叼,則會出現(xiàn)如下文件夾,名字什么的不是問題剂陡,一般創(chuàng)建好的名字都為TodayViewController狈涮,我只不過是改了改名字而已O(∩_∩)O
</img>
3、這里啰嗦一句鹏倘,雖然作為應(yīng)用的拓展,但這兩個應(yīng)用是“獨立”存在的顽爹,你也可以認為這拓展應(yīng)用與宿主應(yīng)用是兩個完全獨立的應(yīng)用纤泵,這也就是說明在開發(fā)過程中會出現(xiàn)一些共享的問題,不過共享問題下面博文會有介紹镜粤。在此之前捏题,對于拓展應(yīng)用,我們也是要去開發(fā)者申請APP ID以及開發(fā)肉渴,發(fā)布證書的公荧。
由于樓主只是為了學(xué)習(xí),用了Xcode8的Automatically manager signing同规,它的作用是自動生成id以及證書循狰。
作用細說一點就是:如果開發(fā)Team沒有相應(yīng)的APP ID,那么Xcode會自動生成APP ID; 如果沒有創(chuàng)建相應(yīng)的證書券勺,那么它會自動創(chuàng)建證書 (當(dāng)然绪钥,正常開發(fā)過程中,還是建議手動去創(chuàng)建ID以及配置證書吧)
</img>
4关炼、證書都配置完畢程腹,運行,添加Widget儒拂,就可以看到咱們的項目已經(jīng)具備了Widget的拓展功能寸潦,默認的是MainInterface.storyboard上的內(nèi)容啦:(我改了改Label上的字色鸳,O(∩_∩)O)
</img>
布局方式interface builder or coding
如果牽扯到UI繪制的方式,這里只需要調(diào)整一點東西即可见转。Demo中樓主選用的是使用storyboard完成快速布局命雀,當(dāng)然,如果開發(fā)者習(xí)慣使用代碼來完成布局池户,依舊是可以的咏雌。需要對拓展應(yīng)用的info.plist文件做如下操作:
使用interface builder
這個是默認的,如果修改了默認的storyboard校焦,只需要將NSExtensionMainStoryboard
的value修改成相應(yīng)的storyboard名字即可
</img>
使用coding
首先將NSExtensionMainStoryboard
字段刪除赊抖,添加NSExtensionPrincipalClass
字典,value為主控制器的類名即可寨典。
使用這個方法不要忘記在todayViewController的ViewDidLoad中設(shè)置preferredContentSize屬性調(diào)整大小氛雪。
</img>
數(shù)據(jù)共享
很多的時候我們需要Widget與宿主應(yīng)用共享一些數(shù)據(jù),想到數(shù)據(jù)共享耸成,如果是單一的APP报亩,我們的方法是很多的,比如單例井氢,文件等形式弦追,但由于拓展與宿主應(yīng)用是兩個完全獨立的App,并且iOS應(yīng)用基于沙盒的形式花竞,所以一般的共享數(shù)據(jù)方法都是實現(xiàn)不了數(shù)據(jù)共享劲件,這里就需要使用App Groups。
App Groups
1约急、首先需要在開發(fā)者網(wǎng)站注冊一個App Groups
</img>
2零远、在 宿主應(yīng)用 以及 拓展應(yīng)用 中將App Groups打開,選中需要共享數(shù)據(jù)的group
兩種共享數(shù)據(jù)的方式
使用UserDefaults共享數(shù)據(jù)
NSUserDefaults大家應(yīng)該都是非常熟悉的了厌蔽,通常用法就是
//獲取UserDefaults的單例對象牵辣,完成對應(yīng)用內(nèi)相關(guān)數(shù)據(jù)的持久化儲存
[NSUserDefaults standardUserDefaults];
正像之前所說,由于沙盒機制奴饮,拓展應(yīng)用是不允許訪問宿主應(yīng)用的沙盒路徑的纬向,因此上述用法是不對的,需要搭配app group完成實例化UserDefaults戴卜,使用UserDefaults類進行數(shù)據(jù)共享樓主封裝為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)!
}
存儲數(shù)據(jù)方法如下罢猪,至于為什么會有open關(guān)鍵詞(與public作用是一樣的,只不過開發(fā)文檔中新的API貌似都改為open了)叉瘩,因為樓主在Demo中將該文件分離出來了膳帕,需要實現(xiàn)"不同命名空間"代碼共用,所以Swift默認的Internal作用域就顯得權(quán)限不足了,至于如何分離下面會提及:
//存放數(shù)據(jù)的鍵值
private static let defaultKey : String = "com.yue.WidgetTest.value"
/// 保存數(shù)據(jù)
open class func saveData(_ value : String)
{
//保存數(shù)據(jù)
__userDefault().set(value, forKey: RITL_ShareDataDefaultsManager.defaultKey)
__userDefault().synchronize()
}
獲取數(shù)據(jù)的方法與保存數(shù)據(jù)很像:
/// 獲取數(shù)據(jù)
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 ""
}
因為是通過文件來生成攒磨,所以必須要在必要的時候?qū)Υ鎯Φ臄?shù)據(jù)進行刪除,如下:
/// 清除數(shù)據(jù)
open class func clearData()
{
__userDefault().removeSuite(named: RITL_ShareDataDefaultsManager.groupIdentifier)
__userDefault().synchronize()
}
使用FileManager共享數(shù)據(jù)
第二種方法說本質(zhì)的與第一種是一樣的汤徽,因為他們都是通過在本地創(chuàng)建文件完成數(shù)據(jù)的共享娩缰,該功能的Demo中封裝成了RITL_ShareDataFileManager
與第一種不同的就是,它不但要實例化對象谒府,還需要獲得保存數(shù)據(jù)的路徑拼坎,如下:
//組名
private static let groupIdentifier : String = "group.com.yue.WidgetTest"
//存儲的路徑
private static let dataSavePathFile : String = "Library/Caches/widgetTest"
/// 獲得存儲的路徑
private class func __fileManagerSavePath() -> URL
{
//獲得當(dāng)前的組的路徑
var url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: RITL_ShareDataFileManager.groupIdentifier)
//返回拼接完畢的路徑
url?.appendPathComponent(RITL_ShareDataFileManager.dataSavePathFile)
return url!
}
保存數(shù)據(jù)的方法,因為在Swift中有的方法是throw異常的完疫,所以寫法稍有不同泰鸡,如下:
/// 保存數(shù)據(jù)
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
}
獲取數(shù)據(jù)的方法只是讀取存放的數(shù)據(jù)即可,當(dāng)然Demo中存的是字符串壳鹤,方法實現(xiàn)如下:
/// 獲取數(shù)據(jù)
open class func getData() -> String
{
//用于接收數(shù)據(jù)
var value : String
do {//讀取數(shù)據(jù)
try value = String(contentsOf: __fileManagerSavePath())
} catch _ as NSError {
return ""http://有誤輸出空字符串
}
return value
}
必要時候不要忘記清除數(shù)據(jù):
/// 清除數(shù)據(jù)
open class func clearData() -> Bool
{
//其實不太規(guī)范盛龄,應(yīng)該先判斷是否存在該文件,再進行刪除
do {//開始刪除
try FileManager.default.removeItem(at: __fileManagerSavePath())
} catch _ as NSError{
return false
}
return true
}
代碼共享
這里為什么會有代碼共享呢芳誓,如果上面兩個存儲的類寫在了宿主應(yīng)用目錄下余舶,那么宿主應(yīng)用使用是沒有問題的,but锹淌,這個時候拓展應(yīng)用是獲取不到這兩個類的匿值,當(dāng)然,如果每個應(yīng)用里都寫一套不就可以了赂摆,雖然這樣也能解決問題挟憔,但我很難用完美解決問題來形容他,因為這樣不僅會出現(xiàn)命名库正,不好維護等眾多問題曲楚,嚴重的時候還會帶來很多問題厘唾,話不多說褥符,如何共享代碼呢?
使用Framework
這個問題在iOS8之后能夠完美的用framework來解決抚垃,(如果有人問iOS7怎么辦喷楣?請面壁3秒鐘,Widget不是iOS8才對我們開放的么0.0)
1鹤树、與創(chuàng)建拓展一樣铣焊,New->Target,選擇Cocoa Touch Framework來創(chuàng)建framework罕伯,Demo中命名隨便了一點曲伊,起名為RITLKit
</img>
2、將需要共享的代碼從源項目的編譯源中刪除,添加到RITLKit中
</img>
3坟募、將創(chuàng)建的framework都要鏈接到 宿主項目 以及 拓展應(yīng)用 的Linked Frameworks and Libraries中岛蚤,不要忘了,都要添加懈糯,不然可能會出現(xiàn)找不到文件的問題
</img>
4涤妒、這里提示一下,如果上面的步驟完成赚哗,但是在拓展中還是提示找不到文件她紫,那么還需要做一個步驟,就是將我們的framework添加到拓展應(yīng)用中Allow app extension API only
選中即可屿储,將如下:
</img>
5贿讹、以上步驟完畢之后,應(yīng)該就可以在拓展以及宿主應(yīng)用中實現(xiàn)代碼共用了扩所,但還有一點:
如果是ObjC項目围详,導(dǎo)入Objc的文件,只需使用#import"XX.h"
導(dǎo)入即可祖屏,但是如果framework中含有Swift文件助赞,使用#import "Project-Swift.h"
是導(dǎo)入不進去項目的,可以使用@import RILTKit;
對創(chuàng)建的framework編譯的文件進行導(dǎo)入袁勺,就可以使用Swift文件了雹食,這件事在Demo中也已經(jīng)實現(xiàn)。
Extension與宿主應(yīng)用交互
通過點擊Widget上的按鈕來打開宿主應(yīng)用并實現(xiàn)響應(yīng)操作也是一種重要的交互手段期丰,如何實現(xiàn)呢群叶?
1、首先我們需要在宿主應(yīng)用的Target->Info->URL Types中添加url Schemes
</img>
2钝荡、通過Widget來打開宿主應(yīng)用街立,Demo中點擊Widget中的按鈕跳轉(zhuǎn)至不同的界面,通過Widget打開宿主應(yīng)用的操作如下:
/// 打開我的App
- (void)openMyApplication:(NSString *)title
{
NSURL * url = [NSURL URLWithString:[NSString stringWithFormat:@"WidgetDemoOpenViewController://%@",title]];
[self.extensionContext openURL:url completionHandler:^(BOOL success) {}];
}
3埠通、宿主App通過AppDelegate中的響應(yīng)openUrl的代理方法赎离,接收信息并發(fā)出通知來響應(yīng)全局:
///
-(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
if ([url.scheme isEqualToString:@"WidgetDemoOpenViewController"])
{
NSLog(@"host = %@",url.host);
//發(fā)送通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"ExtenicationNotification" object:url.host];
}
return false;
}
4、宿主應(yīng)用中響應(yīng)通知的控制器接收通知即可端辱,比如Demo中是主頁進行跳轉(zhuǎn):
//添加獲得拓展打開基礎(chǔ)應(yīng)用的通知
[[NSNotificationCenter defaultCenter] addObserverForName:@"ExtenicationNotification" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
//獲得類型
NSString * type = note.object;
[weakSelf presentTextController:type];
}];
NCWidgetProviding協(xié)議
如何仔細看梁剔,其實Widget的控制器與其他的控制器是沒有區(qū)別的,只不過它履行了一個叫做"NCWidgetProviding"的協(xié)議舞蔽。協(xié)議方法不多荣病,在iOS10中新增了一個,廢棄了一個渗柿,如下:
// 這個就不用多說了吧个盆,沒有很難得單詞哦0.0
typedef NS_ENUM(NSUInteger, NCUpdateResult) {
NCUpdateResultNewData,
NCUpdateResultNoData,
NCUpdateResultFailed
} NS_ENUM_AVAILABLE_IOS(8_0);
/* 該方法是用來告知Widget控制器是否需要更新的一個協(xié)議方法 */
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult result))completionHandler;
比如Demo中為了避免重復(fù)刷新做了如下操作:
- (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
//獲得數(shù)據(jù)
NSString * newValue = [RITL_ShareDataDefaultsManager getData];
if ([newValue isEqualToString:self.textLabel.text])//表明沒有更新
{
completionHandler(NCUpdateResultNoData);
}
else//需要刷新
{
completionHandler(NCUpdateResultNewData);
}
}
// iOS10 版本之后將不會再被喚起
// 用來設(shè)置Widget控制器邊框間距的方法,如果出現(xiàn)偏差,可以調(diào)整此方法的返回值進行操作
- (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 新增的方法
// 用來設(shè)置Widget是展開還是折疊狀態(tài)的方法颊亮,可以設(shè)置相關(guān)的preferredContentSizes屬性修改大小
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize NS_AVAILABLE_IOS(10_0);
保存數(shù)據(jù)的時機
這個看具體的需求鸡岗,比如Demo中就是選擇在宿主應(yīng)用將要失去Active狀態(tài)的時候進行數(shù)據(jù)的保存,實現(xiàn)如下:
//獲得失去前臺的監(jiān)聽
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillResignActiveNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {//進行數(shù)據(jù)的保存
//保存當(dāng)前的數(shù)據(jù)
#ifdef RITL_ShareDataType_UserDefaults
//第一種保存數(shù)據(jù)
[RITL_ShareDataDefaultsManager saveData:weakSelf.mainTextField.text];
#else
//第二種保存數(shù)據(jù)
[RITL_ShareDataFileManager saveData:weakSelf.mainTextField.text];
#endif
}];
Widget無法展開折疊問題
2016-09-24補充
之前丟了一點编兄,也有小伙伴們問轩性,就是說按照上面的方式來開發(fā)插件,不能折疊的問題.
解決方案:
//在TodayViewController的ViewDidLoad里面需要設(shè)置最大展示的類型
#ifdef __IPHONE_10_0 //因為是iOS10才有的狠鸳,還請記得適配
//如果需要折疊
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
#endif
Widget高度范圍
2016-09-28補充
首先感謝一下CSDN用戶qq_29012653的提示揣苏,之前沒有太注意,在協(xié)議方法widgetActiveDisplayModeDidChange:withMaximumSize:
打印了一下各種狀態(tài)下的MaxSize:
// 5s模擬器下:
// NCWidgetDisplayModeCompact模式下:{304, 110}
// NCWidgetDisplayModeExpanded模式下:{304, 528}
// 6s模擬器下:
// NCWidgetDisplayModeCompact模式下:{359, 110}
// NCWidgetDisplayModeExpanded模式下:{359, 616}
//從上面來看件舵,雖說是最大的Size,但不得不說卸察,蘋果還是把Widget的高度范圍限制在了110 ~ 最大值之間
//如果設(shè)置高度小于110,那么default = 110;
//如果設(shè)置高度大于最大的616铅祸,那么default = 最大值;
但折疊狀態(tài)下坑质,可能是出于規(guī)范的考慮,蘋果是果斷的將高度固定在110上临梗,所以這個時候我們設(shè)置preferredContentSize屬性是沒有任何效果的涡扼。
更多的歡迎下載Github代碼一起鉆研~3Q
感謝一下博文對我的幫助,感謝
iOS開發(fā)之widget實現(xiàn)
WWDC 2014 Session筆記 - iOS 通知中心擴展制作入門