擴充應該保持輕巧迅速,並且專註功能單一,在不打擾或者中斷使用者使用當前應用的前提下完成自己的功能點.
當然如果動點腦子會發現,Widget開放iOS上實現應用之間Launcher成為了可能,類似早期一直很魔性應用”Launcher”: Launcher’s Widget
可以讓用在 iOS 的通知中樞裡,以類似應用程式捷徑的方式直接快速切換 App 的小工具,其實當初在推出沒多久後,便被 Apple 以”誤用 / 濫用”Widgets 為理由下架,但有意思的就在幾天前3月20日又重新上架.
2.構建
在Widget技術實現細節上,並不打算在本篇把所有技術細節通覽一遍,我只會寫我個人(其實就是初學者)認為值得寫的容易出錯的點或者耗費一些時間找到一些問題的解決方案.
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遇到一些大大小小的問題.解決問題方式雖然沒有給出細節,但思路是有的.有不清楚可以文後評論@我即可.