之前跟兩個同事一起用業餘時間給我們的 Glow App 做了 Apple Watch 的應用。寫這篇文章來對 Apple Watch 的開發做個介紹,也列出開發過程中遇到的一些坑。雖然 Watch OS 2 已經出來,而我們是用 WatchKit 進行的開發,但很多內容也適用於 Watch OS 2。希望這篇文章對大家有幫助。

Introduction

  • Design
  • WatchKit App Architecture
  • Data Communication
  • Provisioning Profiles and Entitlements
  • Tips

Design

本質上,你可以把 Apple Watch 當作 iPhone 的一個擴充套件螢幕。你不需要掏出手機。只需要稍稍擡一下手腕,就可以獲取資訊,或做一些簡單的操作。實際上,在你開發 Watch App 的時候,你就會發現 Watch 的模擬器就是當作 iPhone 模擬器的一個 external display 實現的。

不過 Apple Watch 展現了全新的人機互動方式,iOS App 的設計互動準則在 Watch 上並不適用。因此在設計開發 Watch App 之前,有必要先理解它的互動和基本的 UI 元素。

首先說互動。除了熟悉的手勢互動,Apple Watch 提供了 3 種新的互動方式:

  1. Force Touch
    Apple Watch 的顯示屏在感知使用者點選的同時,也能感知壓力。通過「重按」可以顯示最多有 4 個操作的上下文選單。

Force Touch

  1. The Digital Crown(數碼錶冠)
    跟傳統手錶一樣,錶冠是最常用的互動。但在 Apple Watch 上,錶冠不是用來調校時間日期,或上弦。通過轉動 Digital Crown,可以在不會遮擋視線的情況下,精確地放大縮放、滾動、或選擇資料。它作為按鈕還有返回的功能,按下返回主螢幕,按兩下回到時鐘介面。

聽起來很美,但目前錶冠的 API 還沒有開放,滾動都是系統自動幫你做的 :[

  1. Side Button
    錶冠下面的一個長長的按鈕。按它會把你帶到 Friends 介面。在這裡,你可以給你選擇好的 12 個聯絡人打電話,發簡訊,或者 Watch 提供的新的交流方式,例如輕點他們一下,畫個塗鴉,或是傳送心跳。

Side Button

恩,這也沒有開放相關的 API,考慮到它聯絡人的功能,估計之後也不會開放。

Watchkit App Architecture

當你新增一個 Watchkit App target 的時候,你會發現 Xcode 實際上給添加了 2 個新的 executables,並同你的 iOS App 打包在一起編譯。

Targets

他們之間的依賴關係如下圖所示,Watch App 依賴於 Watchkit Extension,而 Watchkit Extension 依賴於 iOS App. 從上面下面兩張圖都可以看到,Watch App 裡只有 Storyboard 和 ImageAssets。沒錯,Watch OS 1 裡,Watch App 只包含一些靜態的東西,Extension 是真正執行程式碼的地方。Extension 負責從 iOS App 那裡獲得資料,並控制 Watch App 介面上要顯示什麼。而 Watch App 的操作也是由 Extension 向 iOS App 發起請求。Watch App 不直接與 iOS App 交流。

Targets structre

Watch App 的每一個頁面,都需要有一個對應的 WKInterfaceController 的子類。如上圖 Extension 的資料夾的 InterfaceController 和 GlanceInterfaceController。WKInterfaceController 除了 init 之外,還有 3 個與生命週期有關的方法:

// 在 init 之後被呼叫,可以在這個方法裡配置介面上的元素
- (void)awakeWithContext:(id)context;

// 當這個頁面將要顯示給使用者的時候被呼叫
- (void)willActivate;

// 當這個頁面不再顯示的時候被呼叫
- (void)didDeactivate;

Data Communication

前面說到 Watch App 本身只包含一些靜態內容,它自己不儲存資料,也無法傳送網路請求。它只能藉由 Extension 與 iOS App 互動。所以 Watch App 與 iOS App 的資料傳遞是關鍵,也是大部分 Watch App 的主要開發工作。資料傳遞的方法主要有下面 5 種。第一種是使用 WKInterfaceController 提供的 openParentApplication:reply,然後在 iOS 端 實現 application:handleWatchKitExtensionRequest:reply 來處理 Watch Extension 發來的請求。最後一種 Wormhole 是第三方的一個庫,通過 Dawrin notification center 傳送訊息並捎帶上資料。而中間三種都是通過 App Group,在獨立的共享沙盒裡傳遞資料。

  • WKInterfaceController openParentApplication:reply
  • NSUserDefaults
  • Core Data
  • NSFileManager
  • Dawrin notification center - MMWormhole

WKInterfaceController openParentApplication:reply

這種方法很直觀,也是幾種資料傳遞方式中最實時可靠的。你可以用 Enum 定義幾種請求的型別,然後在傳送請求的時候把請求型別一併傳過去,這樣 iOS App 收到請求時,就能知道要做什麼。iOS App 用 reply 回撥把請求結果傳回去。

用這種方法,iOS App 即使在後臺也能被喚起。但 iOS App 不能主動去喚起 Watch Extension。

NSDictionary *request = @{kRequestType: @(requestType)};

[InterfaceController openParentApplication:request
                    reply:^(NSDictionary *replyInfo, NSError *error) {

}];
- (void)application:(UIApplication *)application 
handleWatchKitExtensionRequest:(NSDictionary *)userInfo  
                         reply:(void (^)(NSDictionary *))reply
{
    RequestType requestType = userInfo[kRequestType];
    if (requestType == RequestTypeRefreshWatchData) {
        //
    }
}

中間三種方式很類似,都是把資料存在一個獨立的共享沙盒中,不同是他們的存放方式。iOS App 或者 Watch App 需要資料了,就去找沙盒裡面找。就像一個祕密的信箱,只有他們倆知道這在哪兒。所以這也是非同步的傳遞方式,雙方不直接打交道。具體怎麼用看下面程式碼吧。

NSUserDefaults

用 NSUserDefaults 最簡單,但有資料大小的限制。

NSString *appGroupName = @"group.com.yourcompnay.shared";  
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:appGroupName];

[defaults setObject:user.name forKey:@"userName"];

Core Data

如果你的 iOS App 已經把 Core Data 放到共享沙盒裡了,那可以考慮這種方法。

NSString *appGroupName = @"group.com.yourcompnay.shared";  
NSURL *storeURL = [[NSFileManager defaultManager]  
    containerURLForSecurityApplicationGroupIdentifier:appGroupName];
storeURL = [storeURL URLByAppendingPathComponent:@"glow.sqlite"];

[self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType 
                                              configuration:nil 
                                                        URL:storeURL 
                                                        options:nil 
                                                        error:&error]

NSFileManager && NSFileCoordinator

檔案讀寫必然要涉及到多執行緒問題,不過不用擔心,用 NSFileCoordinator 就可以了。

- coordinateReadingItemAtURL:options:error:byAccessor:
- coordinateWritingItemAtURL:options:error:byAccessor:

[coordinator coordinateWritingItemAtURL:fileURL 
                                options:nil 
                                  error:nil
                             byAccessor:^(NSURL* writingURL) {
   [dataToSave writeToURL:newURL atomically:true];
}];

NSFilePresenter

你還可以通過實現 NSFilePresenter 協議來監聽檔案的更改,不需要自己實現重新整理機制就能免費獲得實時更新。

- presentedItemDidChange

Dawrin notification - MMWormhole

最後一種用起來也很方便,Watch Extension 和 iOS App 一方傳送訊息,一方監聽訊息。而且還有一大優勢是,Wormhole 會儲存上次傳遞的資料,這樣在 Watch App 喚醒的時候,可以先使用 Wormhole 裡的資料,等 iOS App 傳來最新的資料時,再更新介面。

// init
self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:kApplicationGroupIdentifier  
                                               optionalDirectory:kWormholeDirectory];

// iOS app
NSDictionary *message = [self makeWatchData];  
[self.wormhole passMessageObject:message identifier:kWormholeWatchData];

// WatchKit Extension
NSDictionary *message = [self.wormhole messageWithIdentifier:kWormholeWatchData];  
// do something with message

[self.wormhole listenForMessageWithIdentifier:kWormholeWatchData
                               listener:^(id messageObject) {
    NSLog(@"Received data from wormhole.");
}];

也是我開發最初使用的方式。但在我使用的過程中,發現如果 iOS App 是在後臺模式,就並不能實時接收到 WatchKit Extension 發來的訊息。所以最後,我們選擇openParentApplication:reply 和 Wormhole 的混用。在 Watch App 喚醒時,使用 Wormhole 裡的資料,保證 Watch App 響應的速度,同時用 openParentApplication:reply 向 iOS 請求最新的更新。

Provisioning Profiles and Entitlements

開發之初,最讓人頭疼的可能就是 Code Signing, Provisioning 和 entitlements 這些東西了。

每一個 target 都要有自己的 App ID。所以我們一共需要有三個:

  • yourAppID
  • yourAppID.watchkitextension
  • yourAppID.watchkitapp

你還需要給每個 App ID 建立一個相關聯的 Provisioning Profile。如果你用 Xcode 自動建立 Provisioning Profile,它只會給你建立前面兩個,你需要自己去 developer center 裡手動建立。

另外,你還需要確保你的三個 Entitlements 都是對的。Version Number、Build Number、以及 App Groups (如果使用的話) 都必須是一樣的,不然編譯就不能通過。

Tips

Debug

有時候,你會需要同時 debug iOS App 和 Watch App。但 Xcode 只允許你指定一個 target 執行,你要麼 debug iOS App 的程式碼,要麼 Watch App 的程式碼。但通過 Xcode 的 Attach to Process 就能同時 debug。具體步驟如下:

  • 執行 WatchKit App
  • 在 Simulator 中開啟你的 iOS App
  • 在 Xcode 的選單欄上 Debug -> Attach to Process,選擇你的 iOS App 就能同時 debug iOS 跟 WatchKit app 了。

App Icons and iTunes Connect

如果在上傳你的應用到 iTunes Connect 的時候,遇到 Invalid Binary 的錯誤。很大可能是因為你的 Watch App 的 icon 裡有透明層或者 alpha 通道。一個比較方便的解決辦法是,用 Preview 開啟圖片,選擇匯出(export),然後不要勾選底部的 Alpha 選項,確定。

End

謝謝觀賞。