Apple Watch即將於4月下旬發售,而Watch App的開發已成為iOS開發的熱點。本文作者通過Watch App的實際開發經驗,將其中的一些注意事項總結分享給大家。以下為正文:
接觸Apple Watch相關的開發工作已經差不多快三個月時間了,每天都會去逛逛WatchKit蘋果的開發者論壇,看看最近都有哪些其他開發者po出來的問題。我自己也遇到不少問題,其中很多都是我自己摸索著解決掉的。
蘋果公布的關於Apple Watch的信息很多,用於開發已經足夠,但一切感覺都是在抹黑前行,因為無法進行真機測試,包括Handoff,也包括語音輸入,以及發布會上的那個類似Emoji 的表情都是些什麼。
自己來現在的公司實習到今,主要做的工作幾乎都和iOS8新特性有關,畢竟現在公司這個項目實在是太成熟了,摸熟悉也需要一個過程。包括之前的 Today Widget,到後來的Handoff,包括因為要適配iPhone6做的適配方面的調研等等,都是從去年WWDC之後的新事物,轉眼就到2015年的 WWDC了,不知道今年會有哪些革新的新事物。
閒話說到這裡吧,是時候總結一下這兩個月的收獲和掉坑了。
目前開發者網站上的這幾部分我覺得是開發Watch 必須學習幾遍的東西,還有蘋果開發者論壇也是一個不錯的交流地方。
在iPhone上,主程序是大哥,其他的小擴展必須讓路,但是在Watch上,是不是大哥還要看這個APP主要的功能。如果是一個閱讀性質的 APP,主程序在手表上作用還真不大,例如閱讀新聞等等。如果是這類的應用,想在Watch上出彩,或者讓用戶使用的次數多一些,就要靠良好的 Notification體驗,以及極其方便用戶生活的Glance了。
如上圖,現在手上要做的一個交互是,App啟動的時候是六個頁面,用戶可以左右滑動來切換,這裡就需要在MainInterfaceController中使用下邊這個方法了。
[WKInterfaceController reloadRootControllersWithNames: _controllersArrays contexts:_contextsArray];
在Watch上頁面之間轉換傳值,很重要的一個紐帶就是這個context,傳遞有用的信息和標識,這個方法中,我傳遞進入六個controller的interface builder identifier,以及事前拼好的六個context。
因為Watch App 的打開可以是幾種不同方式的,可以寫一個統一的方法[self showController],在這個方法中去選擇啟動哪一個具體的Controller。我在.h文件中定義了一個枚舉來定義不同的啟動方式:
typedef enum { WKOpenForNormal, //普通打開 WKOpenForComment, //打開評論頁 WKOpenForFavorite, //打開收藏頁 WKOpenForGlance //打開來自glance的內容 } WKOpenType;
因為用戶如果選擇了點擊Glance 來查看具體的內容的話,Glance和MainApp是通過Handoff來實現通信的,我們可以在入口的控制器中的:
- (void)handleUserActivity:(NSDictionary *)userInfo;
這個方法中去將WKOpenType賦值成WKOpenForGlance。
當然了,如果是從Notification來的,我們完全可以通過:
- (void)handleActionWithIdentifier:(NSString *) identifier forRemoteNotification:(NSDictionary *)remoteNotification;
這個方法來根據具體的用戶點擊的動作來區分不同的打開方式。
這裡比較難處理的是,如果用戶是從Glance進來的,退出這個控制器,還是要顯示那六個頁面的,這裡我的解決方法是注冊通知。在出來的控制器中的 - (void)didDeactivate;方法中post出來通知,來讓主控制器重新打開六個Page頁面。Notification同Glance。
因為程序中多處用到了下邊這個方法,因此主程序和Watch App 聯合調試就顯得非常必要了,在Xcode的一個新beta 的release note中蘋果介紹了一種方法。
+ (BOOL)openParentApplication:(NSDictionary *)userInfo reply: (void(^)(NSDictionary *replyInfo, NSError *error)) reply;
完成以上三個步驟,主程序和手表程序上的端點都可以進行調試。
在開發初期,我是在extension中進行數據的申請,這樣嘗試了一段時間之後發現性能上優化的空間不大,而且寫出了很多重復的代碼。復用項目中 已有的代碼是我最好的選擇,尤其是一些第三方用pod管理的庫,但是考慮到公司的項目已經是非常成熟的了,一些管理的第三方庫無法正常的使用,進而又去考 慮寫一個共用的框架,由於時間問題,項目有點大,抽筋抽骨的不是很合適,所以決定充分發揮openParent這個方法,將申請數據這塊放在主程序中,順 便將所有需要“問”主程序的東西全部整理到一個類中,這樣就可以充分發揮老代碼的作用。
數據策略大致如下:首先為了優化Watch App 的啟動速度,采用後台申請數據存起來,Watch每次去使用就可以了,最後處理一下冷啟動的問題,這種情況是當安裝了我們的軟件,沒有在iPhone上打 開過,直接打開Watch上的程序的時候已然有數據,這麼做的話除了第一次會啟動的稍微慢一點點之外,剩下的啟動速度就會快很多。
具體用到的方法是:
- (void)application:(UIApplication *)application performFetchWithCompletionHandler: (void (^)(UIBackgroundFetchResult result))completionHandler NS_AVAILABLE_IOS(7_0);
我和同事做到這裡的時候,就感覺是一個iPhone當做了服務器,而Watch則是一個終端,有什麼需要的數據,我們兩個人設計好協議,通過 openparent這個方法溝通。比如說,軟件運行當中如果想要知道一個用戶是否登錄了,因為沒有登錄是沒有某些功能的,那麼這個時候通過 openparent咨詢一下isLogin就好,判斷一下是否登錄。
Demo中 watch 端代碼實現如下:
[WKInterfaceController openParentApplication:@{@"type":@"isLogin"} reply:^(NSDictionary *replyInfo, NSError *error) {}
Demo中 iphone 服務端代碼實現如下:
#pragma mark - WatchKit Data -(void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply { NSString *type = userInfo[@"type"]; NSDictionary *para = userInfo[@"para"]; ... ... NSDictionary *replyInfo; if ([type isEqualToString:@"isLogin"]) { int random = arc4random()%10 + 1; NSString *whetherLogin = @""; if (random == 1) { whetherLogin = @"YES"; }else { whetherLogin =@"NO"; } replyInfo = @{@"whetherLogin":whetherLogin}; } else if ([type isEqualToString:@"isFavorite"]) { ... ... ... reply(replyInfo); }
Demo中有三種協議,分別是是否登錄,回復信息,是否收藏,當然都是假的,根據項目需求來進行改變,務必注意的是每一種情況都要回調reply(replyInfo);,否則這個方法實際上會響應失敗。
而實際上,項目當中需要在Watch上顯示很多圖片的,這個就需要異步的申請一下,首要想到的還是SDWebImage這個經典框架,這裡就可以在openParent裡使用將data請求到,然後返回給Watch。
PS:最後的最後,我們發現使用App Group來通信數據更加的有效率,因此一部分數據的請求采用了App Group來實現。
在SDK發布的初期,我以為新控件之一WKInterfaceGroup可以點擊,因為目前來看watch上是沒有圖層的概念的,復雜的UI布局是 相當困難的,布局方式和之前有很大的區別,包括在故事板中的布局方法。當初為了實現產品給過來的UI布局也是腦洞大開啊,比如各種嵌套Group,為了要 實現demo中主頁的這種感覺,我很自然的想到了,放一個group,背景放圖片,其他控件放在group上就好了,解決了無法實現控件在控件之上的問 題。但是這就需要group可以點擊,盼星星盼月亮之後,Xcode6.2正式版出來之後徹底斷了我這個念頭,沒辦法,只能通過另一個控件 WKInterfaceTable來實現了,每一頁只有一行不就可以了麼,只能這麼干了。
WKInterfaceTable和UITableView使用上還是有一些不同的,也比UITableView的使用方便了很多。
首先你需要去定義一個Row類,這個Row類相當於一個cell,在這個Row上去布局,如果你的表格中呈現數據的方式不一樣,那就要定義不同的Row類。
定義好之後,調用的時候需要使用如下方法:
#pragma mark - UI - (void)setUpUI { [self.newsRowTabel setNumberOfRows:1 withRowType:@"RowForOneNews"]; for (int i = 0; i < self.newsRowTabel.numberOfRows; i++) { JRWKNewsRow *newsRow = [self.newsRowTabel rowControllerAtIndex:i]; [newsRow.newsCategory setText:[NSString stringWithFormat:@"第%ld張",_index+1]]; ... ... } }
RowType唯一標識了一個Row類,這裡我設置了只有一行,期間設置Row類中每一個屬性的UI數據。
響應點擊事件需要去實現:
#pragma mark - Table Row Select -(void)table:(WKInterfaceTable *)table didSelectRowAtIndex:(NSInteger)rowIndex { NSDictionary *contextDic = @{@"PicName":_picName,@"index":[NSNumber numberWithInteger:_index]}; [self presentControllerWithName:WKNEWSDETAILCONTROLLERIDENTIFIER context:contextDic]; }
這裡去指定具體要呈現出來的是哪一個Controller。
如果表格中的一行不能點擊的話,在故事板中設定的時候把selectable勾選掉就可以了。
API中的幾個關於Controller切換的方法當中幾乎都有context參數,也就是說傳遞數據由我們決定了。在十二月份剛開始寫程序的時 候,我傳遞的是一個很大的字典,發現在程序啟動的時候非常的慢,後來決定寫一個模型管理類,controller之間只需要傳遞一個index就可以了。 在demo中保留了完整的類。
HandOff在iOS8之後出現,著實是為了Apple Watch量身打造的好麼,實在是太應景了,因此在Watch上合理的運用handoff 是一個順理成章的事情,而WKInterfaceController也帶上了相關的一些方法,實際上是要比iphone上的簡單易用一些的。
另一方面,在Glance界面,進入到主App上的時候,handoff也起了決定性的作用,通過handoff將具體的信息交給主App去處理。
主要有兩個Api,這個是update了全局的Activity,將我們需要傳遞的信息打包成一個userinfo即可。
- (void)updateUserActivity:(NSString *)type userInfo: (NSDictionary *)userInfo webpageURL:(NSURL *)webpageURL;
下面這個我還記得是開發者watchkit論壇裡有一位開發者問過這個問題,在watchkit裡怎麼沒有干掉Activity這一個方法。後來蘋 果的工程師估計是采納了。但實際的效果來看,這個方法作用不大,例如在公司的項目中,幾乎每一個頁面都是需要handoff的,給它invalidate 之後,iphone左下角出現logo就會出現異常甚至是不出現的情況。因此如果不是已經很明確的話,輕易的不要用這個方法。
- (void)invalidateUserActivity;
總之,Handoff是Watch和iPhone溝通的絕佳方式之一,蘋果也一直很鼓勵使用SDK新出的一些東西來補充自己的App的。不要再幻想(至少是現在)通過Watch上的一個按鈕能夠使得iPhone 上的Host App 能夠打開並且顯示在前台了。
(1).dynamic notification中蘋果是希望用戶在通知中就把所有的信息都看完的,而不希望用戶點擊內容本身(實際上也是不能點擊的)再進入到Watch app 內查看這個通知的內容的,恰恰相反的是,glance 的交互理念是相反的,也就是蘋果估計用戶點擊glance頁面本身(實際上是可以點擊的)進入到Watch app中進行繼續深度閱讀的。
(2).關於WKTextInputMode,一開始選擇的是WKTextInputModeAllowAnimatedEmoji,後來發現這個 是動態的大表情,返回的是這個大表情的data,不太適合我們一一對應到iphone上的emoji表情,於是後來切換到了 WKTextInputModeAllowEmoji。而WKTextInputModePlain只是顯示了我們所“推薦的”那些回復文本選項。
typedef NS_ENUM(NSInteger, WKTextInputMode) { WKTextInputModePlain, // text (no emoji) from dictation + suggestions WKTextInputModeAllowEmoji, // text plus non-animated emoji from dictation + suggestions WKTextInputModeAllowAnimatedEmoji, // all text, animated emoji (GIF data) };
(3).- (void)becomeCurrentPage; 這個方法主要是在page based頁面當中,如果第三頁在啟動的時候你想讓它先出來,就要標識好,在awake裡邊獲取到之後,調用這個方法,注意的是,這個第三頁不是立馬就出 現在手表的表盤之上的,而是從第一頁蹦到第二頁,然後再第三頁這樣轉的。
(4).推薦一個很好用的工具,叫做Bezel,它能夠將模擬器中運行的watch app 映射到真實的手表裡,表帶的樣式也分38mm以及42mm,有很多種,可以更好的查看自己的App在真實手表上的樣子。更換表帶也很方便,直接拖著下邊的 某一個樣式到Bezel上就自動換了。舉個例子,在開發的時候曾想左右留邊,但是放在Bezel上就會發現手表自帶黑邊,於是留下的左右邊就是很多余了。
Bezel 下載地址,頁面內包含N多種表帶
從目前來看,手表上出現push應該是隨著手機一起來的,也就是同時去顯示在這兩個設備上,除非一切外力因素,比如手表關閉了抬手查看通知等。在之 前的blog中提到過定義category來區分推送通知,如果沒有定義category的故事板的話,就會在手表上顯示一個系統默認的簡短的通知。上邊 說道,蘋果還是鼓勵在notification中將該閱讀的內容都閱讀完,即使增加按鈕也要是一些比較簡單的操作,比如說一個日程安排的軟件,來了一個 push,一個done,一個delete,加上系統的cancel,就可以了。
我嘗試了在Dynamic notification中申請了一個圖片資源,發現系統就選擇去顯示Static notification,因此在notification controller內進行的任務的能力有限,這個在開發的時候要慎重。
開發的時候,Xcode自動生成的Payload很重要,可以定義多個payload來進行相應的模擬,搭配不同的category,不同的category故事板。
我依然認為Glance 的地位在Watch上是最重要的,至少在第三方獨立app登上Watch前,Glance應該是用戶使用最頻繁的一個功能。因此Glance上要呈現的東 西不能太少,也不能太多,一定要簡明扼要,要呈現出最重要的一些東西。例如說如果自己的App不是以天氣為主的,放一個天氣溫度什麼的就不是很合適,系統 的天氣和地圖軟件還是非常出色的,因此還是在Glance 只體現自己App裡邊獨特的東西最好。
另外,Glance的UI布局是很講究的,如果可以盡量要按照Xcode 給的Upper和Lower的模板進行UI布局。不能使用任何可以操作的空間,例如按鈕這樣的,因為Glance就一頁(可以滾動也是禁止的),有點像是 渲染出來的一張圖片似的,因此加個按鈕是沒有意義的。
同Notification,Glance controller 中進行任務的能力也比較有限,因為眾多的Glance會一同呈現出來,用戶翻騰著每一個app 的Glance,這就要求用戶一掃之後就要呈現出來,一個比較好的解決方法就是Glance要呈現的數據提前的申請好,用的時候拿出來,具體實現的方法也 有很多。比如上邊提到的App Group。
Glance 以及主App的通信是依靠Handoff來實現的,也就是說用戶點擊了Glance這個頁面之後,進入到主App,要做的事情需要根據傳過來的userinfo來決定的,主要就是下邊這個方法。
[self updateUserActivity:XXXXX userInfo:userInfo webpageURL:nil];
在入口controller中實現方法,決定啟動什麼頁面,呈現什麼內容,可以放在willActivate裡邊。記住的是請求數據這塊一定要放在awake裡邊,不要放在willActivate裡邊。
-(void)handleUserActivity:(NSDictionary *)userInfo { wkOpenType = JRWKOpenForGlancedemo; if (userInfo) { NSString *sourceString = [userInfo objectForKey:@"Source"]; NSString *picName = [userInfo objectForKey:@"PicName"]; if ([sourceString isEqualToString:@"Glance"]) { _glancePicName = picName; } } }
根據wkopentype決定啟動頁面。
switch (wkOpenType) { case JRWKOpenForGlancedemo: //glance page break; case JRWKOpenForNotificationdemo: //notification page break; default: [self showPageBaseddemoController]; //默認啟動 break; }
Glance 在demo中的表現形式,demo已經整理好,放在了自己的Github上。
AppleWatchDemo
其實WatchKit的東西真不多,更多的是在一個新的平台遇到的各種問題和bug是最讓人頭疼的。隨著真機的即將到來,開發工作也不再是抹黑前 行,這些都是利好的消息。不知道什麼時候可以有獨立的第三方應用的支持,也不知道WatchKit會豐滿到什麼程度,總之我個人還是很看好Watch的未 來的,畢竟蘋果引領的穿戴設備的頭。
劉瑞,中國科學技術大學蘇州研究院在讀碩士,喜歡科技產品,也喜歡制作開箱、體驗視頻。大三起開始自學iOS開發。