天天看點

iOS開發之建構Widget

原文出處:  陳凱 在 jianshu 的部落格(@chenkaiHome)   歡迎分享原創到 伯樂頭條

伴随這iOS 8 系統多達4000項API更新而來同樣還有Today Extension.而對iOS而言,有了Today Extension 開發者可以很好借助系統提供的接入點為系統定制的服務,提供自定義的附加功能.這意味着什麼呢?從iOS 7版本嘗試開路到現在iOS 8更新的到來終于向開發者開放Widget接入,這意味着系統應用和第三方應用都可以通知中心(Notification Center)裡面實作互動.

iOS開發之建構Widget

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隻能做到系統級的行為).點雖小,但這對使用者使用習慣改變卻是巨大的.

iOS開發之建構Widget

Widget on hands [Via Yalantis]

有人看到這肯定一定會問為何沒有提到Windows Phone平台?因為無論從通知中心快捷入口數量還是談到可以互動的點一句話而概之WP的現狀是“一窮二白”,你想作為曾經走過WP7時代使用者根本不知道通知中心為何物的,而是用了足足兩年時間WP8上才有展現,而那些被其他平台玩膩的希望習以為常通知中心互動,就像這樣:

iOS開發之建構Widget

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則互動上堪稱上典範:

iOS開發之建構Widget

Clear’s Widget

當然如果動點腦子會發現,Widget開放iOS上實作應用之間Launcher成為了可能,類似早期一直很魔性應用”Launcher”:

iOS開發之建構Widget

Launcher’s Widget

可以讓用在 iOS 的通知中心裡,以類似應用程式捷徑的方式直接快速切換 App 的小工具,其實當初在推出沒多久後,便被 Apple 以”誤用 / 濫用”Widgets 為理由下架,但有意思的就在幾天前3月20日又重新上架.

2.建構

在Widget技術實作細節上,并不打算在本篇把所有技術細節通覽一遍,我隻會寫我個人(其實就是初學者)認為值得寫的容易出錯的點或者耗費一些時間找到一些問題的解決方案.

2.1 純代碼建構

Xcode 6中已經支援Today Extension建立Widget的模闆,該模闆會預設建立MainInterface.storyboard檔案來建構UI:

iOS開發之建構Widget

StoryBoard UI

當然對于一個純代碼的擁趸而言,肯定直接删除storyboard檔案采用純代碼方式來進行建構,删除完後之後注意需要找到Supporting Files下面的Info.plist中NSExtension字段做如下兩個操作:

A:直接删除NSExtensionMainStoryboard字段

B:添加NSExtensionPrincipalClass字段 并設為TodayViewController

如下:

iOS開發之建構Widget

修改後

注意當采用Xcode預設模闆建立Widget時會自動把ViewController檔案命名設定為“TodayViewController”.當然這個ViewController命名其實是可以修改的,唯一值得注意的修改該ViewController檔案命名後還需要設定NSExtensionPrincipalClass的值與其保持一緻即可.不然Widget編譯時會報找不到對應入口.

2.2 左側間隔

當第一次添加UI元素采用真機來運作Widget會發現,Widget左側到螢幕之間始終會有一段距離的間隔,導緻調整布局和效果圖差距甚遠,類似這樣:

iOS開發之建構Widget

左側間隔

其實這個問題主要是因為Widget裡面的視圖預設居左居下都會有一定距離的間隔,可以采用如下方式取消間隔,使布局區域填充整個Widget:

iOS開發之建構Widget

取消間隔

這種方式把整個布局填充區域間隔都設定為0,當然更簡潔的方式是你可以直接采用“return UIEdgeInsetsZero;”方式.而關于Widget上布局處理則采用Masonry架構做的相對布局,簡單快捷推薦.當然關于Masonry架構快速上手則不得不推薦閱讀Masonry介紹與使用實踐(快速上手Autolayout).

2.3 整個點選區域實作

如你所看當使用者拉開Widget時,因為Widget是依賴于應用程式在分發時是跟應用程式一塊打包的,希望點選Widget布局任何區域都能喚起主應用程式,常用的方式在整個View增加Tap事件訂閱處理:

iOS開發之建構Widget

Tap事件

但這種方式會額外産生一個問題,如果Widget空白區域沒有任何UI元素則無法觸發該事件,那這裡有一個小技巧可以解決改問題,可以整個Widget增加一個透明的ImageView:

iOS開發之建構Widget

設定透明度

初始化時注意把imageview透明度設定為0.01最小值,那麼無論設定其背景色為什麼值肉眼都是不可見的.然後使用Masonry架構布局來填充Widget整個背景如下:

iOS開發之建構Widget

填充整個背景

然後為imageview增加Tap事件訂閱即可:

iOS開發之建構Widget

增加事件訂閱

這樣就能整個Widget區域可點選效果.另外針對通過Widget中喚起主應用程式方式目前隻支援url scheme方式來實作.同時也是Widget向主應用程式回報資料和互動的管道之一.

2.4 定時更新機制

Widget自身更新機制當使用者下拉通知中心(Notification Center)時立即更新資料,但我們仔細研究Widget使用者使用場景時發現,如果使用者鎖屏時間過長,打開Widget後不做任何操作,這個時候針對一些即時類應用,類似我們天氣中可能涉及到災害預警它要求場景資料一旦産生就要實時展現給使用者,這就需要我們基于Widget自身機制外還要處理這個場景下天氣資料自動更新的問題.

這個時候我們需要建構一個定時更新的NSTimer:

iOS開發之建構Widget

初始化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來判斷:

iOS開發之建構Widget

判斷橫屏方法一

當你執行這段代碼調試時你會發現,orientation方向的值始終都會是UIDeviceOrientationUnknown.如果你點開UIDeviceOrientation枚舉你會看到.它包含了兩個扁平方向UIDeviceOrientationFaceUp和UIDeviceOrientationFaceDown,其實它代表的意思螢幕朝上或朝下平躺兩個方向的判斷.是以當你裝置平躺桌面時.即時你有時已經切換了橫屏你會發現它會傳回FaceUp或FaceDown,是以你當你調用UIDeviceOrientationIsPortrait方法時它傳回值其實是沒有意義的,因為裝置目前方向在平躺下Faceup和FaceDown既不是橫屏也不是豎屏.難道沒有更好的方式嘛?

可以采用如下方式能夠完美解決Widget橫豎屏切換狀态判斷的問題:

iOS開發之建構Widget

Widget橫豎屏狀态判斷

其實設定Widget顯示高度時就會發現,高度在橫豎屏狀态切換是不會變化的,但寬度會随着橫豎屏狀态切換會發生變化,是以判斷螢幕寬度這個思路是可取的.因為橫豎屏UI布局不同,調用時機則可以選擇在viewWillLayoutSubviews或viewDidLayoutSubviews方法中進行.因為這兩個方法都是viewWillAppear方法是必然執行的,這也就自然規避Widget自身因為下拉快照儲存機制導緻代碼執行路徑變化導緻布局更新的問題.

2.6 Widget國際化

在來說說這個Widget國際化,因為我們用戶端自身已經支援三種不同語言,這就是導緻Widget也是需要根據端内語言變化必須有國際化的支援.其實我們端内已經做了一套完整的國際化機制.Widget最好處理方式能夠複用端内機制,而不需要單獨開發支援.iOS 8 新引入的自制 framework 的方式來組織需要重用的代碼,這樣在連結 framework 後 app 和Widget就都能使用相同的代碼. 包含Widget中資料請求和資料記憶其他能夠複用的代碼。

這也是我們一開始打算解決方式,但發現剝離這部分代碼時間周期明顯超過我們預期.是以在國際化處理上我們Widget獨立做了一套國際化處理,它和端内在處理機制上并沒有多大的不同:

iOS開發之建構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遇到一些大大小小的問題.解決問題方式雖然沒有給出細節,但思路是有的.有不清楚可以文後評論@我即可.