伴隨這iOS 8 系統多達4000項API更新而來同樣還有Today Extension。而對iOS而言,有了Today Extension 開發者可以很好借助系統提供的接入點為系統定制的服務,提供自定義的附加功能.這意味著什麼呢?從iOS 7版本嘗試開路到現在iOS 8更新的到來終於向開發者開放Widget接入,這意味著系統應用和第三方應用都可以通知中心(Notification Center)裡面實現交互。
Notification Center Widget [Via Apple]
其實相對於Android,因其特有開放性Widget插件已經發展了很多年,擁有極高自由定制性,在新版本的Android系統中甚至可以將部分插件擺在鎖屏頁.而Google和各大軟件廠商制作的Widget插件也能很好與系統的整體風格進行無縫的融合,而直到目前iOS 8版本中,Widget也就只是能擺在通知中心(Notification Center)今天通知欄中而已,相對於Android也聽到很多人把這個作為"iOS不夠開放"一個有力的依據。針對這個問題其實Apple也在iOS Human Interface Guidelines中提到:
iOS 8 中開發者的中心並不應該發生改變,依然應該是圍繞 app.在 app 中提供優秀交互和有用的功能,現在是,將來也會是 iOS 應用開發的核心任務。而Widget在 iOS 中是不能以單獨的形式存在的,一定是隨著一個應用一起打包提供的。
從這個側面可見,Apple對開放一直持有審慎的態度,開放的目的是力求保證整體體驗完整性,雖然iOS的Widget相比Android自定義性太低,但基於Apple目前的開放程度而言是能夠很有效控制Widget與系統的更好的融合.雖似戴著鐐铐起舞,但卻能捕獲人心。
而從用戶角度來看,在無需打開應用前提下就可以對消息進行處理的交互特性,使它在很多場景裡有效提升了用戶操作效率.例如在Widget中快速回復email,即時完成Todo日程等.這種交互更多從更宏觀角度重新定義了消息,通知中心(Notification Center)通過獲取用戶上一行為,還可以起到承接下一行為的作用(雖然目前開放API只能做到系統級的行為).點雖小,但這對用戶使用習慣改變卻是巨大的。
Widget on hands [Via Yalantis]
有人看到這肯定一定會問為何沒有提到Windows Phone平台?因為無論從通知中心快捷入口數量還是談到可以交互的點一句話而概之WP的現狀是“一窮二白”,你想作為曾經走過WP7時代用戶根本不知道通知中心為何物的,而是用了足足兩年時間WP8上才有體現,而那些被其他平台玩膩的希望習以為常通知中心交互,就像這樣:
WP 通知中心[Via PCGGroup]
你就像看這張靜態圖片一樣也就是停留只是看看程度而已(除了刪除操作之外),MS針對通知中心現在最新消息是未來會支持類似可以通知中心直接回復短信等交互,至於什麼時候能夠等到,誰知道呢.
說了這麼多,回歸正題.
1.交互
在開始構建Widget之前,如果想對Widget實現技術細節和交互特點有一個完整概覽,我覺得沒有什麼文檔比官方App Extension Programming Guide更值得一讀了.剛開始接觸iOS通知中心,一直很疑惑為何通知中心采用兩個不同Tab“今日”和“通知”來對消息進行分離.其實這和Widget工作機制有關.
Widget是放在“今日”Tab之中,而它工作機制是只有用戶下拉通知中心時才會去刷新獲取最新數據,這種做法和Android不同在於,Android更偏向於把整個Widget一直放在後台實時持續的更新.設想一下,如果我們看同樣天氣信息,Android會持續消耗資源去做一件用戶不會實時預覽信息,這也就能解釋為何經常看到Android用戶抱怨耗電問題.而對於即時消息,iOS做法是直接把這些消息實時歸類到”通知“Tab中.其實這種做法很好解決采用消耗最少資源前提下保證其操作的靈活性.
因為現有Widget一般來說是展現在系統級別的 UI上,所以在App Extension Programming Guide中Apple對Widget交互提出如下明確的要求:
擴展應該保持輕巧迅速,並且專注功能單一,在不打擾或者中斷用戶使用當前應用的前提下完成自己的功能點.
類似一直摯愛Todo應用Clear則交互上堪稱上典范:
Clear's Widget
當然如果動點腦子會發現,Widget開放iOS上實現應用之間Launcher成為了可能,類似早期一直很魔性應用"Launcher":
Launcher's Widget
可以讓用在 iOS 的通知中心裡,以類似應用程序捷徑的方式直接快速切換 App 的小工具,其實當初在推出沒多久後,便被 Apple 以"誤用 / 濫用"Widgets 為理由下架,但有意思的就在幾天前3月20日又重新上架.
2.構建
在Widget技術實現細節上,並不打算在本篇把所有技術細節通覽一遍,我只會寫我個人(其實就是初學者)認為值得寫的容易出錯的點或者耗費一些時間找到一些問題的解決方案.
2.1 純代碼構建
Xcode 6中已經支持Today Extension創建Widget的模板,該模板會默認創建MainInterface.storyboard文件來構建UI:
StoryBoard UI
當然對於一個純代碼的擁趸而言,肯定直接刪除storyboard文件采用純代碼方式來進行構建,刪除完後之後注意需要找到Supporting Files下面的Info.plist中NSExtension字段做如下兩個操作:
A:直接刪除NSExtensionMainStoryboard字段
B:添加NSExtensionPrincipalClass字段 並設為TodayViewController
如下:
修改後
注意當采用Xcode默認模板創建Widget時會自動把ViewController文件命名設置為“TodayViewController”.當然這個ViewController命名其實是可以修改的,唯一值得注意的修改該ViewController文件命名後還需要設置NSExtensionPrincipalClass的值與其保持一致即可.不然Widget編譯時會報找不到對應入口.
2.2 左側間隔
當第一次添加UI元素采用真機來運行Widget會發現,Widget左側到屏幕之間始終會有一段距離的間隔,導致調整布局和效果圖差距甚遠,類似這樣:
左側間隔
其實這個問題主要是因為Widget裡面的視圖默認居左居下都會有一定距離的間隔,可以采用如下方式取消間隔,使布局區域填充整個Widget:
取消間隔
這種方式把整個布局填充區域間隔都設置為0,當然更簡潔的方式是你可以直接采用“return UIEdgeInsetsZero;”方式.而關於Widget上布局處理則采用Masonry框架做的相對布局,簡單快捷推薦.當然關於Masonry框架快速上手則不得不推薦閱讀Masonry介紹與使用實踐(快速上手Autolayout).
2.3 整個點擊區域實現
如你所看當用戶拉開Widget時,因為Widget是依賴於應用程序在分發時是跟應用程序一塊打包的,希望點擊Widget布局任何區域都能喚起主應用程序,常用的方式在整個View增加Tap事件訂閱處理:
Tap事件
但這種方式會額外產生一個問題,如果Widget空白區域沒有任何UI元素則無法觸發該事件,那這裡有一個小技巧可以解決改問題,可以整個Widget增加一個透明的ImageView:
設置透明度
初始化時注意把imageview透明度設置為0.01最小值,那麼無論設置其背景色為什麼值肉眼都是不可見的.然後使用Masonry框架布局來填充Widget整個背景如下:
填充整個背景
然後為imageview增加Tap事件訂閱即可:
增加事件訂閱
這樣就能整個Widget區域可點擊效果.另外針對通過Widget中喚起主應用程序方式目前只支持url scheme方式來實現.同時也是Widget向主應用程序反饋數據和交互的渠道之一.
2.4 定時更新機制
Widget自身更新機制當用戶下拉通知中心(Notification Center)時立即更新數據,但我們仔細研究Widget用戶使用場景時發現,如果用戶鎖屏時間過長,打開Widget後不做任何操作,這個時候針對一些即時類應用,類似我們天氣中可能涉及到災害預警它要求場景數據一旦產生就要實時展現給用戶,這就需要我們基於Widget自身機制外還要處理這個場景下天氣數據自動更新的問題.
這個時候我們需要構建一個定時更新的NSTimer:
初始化NSTimer
非常簡單,在NSTimer固定更新間隔執行的方法調用就是更新數據方法,當然重點不在這裡,而是觸發和關閉這個NSTimer時機.按照Widget生命周期來說,如果用戶是第一次下拉查看Widget其實就是執行整個ViewController生命周期調用過程,這個並沒有什麼問題,但是還是存在一個特殊情況.系統為了保證Widget上數據是及時更新的,默認會截取上次顯示成功Widget的快照.這個快照會一直保存到新的數據或UI被更新才回被替換,那這就會帶來一個問題,當你拖拽通知中心(Notification Center)下拉過於頻繁時,Debug跟蹤代碼執行路徑你會發現整個Widget生命周期執行過程和第一次下拉執行的路徑發生了變化.
第一次下拉執行路徑是viewDidLoad->viewWillAppear,而如果下拉過於頻繁你就會發現代碼執行路徑直接只會執行viewWillAppear方法,這個就是系統默認保存上次快照而導致的執行路徑上變化.這對我們選擇NSTimer更新時機以及後面會提到的Widget橫豎屏處理都會有影響.
那麼很明顯,為了保證這個定時更新機制能夠無論用戶什麼情況下操作都能起作用,我們需要把NSTimer fire觸發代碼調用放到viewWillAppear方法中來.同理當Widget關閉後在viewDidDisappear方法取消NSTimer invalidate定時更新即可.
2.5 Widget橫屏支持
關於Widget橫屏支持在開發中耽誤一點時間來解決這個問題,在iPhone 6 & Plus上已經橫豎屏直接切換,Widget默認是豎屏,但如果你需求中橫屏UI的布局和豎屏布局完全不同,這個時候你就需要判斷當前Widget橫豎屏狀態來切換對應的布局.
當然一般思路我們都會按照端內處理橫豎屏方式來處理Widget,如果你翻過官方的開發文檔,你會發現在iOS 6.0版本之前UIViewController之間橫豎屏切換,只需要設置shouldAutorotateToInterfaceOrientation函數即可.UIInterfaceOrientation是UIApplication.h頭文件中定義的枚舉類型,總共有四個方向.在shouldAutorotateToInterfaceOrientation方法中返回相應的結果即可,如果直接返回YES將支持所有方向.而在iOS 6.0版本之後,UIViewController之間橫豎屏切換需要多設置一個supportedInterfaceOrientations函數返回UIInterfaceOrientationMask枚舉類型.除了設置shouldAutorotateToInterfaceOrientation之外,還要將supportedInterfaceOrientations返回的方向與shouldAutorotateToInterfaceOrientation保持一致,否則會在兩個支持不同橫豎屏ViewController中切換時,會出現豎屏變橫屏,橫屏變豎屏的情況.但問題是這種方式是否適用Widget橫屏處理呢?
使用UIDeviceOrientationIsPortrait來判斷:
判斷橫屏方法一
當你執行這段代碼調試時你會發現,orientation方向的值始終都會是UIDeviceOrientationUnknown.如果你點開UIDeviceOrientation枚舉你會看到.它包含了兩個扁平方向UIDeviceOrientationFaceUp和UIDeviceOrientationFaceDown,其實它代表的意思屏幕朝上或朝下平躺兩個方向的判斷.所以當你設備平躺桌面時.即時你有時已經切換了橫屏你會發現它會返回FaceUp或FaceDown,所以你當你調用UIDeviceOrientationIsPortrait方法時它返回值其實是沒有意義的,因為設備目前方向在平躺下Faceup和FaceDown既不是橫屏也不是豎屏.難道沒有更好的方式嘛?
可以采用如下方式能夠完美解決Widget橫豎屏切換狀態判斷的問題:
Widget橫豎屏狀態判斷
其實設置Widget顯示高度時就會發現,高度在橫豎屏狀態切換是不會變化的,但寬度會隨著橫豎屏狀態切換會發生變化,所以判斷屏幕寬度這個思路是可取的.因為橫豎屏UI布局不同,調用時機則可以選擇在viewWillLayoutSubviews或viewDidLayoutSubviews方法中進行.因為這兩個方法都是viewWillAppear方法是必然執行的,這也就自然規避Widget自身因為下拉快照保存機制導致代碼執行路徑變化導致布局更新的問題.
2.6 Widget國際化
在來說說這個Widget國際化,因為我們客戶端自身已經支持三種不同語言,這就是導致Widget也是需要根據端內語言變化必須有國際化的支持.其實我們端內已經做了一套完整的國際化機制.Widget最好處理方式能夠復用端內機制,而不需要單獨開發支持.iOS 8 新引入的自制 framework 的方式來組織需要重用的代碼,這樣在鏈接 framework 後 app 和Widget就都能使用相同的代碼. 包含Widget中數據請求和數據記憶其他能夠復用的代碼。
這也是我們一開始打算解決方式,但發現剝離這部分代碼時間周期明顯超過我們預期.所以在國際化處理上我們Widget獨立做了一套國際化處理,它和端內在處理機制上並沒有多大的不同:
Widget國際化處理
當然重點不再於它的實現,你可以發現我們Widget中國際化文本文件Locallizable.string命名加了一個"WG",這個問題是剛開始開發之初我們一直認為Widget作為端是獨立於主應用程序的.所以當初理解為只有把這個文件命名為的“Locallizable.string”才是正常的能夠被識別的,但我們調試時發現,Widget打包時會把這些國際化單獨放到PlugIns文件下,這裡給出一個簡體中文全路徑:
/private/var/mobile/Containers/Bundle/Application/61C637FF-B5BC-432A-ADD5-BA64EBFE98E8/MojiWeather.app/PlugIns/MojiWidget.appex/zh-Hans.lproj
根據這個路徑你會發現文件時可以找到的,但調試時發現國際化取對應Key的值一直是取不到的,但我們任意非“Locallizable.string”時則是沒有問題的,後來我們發現當我們打包在不同機型上測試這個問題時,如果“Locallizable.string”名稱命名會導致調試時ok,而最終打包上會出現找不到對應key值得問題.這個原因到我寫這篇blog一直沒有找到具體的原因.所以我們給出解決方案是一定要和主應用程序“Locallizable.string”保持不同即可解決.
當然關於Widget中閃現的問題,因為我們Widget存在兩個不同尺寸切換,導致這個問題很明顯,處理方式自然是viewWillLoad方式中做好Widget高度在不同場景高度初始化就可以完美避免.這裡就不做贅述.
如上只是我們解決Widget遇到一些大大小小的問題.解決問題方式雖然沒有給出細節,但思路是有的.有不清楚可以文後評論@我即可..