天天看點

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

apple watch 和 watchos 第一代産品隻允許使用者在 iphone 裝置上進行計算,然後将結果傳輸到手表上進行顯示。在這個架構下,手表充當的功能在很大程度上隻是手機的另一塊小一些的顯示器。而在 watchos 2 中,apple 開放了在手表端直接進行計算的能力,一些之前無法完成的 app 現在也可以進行建構了。本文将通過一個很簡單的天氣 app 的例子,講解一下 watchos 2 中新引入的一些特性的使用方法。

本文是我的 wwdc15 筆記中的一篇,在 wwdc15 中涉及到 watchos 2 的相關内容的 session 非常多,本文所參考的有:

introducing watchkit for watchos 2

watchkit in-depth, part 1

watchkit in-depth, part 2

introducing watch connectivity

building watch apps

creating complications with clockkit

作為一個示例項目,我們就來建構一個最簡單的天氣 app 吧。本文将一步步帶你從零開始建構一個相對完整的 ios + watch app。這個 app 的 ios 端很簡單,從資料源取到資料,然後解析成天氣的 model 後,通過一個 pageviewcontroller 顯示出來。為了讓 demo 更有說服力,我們将展示目前日期以及前後兩天的天氣情況,包括天氣狀況和氣溫。在手表端,我們希望建構一個類似的 app,可以展示這幾天的天氣情況。另外我們當然也介紹如何利用 watchos 2 的一些新特性,比如 complications 和 time travel 等等。

雖然本文的重點是 watchos,但是為了完整性,我們還是從開頭開始來建構這個 app 吧。因為不管是 watchos 1 還是 2,一個手表 app 都是無法脫離手機 app 單獨存在和申請的。是以我們首先來做的是一個像模像樣的 ios app 吧。

第一步當然是使用 xcode 7 建立一個工程,這裡我們直接選擇 ios app with watchkit app,這樣 xcode 将直接幫助我們建立一個帶有 watchos app 的 ios 應用。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

在接下來的畫面中,我們選中 include complication 選項,因為我們希望制作一個包含有 complication 的 watch app。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

這個 app 的 ui 部分比較簡單,我将使用到的素材都放到了這裡。你可以下載下傳這些素材,并把它們解壓并拖拽到項目 ios app 的 assets.xcassets 裡去:

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

接下來,我們來建構 ui 部分。我們想要使用 pageviewcontroller 來作為 app 的導航,首先,在 main.storyboard 中删掉原來的 viewcontroller,并新加一個 page view controller,然後在它的 attributes inspector 中将 transition style 改為 scroll,并勾選上 is initial view controller。這将使這個 view controller 成為整個 app 的入口。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

接下來,我們需要将這個 page view controller 和代碼關聯起來。首先将 viewcontroller.swift 檔案中,将 viewcontroller 的繼承關系從 <code>uiviewcontroller</code> 改為 <code>uipageviewcontroller</code>。

然後我們就可以在 storyboard 檔案中将剛才的 page view controller 的 class 改為我們的<code>viewcontroller</code> 了。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

另外我們還需要一個實際展示天氣的 view controller。建立一個繼承自 <code>uiviewcontroller</code> 的<code>weatherviewcontroller</code>,然後将 weatherviewcontroller.swift 的内容替換為:

這裡僅隻是定義了一個 <code>day</code> 的枚舉,它将用來标記這個 <code>weatherviewcontroller</code> 所代表的日期 (可能你會說把 <code>day</code> 在 viewcontroller 裡并不是很好的選擇,沒錯,但是放在這裡有助于我們快速搭建 app,在之後我們會對此進行重構)。接下來,我們在 storyboard 中添加一個 viewcontroller,并将它的 class 改為 <code>weatherviewcontroller</code>。我們可以在這裡建構 ui,對于這個 demo 來說,一個簡單的背景,加上表示天氣的圖示和表示溫度的标簽就足夠了。因為這裡并不是一個關于 auto layout 或是 size class 的 demo,是以就不詳細一步步地做了,我随意拖了拖 ui 和限制,最後結果如下圖所示。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

接下來就是從 storyboard 中把需要的 iboutlet 拖出來。我們需要天氣圖示,最高最低溫度的 label。完成這些 ui 工作之後的項目可以在 github 的這個 tag 下找到,如果你不想自己完成這些步驟的話,也可以直接使用這個 tag 的源檔案來繼續下面的 demo。當然,如果你對 autolayout 和 interface builder 還不熟悉的話,這會是一個很好的機會來從簡單的布局入手,去了解對 ib 的使用。關于更多 ib 和 storyboard 的教程,推薦 raywenderlich 的這兩篇系列文章:storyboards tutorial in swift 和 auto layout tutoria。

然後我們可以考慮先把 page view controller 的架構實作出來。在 <code>viewcontroller.swift</code> 中,我們首先在 <code>viewcontroller</code> 類中加入以下方法:

這将從目前的 stroyboard 裡尋找 id 為 "weatherviewcontroller" 的 viewcontroller,并且初始化它。我們希望能為每一天的天氣顯示一個 title,一個比較理想的做法就是直接将我們的 weatherviewcontroller 嵌套在 navigation controller 裡,這樣我們就可以直接使用 navigation bar 來顯示标題,而不用去操心它的布局了。我們剛才并沒有為 <code>weatherviewcontroller</code> 指定 id,在 storyboard 中,找到 weatherviewcontroller,然後在 identity 裡添加即可:

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

接下來我們來實作 <code>uipageviewcontrollerdatasource</code>。在 <code>viewcontroller.swift</code> 的<code>viewdidload</code> 裡加入:

首先它将 <code>viewcontroller</code> 自己設定為 datasource。然後設定了初始需要表示的 viewcontroller。對于 <code>uipageviewcontrollerdatasource</code> 的實作,我們在同一檔案中加入一個<code>viewcontroller</code> 的 extension 來搞定:

這兩個方法分别根據輸入的 view controller 對象來确定前一個和後一個 view controller,如果傳回 <code>nil</code> 則說明沒有之前/後的頁面了。另外,我們可能還想要先将 title 顯示出來,以确定現在的架構是否正确工作。在 <code>weatherviewcontroller.swift</code> 的 day 枚舉裡添加如下屬性:

然後将 <code>day</code> 屬性改為:

運作 app,現在我們應該可以在五個頁面之間進行切換了。你也可以從 github 上對應的 tag 中下載下傳到目前為止的項目。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

很難有人一次性就把代碼寫得完美無瑕,這也是重構的意義。重構從來不是一個“等待項目完成後再開始”的活動,而是應該随着項目的展開和進行,一旦發現有可能存在問題的地方,就盡快進行改進。比如在上面我們将 <code>day</code> 放在了 <code>weatherviewcontroller</code> 中,這顯然不是一個很好地選擇。這個枚舉更接近于 model 層的東西而非控制層,我們應該将它遷移到另外的地方。同樣現在還需要實作的還有天氣的 model,即表征天氣狀況和高低溫度的對象。我們将這些内容提取出來,放到一個 framework 中去,以便使用的維護。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

我們首先對現有的 <code>day</code> 進行遷移。建立一個新的 cocoa touch framework target,命名為<code>watchweatherkit</code>。在這個 target 中建立 <code>day.swift</code> 檔案,其中内容為:

這就是原來存在于 <code>weatherviewcontroller</code> 中的代碼,隻不過将必要的内容申明為了 <code>public</code>,這樣我們才能在别的 target 中使用它們。我們現在可以将原來的 day 整個删除掉了,接下來,我們在<code>weatherviewcontroller.swift</code> 和 <code>viewcontroller.swift</code> 最上面加入 <code>import watchweatherkit</code>,并将 <code>weatherviewcontroller.day</code> 改為 <code>day</code>。現在 <code>day</code> 枚舉就被隔離出 view controller 了。

然後實作天氣的 model。在 <code>watchweatherkit</code> 裡建立 <code>weather.swift</code>,并書寫如下代碼:

model 包含了天氣的狀态資訊和最高最低溫度,我們稍後會用一個 json 字元串中拿到字典,然後初始化它。如果字典中資訊不全的話将直接傳回 <code>nil</code> 表示天氣對象建立失敗。到此為止的項目可以在 github 的 model tag 中找到。

接下來的任務是擷取天氣的 json,作為一個 demo 我們完全可以用一個本地檔案替代網絡請求部分。不過因為之後在介紹 watch app 時會用到使用手表進行網絡請求,是以這裡我們還是從網絡來擷取天氣資訊。為了簡單,假設我們從伺服器收到的 json 是這個樣子的:

其中 <code>day</code> 0 表示今天,<code>state</code> 是天氣狀況的代碼。

我們已經有 <code>weather</code> 這個 model 類型了,現在我們需要一個 api client 來擷取這個資訊。在<code>weatherwatchkit</code> target 中建立一個檔案 <code>weatherclient.swift</code>,并填寫以下代碼:

其實我們的 client 現在有點過度封裝和耦合,不過作為 demo 來說的話還不錯。它現在隻有一個方法,就是從網絡源請求一個 json 然後進行解析。解析的代碼 <code>parseweatherresult</code> 我們放在了<code>weather</code> 中,以一個 extension 的形式存在:

我們在 viewcontroller 中使用這個方法即可擷取到天氣資訊,就可以建構我們的 ui 了。在<code>viewcontroller.swift</code> 中,加入一個屬性來存儲天氣資料:

然後更改 <code>viewdidload</code> 的代碼:

在這裡一開始使用了一個臨時的 <code>uiviewcontroller</code> 來作為 pageviewcontroller 在網絡請求時的初始視圖控制 (雖然在我們的例子中這個初始視圖就是一塊白螢幕)。接下來進行網絡請求,并把得到的資料存儲在 <code>data</code> 變量中以待使用。之後我們需要把這些資料傳遞給不同日期的 viewcontroller,在 <code>weatherviewcontrollerforday</code> 方法中,換為對 weather 做設定,而非<code>day</code>:

同時我們還需要修改一下 <code>weatherviewcontroller</code>,将原來的:

改為

另外還需要在 <code>uipageviewcontrollerdatasource</code> 的兩個方法中,把對應的 <code>viewcontroller.day</code>換為 <code>viewcontroller.weather?.day</code>。最後我們要做的是在 <code>weatherviewcontroller</code> 的<code>viewdidload</code> 中根據 model 更新 ui:

一個可能的改進是建立一個 <code>weatherviewmodel</code> 來将對 view 的内容和 model 的映射關系代碼從 viewcontroller 裡分理出去,如果有興趣的話你可以自己研究下。

到此我們的 ios 端的代碼就全部完成了,運作一下看看,perfect!到現在為止的項目可以在這裡找到。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

終于進入正題了,我們可以開始設計和制作 watch app 了。

首先我們把需要的圖檔添加到 watch app target 的 assets.xcassets 中,這樣在之後使用者安裝 app 時這些圖檔将被存放在手表中,我們可以直接快速地從手表本地讀取。ui 的設計非常簡單,在 watch app 的 interface.storyboard 中,我們先将代表天氣狀态的圖檔和溫度标簽拖拽到 interfacecontroller 中,并将它們連接配接到 <code>interfacecontroller.swift</code> 中的 iboutlet 去。

接下來,我們将它複制四次,并用 next page 的 segue 串聯起來,并設定它們的 title。這樣,在最後的 watch app 裡我們就會有五個可以左右 scorll 滑動的頁面,分别表示從前天到後天的五個日子。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

為了标記和區分這五個 interfacecontroller 執行個體。因為使用 next page 級聯的 wkinterfacecontroller 會被依次建立,是以我們可以在 <code>awakewithcontext</code> 方法中用一個靜态變量計數。在這裡,我們想要将序号為 2 的 interfacecontroller (也就是代表 “今天” 的那個) 設為目前 page。在 <code>interfacecontroller.swift</code> 裡添加一個靜态變量:

然後在 <code>awakewithcontext</code> 方法中加入:

和 ios app 類似,我們希望能夠使用架構來組織代碼。watch app 中的天氣 model 和網絡請求部分的内容其實和 ios app 中的是完全一樣的,我們沒有理由重複開發。在一個 watch app 中,其實 app 本身隻負責圖形顯示,實際的代碼都是在 extension 中的。在 watchos 2 之前,因為 extension 是在手機端,和 ios app 處于同樣的實體裝置中,是以我們可以簡單地将為 ios app 中建立的架構使用在 watch extension target 中。但是在 watchos 2 中發生了變化,因為 extension 現在直接将運作在手表上,我們無法與 ios app 共享同一個架構了。取而代之,我們需要為手表 app 建立新的屬于自己的 framewok,然後将合适的檔案添加到這個 framework 中去。

為項目建立一個 target,類型選擇為 watch os 的 watch framework。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

接下來,我們把之前的 <code>day.swift</code>,<code>weather.swift</code> 和 <code>weatherclient.swift</code> 三個檔案添加到這個新的 target (在這裡我們叫它 watchweatherwatchkit) 裡去。我們将在新的這個 watch framework 中重用這三個檔案。這樣做相較于直接把這三個檔案放到 watch extension target 中來說,會更易于管理組織和子產品分割,也是 apple 所推薦的使用方式。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

接下來我們需要手動在 watch extension 裡将這個新的 framework 連結進來。在 <code>watchweather watchkit extension</code> target 的 general 頁面中,将 <code>watchweatherwatchkit</code> 添加到 embedded binaries 中。xcode 将會自動把它加到 link binary with libraries 裡去。這時候如果你嘗試編譯 watch app,可能會得到一個警告:"linking against dylib not safe for use in application extensions"。這是因為不論是 ios app 的 extension 還是 watchos 的 extension,所能使用的 api 都隻是完整 ios sdk 的子集。編譯器無法确定我們所動态連結的架構是否含有一些 extension 無法調用的 api。要解決這個警告,我們可以通過在 <code>watchweatherwatchkit</code> 的 build setting 中将 "require only app-extension-safe api" 設定為 <code>yes</code> 來将 target 裡可用的 api 限制在 extension 中。

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

是時候來實作我們的 app 了。首先一刻都不能再忍受的是 <code>interfacecontroller.swift</code> 中的<code>index</code>。我們既然有了 <code>watchweatherwatchkit</code>,就可以利用已有的模型将這裡寫得更清楚。在<code>interfacecontroller.swift</code> 中,首先在檔案上面 <code>import watchweatherwatchkit</code>,然後修改<code>index</code> 的定義,并添加一個字典來臨時儲存這些 interface controller,以便之後使用:

将剛才我們的在 <code>awakewithcontext</code> 中添加的内容删掉,改為:

現在表意就要清楚不少了。

接下來就是擷取天氣資訊了。和 ios app 中一樣,我們可以直接使用 <code>weatherclient</code> 來擷取。在<code>interfacecontroller.swift</code> 中加入以下代碼:

如果我們擷取到了天氣,就設定 <code>weather</code> 屬性并調用 <code>updateweather</code> 方法依次對相應的 interfacecontroller 的 ui 進行設定。如果出現了錯誤,我們這裡簡單地用一個 watchos 2 中新加的 alert view 來進行提示并讓使用者重試。在這個方法的下面加上更新 ui 的方法 <code>updateweather</code>:

我們隻需要網絡請求進行一次就可以了,是以在這裡我們用一個 once_token 來限定一開始的 request 隻執行一次。在 <code>interfacecontroller.swift</code> 中加上一個類變量:

然後在 <code>awakewithcontext</code> 的最後用 <code>dispatch_once</code> 來開始請求:

最後,在 <code>willactivate</code> 中也需要重新整理 ui:

應該就這麼多了。標明手表 scheme,運作程式,除了圖示的尺寸不太對以及網絡請求時還顯示預設的天氣狀況和溫度以外,其他的看起來還不賴:

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

至于顯示預設值的問題,我們可以通過簡單地在 storyboard 中将圖和标簽内容設為空來改善,在此就不再贅述了。

值得一提的是,如果你多測試幾次,比如關閉整個 app (或者模拟器),然後再運作的話,可能會有一定幾率遇到下面這樣的錯誤:

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

如果你還記得的話,這個 1000 錯誤就是我們定義在 <code>weatherclient.swift</code> 中的 <code>corruptedjson</code>錯誤。調試一下,你就會發現在請求傳回時得到的資料存在問題,會得到一個内容被完整複制了一遍的傳回 (比如正确的資料 {a:1},但是我們得到的是 {a:1} {a:1})。雖然我不是太明白為什麼會出現這樣的狀況,但這應該是 <code>nsurlsession</code> 在 watchos sdk 上的一個緩存上的 bug。我之後會嘗試向 apple 送出一個 radar 來彙報這個問題。現在的話,我們可以通過設定不帶緩存的<code>nsurlsessionconfiguration</code> 來繞開這個問題。将 weatherclient 中的 <code>session</code> 屬性改為以下即可:

至此,我們的 watch app 本體就完成了。到這一步為止的項目可以在這個 tag 找到。notification 和 glance 兩個特性相對簡單,基本隻是界面的制作,為了節省篇幅 (其實這篇文章已經夠長了,如果你需要休息一下的話,這裡會是一個很好地機會),就不再詳細說明了。你可以分别在這裡和這裡找到開發兩者所需要的一切知識。

在下一節中,我們将着重于 watchos 2 的新特性。首先是 complications。

complications 是 watchos 2 新加入的特性,它是表盤上除了時間以外的一些功能性的小部件。比如我們的天氣 app 裡,将今天的天氣狀況顯示在表盤上就是一個非常理想的應用場景,這樣使用者就不需要打開你的 app 就能看到今天的天氣狀況了 (其實今天的天氣的話使用者擡頭望窗外就能知道。如果是一個實際的天氣 app 的話,顯示明天或者兩小時後的天氣狀況會更理想,但是作為 demo 就先這樣吧..)。我們在這一小節中将為剛才的天氣 app 實作一個 complication。

complications 可以是不同的形狀,如圖所示:

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

根據使用者表盤選擇的不同,表盤上對應的可用的 complications 形狀也各不相同。如果你想要你的 complication 在所有表盤上都能使用的話,你需要實作所有的形狀。掌管 complications 或者說是表盤相關的架構并不是我們一直使用的 watchkit,而是一個 watchos 2 中全新架構,clockkit。clockkit 會提供一些模闆給我們,并在一定時間點向我們請求資料。我們依照模闆使用我們的資料來實作 complication,最後 clockkit 負責幫助我們将其渲染在表盤上。在 clockkit 請求資料時,它會喚醒我們的 watch extension。我們需要在 extension 中實作資料源,并以一段時間線的方式把資料提供給 clockkit。這樣做有兩個好處,首先 clockkit 可以一次性擷取到很多資料,這樣它就能在合适的時候更新 complication 的顯示,而不必再次喚醒 extension 來請求資料。其次,因為有一條時間線的資料,我們就可以使用 time travel 來檢視 complication 已經過去的和即将到來的狀況,這在某些場合下會十分友善。

理論已經說了很多了,來實際操作一下吧。

首先,因為我們在建立項目的時候已經選擇了包含 complications,是以我們并不需要再進行額外的配置就可以開始了。如果你不小心沒有選中這個選項,或者是想在已有項目中進行添加的話,你就需要手動配置,在 extension 的 target 裡的 complications configuration 中指定資料源的 class 和支援的形狀。在運作時,系統會使用在這個設定中指定的類型名字去初始化一個的執行個體,然後調用這個執行個體中實作的資料源方法。我們要做的就是在被詢問這些方法時,盡快地提供需要的資料。

第一步是實作資料源,這在在我們的項目中已經配置好了,就是 <code>complicationcontroller.swift</code>。這是一個實作了 <code>clkcomplicationdatasource</code> 的類型,打開檔案可以看到所有的方法都已經有預設空實作了,我們現在要做的就是把這些空填上。其中最關鍵的是<code>getcurrenttimelineentryforcomplication:withhandler:</code>,我們需要通過這個方法來提供目前表盤所要顯示的 complication。羅馬不是一天建成的,項目也不是。我們先提供一個 dummy 的資料來讓流程運作起來。在 complicationcontroller.swift 中,将這個方法的内容換成:

在這個方法中,系統會提供給我們所需要的 complication 的類型,我們要做的是使用合适的系統所提供的模闆 (這裡是 <code>clkcomplicationtemplatemodularsmallsimpleimage</code>) 以及我們自己的資料,來建構一個 <code>clkcomplicationtimelineentry</code> 對象,然後再 handler 中傳回給系統。結合天氣 app 的特點,我們這裡選擇了一個小的簡單圖檔的模闆。另外因為篇幅有限,這裡隻實作了<code>.modularsmall</code>。在實際的項目中,你應該支援盡量多的 complication 類型,這樣可以保證你的使用者在不同的表盤上都能使用。

在提供具體的資料時,我們使用 template 的 <code>imageprovider</code> 或者 <code>textprovider</code>。在我們現在使用的這個模闆中,隻有一個簡單的 <code>imageprovider</code>,我們從 extension 的 assets category 中擷取并設定合适的圖像就可以了 (對于 <code>.modularsmall</code> 來說,需要圖像的尺寸為 52px 和 58px 的 @2x。關于其他模闆的圖像尺寸要求,可以參考文檔)。

運作程式,選取一個帶有 <code>modularsmall</code> complication 的表盤 (如果是在模拟器的話,可以使用 shift+cmd+2 然後點選表盤來打開表盤選擇界面),然後在 complication 中選擇 watchweather,可以看到以下的結果:

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

看起來不錯,我們的小太陽已經在界面上熠熠生輝了,接下來就是要實作把實際的資料替換進來。對于 complication 來說,我們需要以盡可能快的速度去調用 handler 來向系統提供資料。我們并沒有那麼多時間去從網絡上擷取資料,是以需要使用之前在 watch app 或者是 ios app 中擷取到的資料來組織 complication。為了在 complication 中能直接擷取資料,我們需要在用 client 擷取到資料後把它存在本地。這裡我們用 userdefaults 就已經足夠了。在 <code>weather.swift</code> 中加入以下 extension:

這裡我們需要知道擷取到這組資料時的時間,我們以目前時間作為擷取時間進行存儲。一個更加合适的做法應該是在請求的傳回中包含每個天氣狀況所對應的時間資訊。但是因為我們并沒有真正的伺服器,也并非實際的請求,是以這裡就先簡單粗暴地用本地時間了。接下來,在每次請求成功後,我們調用 <code>storeweathersresult</code> 将結果存儲起來。在 <code>weatherclient.swift</code> 中,把

這段代碼改為:

接下來我們還需要另外一項準備工作。complication 的時間線是以一組<code>clkcomplicationtimelineentry</code> 來表示的,一個 entry 中包含了 template 和對應的 <code>nsdate</code>。watchos 将在目前時間超過這個 <code>nsdate</code> 時表示。是以如果我們需要顯示當天的天氣情況的話,就需要将對應的日期設定為當日的 0 點 0 分。對于其他幾個日期的天氣來說,這個狀況也是一樣的。我們需要添加一個方法來通過 weather 的 <code>day</code> 屬性和請求的當日日期來傳回一個對應 entry 中需要的日期。為了運算簡便,我們這裡引入一個第三方架構,swiftdate。将這個項目導入我們 app,然後在 <code>weather.swift</code> 中添加:

接下來我們就可以更新 <code>complicationcontroller.swift</code> 的内容了。首先我們需要實作<code>gettimelinestartdateforcomplication:withhandler:</code> 和<code>gettimelineenddateforcomplication:withhandler:</code> 來告訴系統我們所能提供 complication 的日期區間:

最早的時間是前天的 00:00,這是毫無疑問的。但是最晚的可顯示時間并不是後天的 00:00,而是 23:59:59,這裡一定需要注意。

另外,為了之後建立 template 能容易一些,我們添加一個由 <code>weather.state</code> 建立 template 的方法:

接下來就是實作核心的三個提供時間軸的方法了,雖然很長,但是做的事情卻差不多:

代碼來說非常簡單。<code>getcurrenttimelineentryforcomplication</code> 中我們找到今天的 <code>weather</code> 對象,然後建構合适的 entry。而對于 <code>beforedate</code> 和 <code>afterdate</code> 兩個版本的方法,按照系統提供的<code>date</code> 我們需要組織在這個 <code>date</code> 之前或者之後的所有 entry,并将它們放到一個數組中去調用回調。這兩個方法中還為我們提供了一個 <code>limit</code> 參數,我們的結果數應該不超過這個數字。在實作這三個方法後,我們的時間線就算是建構完畢了。

另外,我們還可以通過實作 <code>getplaceholdertemplateforcomplication:withhandler:</code> 來提供一個在表盤定制界面是會用到的占位圖像。

這樣,在自定義表盤界面我們也可以在選擇到我們的 complication 時看到表示我們的 complication 的樣式了:

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

<code>complicationcontroller</code> 中最後需要實作的是 <code>getnextrequestedupdatedatewithhandler</code>。系統會在你的 watch app 被運作時更新時間線,另外要是你的 app 一直沒有被運作的話,你可以通過這個方法提供給系統一個參考時間,用來建議系統應該在什麼時候為你更新時間線。這個時間應該盡可能長,以節省電池的電量。在我們的天氣的例子中,每天一次更新也許會是個不錯的選擇:

你也許會注意到,因為我們這裡要是不開啟 watch app 的話,其實天氣資料時不會更新的,這樣我們設定重新整理時間線似乎并沒有什麼意義 - 因為不開 watch app 的話資料并不會變化,而開了 watch app 的話時間線就會直接被重新整理。這裡我們考慮到了之後使用 watch connectivity 從手機端重新整理 watch 資料的可能性,是以做了每天重新整理一次的設定。我們在稍後會詳細将這方面内容。

另外,我們還需要記得在 watch app 資料更新之後,強制 reload 一下 complication 的資料。在 complicationcontroller.swift 中加入:

然後在 <code>interfacecontroller.swift</code> 的 <code>request</code> 中,在請求成功傳回後調用一下這個方法就可以了。

現在,我們的 watch app 已經支援 complication 了。同時,因為我們努力提供了之前和之後的資料,我們免費得到了 time travel 的支援。現在你不僅可以在表盤上看到今天的天氣,通過旋轉 digital crown 你還能了解到之前和之後的天氣狀況了:

WWDC15 Session筆記 - 30 分鐘開發一個簡單的 watchOS 2 app

到這裡為止的項目代碼可以在 complication tag 中找到。

在 watchos 1 時代,watch 的 extension 是和 ios app 一樣,存在于手機裡的。是以在 watch extension 和 ios app 之間共享資料是比較簡單的,和其他 extension 類似,使用 app group 将 app 本體和 extension 設為同一組 app,就可以在一個共享容器中共享資料了。但是這在 watchos 2 中發生了改變。因為 watchos 2 的手表 extension 是直接存在于手表中的,是以之前的 app group 的方法對于 watch app 來說已經失效。watch extension 現在會使用自己的一套資料存儲 (如果你之前注意到了的話,我們在請求資料後将它存到了 userdefaults 中,但是手機和手表的 userdefaults 是不同的,是以我們不用擔心資料被不小心覆寫)。如果我們想要在 ios 裝置和手表之間共享資料的話,我們需要使用新的 watch connectivity 架構。

<code>watchconnectivity</code> 架構所扮演的角色就是 ios app 和 watch extension 之間的橋梁,利用這個架構你可以在兩者之間互相傳遞資料。在這個例子中,我們會用 <code>watchconnectivity</code> 來改善我們的天氣 app 的表現 -- 我們打算實作無論在手表還是 ios app 中,每天最多隻進行一次請求。在一個裝置上請求後,我們會把資料傳遞到配對的另一個裝置上,這樣在另一個裝置上打開 app 時,就可以直接顯示天氣狀況,而不再需要請求一次了。

我們在 ios app 和 watchos app 中都可以使用 watchconnectivity。首先我們需要檢查裝置上是否能使用 session,因為在一部分裝置 (比如 ipad) 上,這個架構是不能使用的。這可以通過<code>wcsession.issupported()</code> 來判斷。在确認平台上可以使用後,我們可以設定 delegate 來監聽事件,然後開始這個 session。當我們有一個已經啟動的 session 後,就可以通過架構的方法來向配對的另一個裝置發送資料了。

大緻來說資料發送分為背景發送和即時消息兩類。當 ios app 和 watch app 都在前台的時候,我們可以通過 <code>-sendmessage:replyhandler:errorhandler:</code> 來在兩者之間發送消息,這在 ios app 和 watch app 之間需要互動的時候是非常有用的。另一種是背景發送,在 ios 或 watch app 中有一者不在前台時,我們就需要考慮使用這種方式。背景通訊有三種方式:通過 application context,通過 user info,以及傳送檔案。檔案傳送簡單明了就是傳遞一個檔案,另外兩個都是傳遞一個字典,不同之處在于 application context 将會使用新的資料覆寫原來的内容,而 user info 則可以使多次内容形成隊列進行傳送。每種方式都會在另外一方的 session 開始運作後調用相應的 delegate 方法,于是我們就能知道有資料發送過來了。

結合天氣 app 的特點,我們應該選擇使用 application context 來收發資料。這篇文章已經太長了,是以我們這裡隻做從 ios 到 watchos 的發送了。因為反過來的代碼其實完全一樣,我會在 repo 中完成,在這裡就不再重複一遍了。

首先是在 ios app 中啟動 session。在 <code>viewcontroller.swift</code> 中添加一個屬性:<code>var session: wcsession?</code>,然後在 <code>viewdidload:</code> 中添加:

為了讓 <code>self</code> 成為 session 的 delegate,我們需要聲明 <code>viewcontroller</code> 實作<code>wcsessiondelegate</code>。這裡我們先在檔案最後添加一個空的 extension 即可:

注意我們一定需要設定 session 的 delegate,即使它什麼都沒有做。一個沒有 delegate 的 session 是不能被啟動或正确使用的。

然後就是發送資料了。在 <code>requestweathers</code> 的回調中,資料請求一切正常的分支最後,添加一段

這裡的 <code>storedweathersdictionary</code> 是個新加入的方法,它傳回存儲在 user defaults 中的内容的字典表現形式 (我們在請求傳回的時候就已經将結果内容存儲在 user defaults 裡了,希望你還記得)。

在 watchos app 一側,我們類似地啟動一個 session。在 <code>interfacecontroller.swift</code> 的<code>awakewithcontext</code> 中的 <code>dispatch_once</code> 裡,添加

然後添加一個 extension 來接收傳輸過來的資料:

最後,在請求資料之前我們可以判斷一下已經存儲在 user defaults 中的内容是否是今天請求的。如果是的話,就不再需要進行請求,而是直接使用存儲的内容來重新整理界面,否則的話進行請求并存儲。将原來的 <code>self.request()</code> 改為:

如果你隻是單純地 copy 這些代碼的話,在之前項目的基礎上應該是不能編譯的。這是因為在這裡我并沒有列舉出所有的改動,而隻是寫出了關于 watchconnectivity 的相關内容。這裡涉及到了每次啟動或者從背景切換到 app 時都需要檢測并重新整理界面,是以我們還需要一些額外的重構來達到這個目的。這些内容我們在此也略過了。同理,在 watchos app 需要請求,并且請求結束的時候,我們也可以如前所述,通過幾乎一樣的代碼和方式将請求得到的内容發回給 ios app。這樣,當我們打開 ios app 時,也就不需要再次進行網絡請求了。

這部分的完整的代碼可以在這個 repo 的最終的 tag 上找到,您可以嘗試自己實作一下,也可以直接找這裡的代碼進行參考。如果後續還有修正的話,我會直接在 master 上進行。

本文從零開始完成了一個 ios 和 apple watch 上的天氣情況的 app。雖然說資料源上用的是一個 stub,但是在其他方面還算是比較完整的。本來主要的目的是探索下 watchos 2 中的幾個新 api 的用法,主要是 complication 和 watchconnectivity。但是發現如果隻是單純地照搬文檔的話一是不夠直覺,二是很難說明問題,是以幹脆不如從頭開始,和大家一起完成一個 app 來的更實在。

apple watch 作為 apple 的新産品線,其實所扮演的角色會非常重要。watchos 一代由于種種限制,開發者們很難發揮出裝置的優勢來做出一些有意思的 app。在一代系統中,手表更多地還是隻是一塊 iphone 的額外螢幕。但是在 watchos 2 中,這一狀況有望改善。更加合理和靈活的 app 組織方式以及在手表上的 native 開發,使得 apple watch 的可用範圍提升了不止一個檔次。而在經曆了大半年的彷徨之後,apple watch 開發也逐漸趨于穩定,系統的架構和 api 也逐漸合理。其實 apple watch 還是一款非常有希望的産品,相信随着裝置的進一步成熟和 sdk 的更加開放,我們會有機會像是直接利用 digital crown 或者其他一個手表特性來開發令人耳目一新的 app。個人的對于 apple watch 開發的建議是,現在最好能緊跟上 watch 開發的腳步,盡量進行積累,這樣你才有可能在之後的爆發中取得先機和靈感。

就這麼多吧 (其實已經很多了),祝程式設計愉快~