WatchKit

更新

Xcode 6.2 beta 4 之後,就有「+ openParentApplication:reply:」可以呼叫,可以解決大部分的 Watch App Extension 與 Hosting App 之間的溝通問題。這篇大概是 code 6.2 beta 1 推出時寫的。

另外,Hosting App 要通知 Watch App Extension,大概還是得透過 CFNotificationCenter。

原文如下:

要開始嘗試使用 WatchKit 開發 Apple Watch 的相關應用,首先要安裝 Xcode 6.2 Beta 版本與 iOS 8.2 SDK,然後從 File 選單中,選擇 New Target,再從 Apple Watch 分頁中選擇 Watch App。

Xcode 6.2 Beta 版本有個 Bug,如果你原本的專案檔案中,Build Phases 裡頭包含了 Run Script Phase,那麼再加入了 Watch App 之後,Xcode Beta 就會一直當一直當一直當。

所以,在目前 Apple Watch 還沒有上市,蘋果也只提供 Beta 版本的開發工具,只能夠用模擬器模擬運作,一切都還不穩定的狀況下,我們想要先了解 WatchKit 的架構,必須把 Watch App 加到一個沒有 Run Script Phase 的專案中,或是可以把原本專案的 Run Script Phase 砍了,或是開一個新的測試專案。

從 File 選單加入新的 Target 之後,我們可以看到,專案裡頭程式分成了三塊:

  1. 在我們的 App 中,會包入一個叫做 WatchKit Extension 的 bundle,我們可以把它想像成是一種 iOS 系統 plugin
  2. 放在 Apple Watch 上的 Watch App
  3. 我們原本的 App。因為現在 App 中包入了一個 Extension,所以我們叫它 Containing App。

第一眼看到 WatchKit 的架構,會讓人想到某種類似 Web App 的架構。你可能會寫出這樣的 Web App:在你的 Web Server 上,沒有用到任何的動態網頁技術,只放了一些靜態 HTML 檔案,而這些靜態檔案又是沒有內容的,只有一些用來當做 template 的 HTML tag。但是,這些 HTML 檔案裡頭宣告了在完成 onLoad 之後,會去呼叫一些在 JavaScript 程式中的 Controller 物件,這些 Controller 物件再動態決定網頁中會出現哪些內容。

我們可以把 Apple Watch 想像成上面提到的 Web Server,放在上面執行的 Watch App,就只有一個 Story Board 檔案,負責用來呈現 view,至於 controller,則是由 WatchKit extension 負責。在 Story Board 裡頭有很多個我們定義的 page(也可以說就是一種 view),每個 page 有屬於各自的 ID,每個 ID 又會對應到 WatchKit Extension 中的 controller(繼承自 WKInterfaceController)。

流程是,Apple Watch 先載入某個 page 之後,這個 page 就會透過藍牙,呼叫你原本 iOS 裝置上的 WatchKit Extension,處理後續的 Controller 邏輯。在頁面載入完成的時候,會呼叫 Controller 的 willActivate method,就像是我們在 Web App 中,我們可以在發生 onLoad 時做一些事情,而接下來在這個畫面中發生的各種觸控事件,像是按了某個按鈕…等等,都會送到這個 Controller 處理。

在處理 Apple Watch 上的 UI 時,也比較像是在寫網頁的 UI。Apple Watch 上每個 UI 元件繼承自 WKInterfaceObject,包含了像是按鈕、文字標籤、圖片等,我們可以去修改這些 UI 元件的屬性,但是卻沒有辦法繼承這些 UI 物件,override 掉 drawRect:,完全改寫這個物件的繪圖行為。就像是做網頁的時候,我們可以修改某個包在像是 <p> tag 的 DOM 物件的一些屬性,但是卻沒有辦法寫一段 JavScript code 繼承這個 DOM 物件改寫繪圖 method。

光靠 Watch App 與 WatchKit Extension 就可以做一些事情,但我們通常會希望在手錶上的 App,可以跟我們原本的 App 之間有一些互動,你在接觸這套開發環境之前,應該會有這樣的期待,但是這件事情卻是出乎意料的麻煩。

我們容易呼叫我們原本的 App 的管道,Watch App 呼叫的不是我們的 App 本身,而是一個跟原本 App 分開、甚至可以說是隔絕的 WatchKit Extension,而 WatchKit Extension 與 App 之間的溝通方式十分迂迴。你可以想像成,你寫了一個主要以 JavaScript 開發的 Web App,但是 JS code 裡頭卻很難呼叫你更後面的 API server。對,就是這麼彆扭。

蘋果在官方文件中,對 App 與 WatchKit Extension 之間溝通的管道,就只有提到可以共用檔案—其實光是這件事情就有點麻煩,要能夠讓兩者共用檔案,我們也得先去 Apple Developer 網站,把兩邊的 bundle ID 都加入到同一個 App Group 中。但,我們在手錶上觸發了一個觸控事件,我們要怎麼送到 App 上?我們在 App 裡頭做了一件事情,又要怎麼通知手錶?

在 Mac OS X 上面要做這件事情很容易。Mac OS X 本身就提供 Distributed Objects 與 XPC 等 IPC 管道,但是 iOS 上這些都禁止使用,我們要怎樣讓兩個 process 互相溝通?

我們可能想到幾種作法:一般來說,我們在不同的 iOS App 之間溝通,大概會選擇對 UIApplication 呼叫 openURL:,要求指定的 App 處理某個 URL,用 URL 描述我們想要做的事情,並且傳遞相關的參數,可是呢,WatchKit 禁止呼叫 UIApplication。至於交換資料,我們也可能會想用 UIPasteboard,用剪貼簿這一塊 API 來交換資料,WatchKit 也擋住了這條路。

蘋果官方論壇上 iOS App and Extension communication這串討論實在十分歡樂(需要登入蘋果帳號)。裡頭大概提到幾個作法:

首先,有人覺得可以呼叫 iOS 7 之後的 Multipeer Connectivity,把 WatchKit Extension 與 App 都變成網路上的節點,WatchKit Extension 把想要送出的訊息廣播到附近所有的節點。這…光用想的就很糟。你的 App 有機會可以收到訊息,但你的 App 與 WatchKit Extension 雖然在同一台機器上,卻不見得會成功建立連線,而周圍的其他裝置,也有機會會收到你送出的訊息,而你應該不希望發生這種事情。

如果你想讓你的 App 變成一個 Server,讓 WatchKit Extension 變成 client,兩者之間用 Client/Server 的方式溝通,似乎…聽起來很扯,但好像可以作用。但我們會期待兩者的溝通會是雙向的,在 App 發生變化的時候,WatchKit Extension 這邊也發生對應的事件—所以我們也在 WatchKit Extension 這邊也放個 Server?這樣越來越扯了。

另外一種想法是,兩邊都寫入/讀取某一個共用的檔案,並且都監控這個檔案的變化,當一方寫入檔案的時候,另外一邊由於有監控這個檔案的改變,就從檔案中讀取新的資料。

我們想要讓不同的 process 都讀取/寫入同一個檔案,我們會想用 NSFileCoordinator,因為當兩個 process 同時想寫入同一個檔案的時候,會把檔案寫爛,而 NSFileCoordinator 是一個 queue,負責把各種讀取/寫入的工作排入 queue 中,再一個一個執行,當檔案改動的時候,。

NSFileCoordinator 是在 iOS 5 推出時,用來處理 iCloud 文件同步而設計的,不過蘋果在 TN2408 中,卻不怎麼建議像 WatchKit Extension 這類的 App Extension,透過 NSFileCoordinator 與 Containing App 溝通。理由是,Containing App 因為進入背景而 suspend 的時候,這套機制可能會產生 deadlock,蘋果建議改用 atomic 方式寫入檔案,或是使用 CFPreferences、SQLite 或 Core Data 等。

不過,用這幾種方式寫入檔案,WatchKit Extension 與 Containing App 之間,還是不知道對方是不是改變了檔案內容,所以在寫入檔案後,可以再用 CFNotificationCenter 互相通知。

Be Sociable, Share!

One thought on “WatchKit

  1. Pingback: [iOS] Watch App Architecture | 逍遙文工作室

Leave a Reply