本文是我的 WWDC15 筆記中的一篇,涉及的 Session 有
iOS 9 中最引人注目的新特性就是多任務了,在很久以前的越獄開發裡就已經出現過類似的插件,而像是 Windows Surface 系列上也已經有分屏多任務的特性,可以讓用戶同時使用兩個或多個 app。iOS 9 中也新加入類似的特性。iOS 9 中的多任務有三種表現形式,臨時出現和交互的滑動覆蓋 (Slide Over),真正的分屏同時操作兩個 app 的分割視圖 (Split View),以及在其他 app 裡依然可以進行視頻播放的畫中畫 (Picture in Picture) 模式。
在關於多任務的文檔中,Apple 明確指出:
絕大部分 app 都應當適配 Slide Over 和 Split View
因為這正是 iOS 9 的核心功能之一,也是你的用戶所期望看到的。另一方面,支持多任務也增加了你的用戶打開和使用你的 app 的可能。不過多任務有一點限制,那就是在能夠安裝 iOS 9 的 iPad 設備上,僅只有性能最強大的 iPad Air 2 和之後的機型支持分割視圖模式,而其他像是 iPad mini 2,iPad mini 3 以及 iPad Air 只支持滑動覆蓋和畫中畫兩種模式。這在一定程度上應該還是基於移動設備資源和性能限制的考慮做出的決策,畢竟要保證良好的使用體驗為前提,多任務才會有意義。
對於開發者來說,雖然多種布局看起來很復雜,但是實際上如果緊跟 Apple 的技術步伐的話,將自己的 iPad app 進行多任務適配並不會是一件非常困難的事情。因為滑動覆蓋模式和分割視圖模式所采用的布局其實就是 Compact Width 的布局,而這一概念就是 WWDC14 上引入的基於屏幕特征的 UI 布局方式。如果你已經在使用這套布局方式了的話,那麼可以說多任務視圖的支持也就順帶自動完成了。不過如果你完全沒有使用過甚至沒有聽說過這套布局方法的話,我去年的一篇筆記可能能幫你對此有初步了解,在下一節裡我也會稍微再稍微復習一下相關概念和基本用法。
Adaptive UI 是 Apple 在 iOS 8 提出的概念。在此之前,我們如果想要同時為 iPhone 和 iPad 開發 app 的話,很可能會寫很多設備判斷的代碼,比如這樣:
<code class="swifts hljs cpp"><span class="hljs-function"><span class="hljs-keyword">if</span> <span class="hljs-title">UI_USER_INTERFACE_IDIOM</span><span class="hljs-params">()</span> </span>== .Pad { <span class="hljs-comment">// 設備是 iPad</span> } </code>
除此之外,如果我們想要同時適配橫向和縱向的話,我們會需要類似這樣的代碼:
<code class="swift hljs"><span class="hljs-keyword">if</span> <span class="hljs-type">UIInterfaceOrientationIsPortrait</span>(orientation) { <span class="hljs-comment">// 屏幕是豎屏</span> } </code>
這些判斷和分支不僅難寫難讀,也使適配開發困難重重。從 iOS 8 之後,開發者不應該再依賴這樣設備向來進行 UI 適配,而應該轉而使用新的 Size Class 體系。Apple 將自家的移動設備按照尺寸區別,將縱橫兩個方向設計了 Regular 和 Compact 的組合。比如 iPhone 在豎屏時寬度是 Compact,高度是 Regular,橫屏時 iPhone 6 Plus 寬度是 Regular,高度是 Compact,而其他 iPhone 在橫屏時高度和寬度都是 Compact;iPad 不論型號和方向,寬度及高度都是 Regular。現有的設備的 Size Class 如下圖所示:
針對 Size Class 進行開發的思想下,我們不再關心具體設備的型號或者尺寸,而是根據特定的 Size Class 的特性來展示內容。在 Regular 的寬度下,我們可以在水平方向上展示更多的內容,比如同時顯示 Master 和 Detail View Controller 等。同樣地,我們也不應該再關心設備旋轉的問題,而是轉而關心 Size Class 的變化。在開發時,如果是使用 Interface Builder 的話,在制作 UI 時就注意為不同的 Size Class 配置合適的約束和布局,在大多數情況下就已經足夠了。如果使用代碼的話,UITraitCollection
類將是使用和操作 Size Class 的關鍵。我們可以根據當前工作的UIViewController
的 traitCollection
屬性來設置合適的布局,並且在 -willTransitionToTraitCollection:withTransitionCoordinator:
和 -viewWillTransitionToSize:withTransitionCoordinator:
被調用時對 UI 布局做出正確的響應。
雖然並不是理論上不可行,但是使用純手寫來操作 Size Class 會是一件異常痛苦的事情,我們還是應該盡可能地使用 IB 來減少這部分的工作量,加快開發效率。
對於 iOS 9 中的多任務,滑動覆蓋和分割視圖的初始位置,新打開的 app 的尺寸都將是設備尺寸的 1/3。不過這個比例並不重要,我們需要記住的是新打開的 app 將運行在 Compact Width 和 Regular Height 的 Size Class 下。也就是說,如果你的 iPad app 使用了 Size Class 進行布局,並且是支持 iPhone 豎屏的,那麼恭喜,你只需要換到 iOS 9 SDK 並且重新編譯你的 app,就搞定了。
因為本文的重點不是教你怎麼開發一個 Adaptive UI 的 app,所以並不打算在這方面深入下去。如果你在去年缺了課,不是很了解這方面的話,這篇教程可能可以幫你快速了解並掌握這些內容。如果你想要直接上手看看 iOS 9 中的 多任務是如何工作的話,可以新建一個 Master-Detail Application,並將其安裝到 iPad 模擬器上。Master-Detail 的模板工程為我們搭設了一個很好的適配 Size Class 的框架,讓項目可以在任何設備上都表現良好。同樣你也可以觀察它在 iOS 9 的 iPad 上的表現。
但是其實並不是所有的 app 都應該適配多任務,比如一個需要全屏才能體驗的游戲就是典型。如果你不想你的 app 可以作為多任務的副 app 被使用的話,你可以在 Info.plist 中添加UIRequiresFullScreen
並將其設為 true
。
Easy enough?沒錯,要適配 iPad 的多任務,你需要做的就只有按照標准流程開發一個全平台通用 app,僅此而已。
雖說沒太多特別值得一提的內容,但是也還是有一些需要注意的小細節。
在以前是不存在 app 在前台還要和別的 app 共享屏幕這種事情的,所以 UIScreen.bounds
和主窗口的 UIWindow.bounds
使用上來說基本是同義詞。但是在多任務時代,UIWindow
就有可能只有 1/3 或者 1/2 屏幕大小了。如果你在之前的 app 中有使用它來定義你的視圖的話,就有必要為多任務做特殊的處理了。不過雖然滑動覆蓋和分割視圖都是在右側展示,但是它們的 Window 的 origin 依然是 (0, 0),這也方便了我們定義視圖。
第二個細節是現在 iPad UI 的 Size Class 是會發生變化的。以前不論是豎直還是水平,iPad 屏幕的 Size 總是長寬均為 Regular 的。但是在 iOS 9 中情況就不一樣了,你的 app 可能被作為附加 app 通過多任務模式打開,可能會在多任務時被用戶拖動從而變成全屏 app (這時 Size Class 將從 Compact 的寬度變為 Regular),甚至可能你的 app 作為主 app 被使用是會因為用戶拖動而變成 Compact 寬度的 app:
換句話說,你不知道你的 app 的 Size Class 會不會變化,以及何時變化,這都是用戶操作的結果。因此在開發時,就必須充分考慮到這一點,力求在尺寸變化時呈現給用戶良好的效果。根據屏幕大小進行合適的 UI 設計和調整自不用說,另外還應當注意在合適的時機利用transitionCoordinator
的 -animateAlongsideTransition:
來進行布局動畫,讓切換更加自然。
由於多任務帶來了多個 app 同台運行的可能性,因此你的 app 必定會面臨和別的 app 一起運行的情況。在開發移動應用時永遠不能忘記的是設備平台的限制。相比於桌面設備,移動端只有有限的內存,而兩個甚至三個 app 同時在前台運行,就需要我們精心設計內存的使用。對於一般開發者來說,合理地分配內存,監聽 Memory Warning 來釋放 cache 和不必要的 view controller,避免循環引用等等,應該成為熟練掌握的日常開發基本功。
最後一個細節是對完美的苛求了。在 iOS 9 中多任務也通過 App Switcher 來進行 app 之間的切換的。所以在你的 app 被切換到後台時,系統會保存你的 app 的當前狀態的截圖,以供之後切換時顯示。你的 app 現在有可能被作為 Regular 的全屏 app 使用,也可能使用 Compact 布局,所以在截圖時系統也會依次保存兩份截圖。用戶可能會在全屏模式下把你的 app 關閉,然後通過多任務再將你的 app 作為附加 app 打開,這時最好能保證 App Switcher 中的截圖和 app 打開後用戶看到的截圖一致,以獲取最好的體驗。可能這並不是一個很大的問題,但是如果追求極致的用戶體驗的話,這也是必行的。對於那些含有用戶敏感數據,需要將截圖模糊處理的 app,現在也需要注意同時將兩種布局的截圖都進行處理。
iOS 9 中多任務的另一種表現形式就是視頻的畫中畫模式:即使退出了,你的視頻 app 也可以在用戶使用別的 app 的時候保持播放,比如一邊看美劇一邊寫日記或者發郵件。這大概會是所有的視頻類 app 都必須要支持的特性了,實現起來也很容易:
AVAudioSessionCategoryPlayback
在 iOS 9 中,一直伴隨我們的 MediaPlayer 框架中的視頻播放部分正式宣布壽終正寢。也就是說,如果你在使用 MPMoviePlayerViewController
或者 MPMoviePlayerController
在播放視頻的話,你就無法使用畫中畫的特性了,因此盡快轉型到新的視頻播放框架會是急迫的適配任務。因為畫中畫模式是基於 AVPlayerLayer
的。當切換到畫中畫時,會將正在播放視頻的 layer 取出,然後進行縮小後添加到新的界面的 layer 上。這也是舊的 MediaPlayer 框架無法支持畫中畫的主要原因。
如果你使用 AVPlayerViewController
的話,一旦滿足這些簡單的條件以後,你應該就可以在使用相應框架全屏播放視頻時看到右下角的畫中畫按鈕了。不論是點擊這個按鈕進入畫中畫模式還是直接使用 Home 鍵切換到後台,已經在播放的視頻就將縮小到屏幕右下角成為畫中畫,並保持播放。在畫中畫模式下,系統會在視頻的 AVPlayerLayer 上添加一套默認控件,用來控制暫停/繼續,關閉,以及返回 app。前兩個控制沒什麼可多說的,返回 app 的話需要我們自己處理返回後的操作。一般來說我們希望能夠恢復到全屏模式並且繼續播放這個視頻,因為 AVPlayerViewController
進行播放時我們一般不會去操作 AVPlayerLayer
,在恢復時就需要實現AVPlayerViewControllerDelegate
中的 -playerViewController:restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:
來根據傳入的 ViewController 重建 UI,並將 true
通過 CompletionHandler 返回給系統,已告知系統恢復成功 (當然如果無法恢復的話需要傳遞 false)。
我們也可以直接用 AVPlayerLayer
來構建的自定義的播放器。這時我們需要通過傳入所使用的AVPlayerLayer
來創建一個 AVPictureInPictureController
。AVPictureInPictureController
提供了檢查是否支持畫中畫模式的 API,以及其他一些控制畫中畫行為的方法。與直接使用AVPlayerViewController
不太一樣的是,在恢復時,系統將會把畫中畫時縮小的 AVPlayerLayer
返還到之前的 view 上。我們可以通過 AVPictureInPictureControllerDelegate
中的相應方法來獲知畫中畫的執行情況,並結合自己 app 的情況來恢復 UI。
通過之前幾年的布局,在 AutoLayout 和 Size Class 的基礎上,Apple 在 iOS 9 中放出了多任務這一殺手锏。可以說同屏執行多個 app 的需求從初代 iPad 開始就一直存在,而現在總算是姗姗來遲。在 OS X 10.11 中,Apple 也將類似的特性引入了 OSX app 的全屏模式中,可以說是統一 OSX 和 iOS 這兩個平台的進一步嘗試。
但是 iPad 上的多任務還是有一些不足的。最大的問題是 app 依然是運行在沙盒中的,這就意味著在 iOS 上我們還是無法在兩個 app 之間進行通訊:比如同時打開照片和一個筆記 app,我們無法通過拖拽方式將某張圖片直接拖到筆記中去。雖然在 iOS 中也有 XPC 服務,但是第三方開發者現在並不能使用,這在一定程度上還是限制了多任務的可能性。
不過總體來說,多任務特性使得 iPad 的實用性大大上升,這也肯定會是未來用戶最常用以及最希望在 app 中看到的特性之一。花一點時間,學習 Adaptive UI 的制作方式,讓 app 支持多任務運行,會是一件很合算的事情。