Sam 集開發者、作者和教練的身份為一體。白天,他會為 raywenderlich.com 錄制視頻,撰寫教程,參加會議以及做一位正經人士。到了晚上,他更喜歡出去,通過他的長號以及魔鬼般的步伐給人們帶來歡樂。你可以在 Github 上找到他,名字是 sammyd ,也可以通過他的個人網站 iwantmyreal.name 找他,他會十分歡迎。
</div>
@iwantmyrealname
</div>
概述(0:00)
我是 Sam,推ters 上的名字是 @iwantmyrealname ,現在我為 Razeware 公司工作,這家小公司對管理 raywenderlich.com 團隊付出了極大的努力。現在讓我們開始談論自適應 UI 吧!
開始時都有什么?(1:16)
在我們為 “iPhone OS” 開發的黑暗年代,我們只有一種尺寸進行開發:那就是 3.5 寸的 iPhone,此外設計布局也是非常容易。實際上,雖然我們也需要處理橫屏的情況,但是就算這樣也只有兩種尺寸。不過通常情況下,絕大多數應用是不允許讓 iPhone 橫屏的,必須要在豎屏中使用。但這個日子已經一去不復返了。
隨后就是 iPad 的推出了,這塊類似大板磚的東西掀起了一場革命。iPhone 和 iPad 之間的尺寸差異十分巨大。你可以選擇為之搭建兩個完全分離的應用,也可以在同一個應用中使用不同的布局進行開發,然而實際上不管怎樣,所編譯出來的都是兩個分離的應用。
隨后就是 4寸 iPhone 的推出了,也就是 iPhone 5 和 5s。4 寸和 3.5 寸擁有相同的寬度,只不過是在底部多出了那么一點點空間而已。剛好可以向其中塞一個廣告,這也是大家經常做的。只不過是在底部加點東西而已,沒什么大不了的。通常情況下,現在的 3.5寸手機都是基于 4寸手機而設計的,就算底部丟點什么東西也無所謂,又不會讓應用無法使用。沒有人覺得有必要擔心這么做的問題,那些使用老款手機用戶的體驗已經被我們拋棄了。
去年推出了 iPhone 6,它參考了 Android 中常見的屏幕尺寸,也就是 4.7寸。然后就是 5.5寸的 iPhone 6 Plus 和現在的 6s Plus,參考了晚餐盤子的尺寸。
最后,就是即將推出的巨型 iPad Pro 了,我們現在又要處理另一種新的尺寸了。
如果算上橫屏和豎屏的話,你會發現我們現在要處理 12種不同的屏幕尺寸 。
很久以前,我們所寫的代碼可能會像這樣:
if UIDevice.currentDevice().userInterfaceIdiom == .Pad {
if UIDevice.currentDevice().orientation == .LandscapeLeft ||
UIDevice.currentDevice().orientation == .LandscapeRight {
doSomething()
} else {
doSomethingElse()
}
} else {
if UIDevice.currentDevice().orientation == .LandscapeLeft ||
UIDevice.currentDevice().orientation == .LandscapeRight {
yetAnotherAlternative()
} else {
theFourthWay()
}
}
你會在代碼中查看當前用戶的設備類型,然后根據該類型來寫布局代碼。還有,當處于橫屏狀態時,處理布局的代碼可能就會發生一點特殊的變化。這個行為是不可持續的,你不能一直為這 12 種屏幕尺寸分別寫不同的布局代碼,這種做法完全行不通。
自適應布局介紹(4:53)
這就是為什么蘋果要發布自適應布局的原因,這也是我們處理布局的首選方式之一。它將尺寸這個設計細節進行了抽象。我們無需再關心設備的類型或者設備的方向。相反,我們將這些觀念用一種稱為“屏幕分類 (size classes) ”的東西組織起來。
什么是屏幕分類呢?這不是在說屏幕有多少個像素點的問題,而是在說有多少空間的問題。我們有多少空間可供我們放東西進去呢?
我們將屏幕劃分為兩種不同的類型:緊湊的 (compact) 和常規的 (regular)。緊湊意味著空間不是很充足,我們會受到某種局限。常規意味著空間的余量是正常的,這就是這兩種類型的涵義。
我們同時還會以兩種不同的方向來討論屏幕分類,就是橫向和縱向。比如說橫向緊湊、縱向常規之類的屏幕類型。這是一種分類的方式,描述特定視圖的空間量。
設備上的屏幕分類(7:34)
那么這些概念是如何與真實設備映射的呢?
|
橫向(Horizontal) |
縱向(Vertical) |
</tr>
</thead>
iPad 豎屏 |
常規 |
常規 |
</tr>
iPad 橫屏 |
常規 |
常規 |
</tr>
iPhone 豎屏 |
緊湊 |
常規 |
</tr>
iPhone 橫屏 |
緊湊 |
緊湊 |
</tr>
iPhone 6(s) Plus 橫屏 |
常規 |
緊湊 |
</tr>
</tbody>
</table>
當你在使用 iPad 或者 iPad Pro 的時候,兩個方向都是常規類型。橫向和縱向都存有大量的空間,一般來說在 iPad 上你隨時都可以將足夠的內容放到上面。
然而,當你看到豎屏的 iPhone 時,會發現它的寬度變為了緊湊型。對于 iPhone 來說,它的橫向層面并沒有太多的空間。接著當你將 iPhone 橫置過來時,縱向方向上就沒有太多空間了,因此縱向變為了緊湊。如果你在豎屏的 iPhone 上查看內容的時候,縱向方向有著充足的空間,因為你可以上下滾動,但是當你將其橫置過來的時候,一般來說你是無法左右滾動的。沒有人愿意讀完某個東西后,滾動到另一側,然后如果要讀取開頭的話,還得將所有東西回滾回去。因此,我們可以將其認為是緊湊的。
在 iOS 9 中,隨著多任務 (multi-tasking) 的推出,這個概念變得愈發重要。例如,無論其方向如何,iPad 通常是常規/常規尺寸的。然而,當你在 iOS 9 中使用新推出的多任務時,你可以在右側進行滑動從而顯示出其他的應用。如果你使用新的 iPad 的話,你可以將其推得更遠,讓兩個應用分別對半占據屏幕。這時候,實際上它是將其視為運行著兩個相鄰 iPhone 應用的 iPad。即使這個應用是專門針對 iPad 推出的,但是在這個情形下它會使用和 iPhoen 相同的屏幕分類配置,也就是橫向緊湊的縱向常規的屏幕配置。
隨后,在 iPad Pro 中,我覺得你可以擁有兩個相鄰的常規/常規應用。這時候,我們已經將設備尺寸從特定設備中抽象出來了,我們現在可以在同一個設備上運行不同的尺寸設計。
適配自適應布局(11:16)
那么,怎樣適配自適應布局才是合理的呢?我們的最終目標就是創建一個管理全局的故事板文件。這個故事板可以在 iPad、iPhone 以及所有不同尺寸的 iOS 設備上運行。我們不再使用 4 個不同的故事板來構建布局,我們同時也可以不再 UI 更新時一個個地對這幾個故事板進行更改。對這個管理全局的故事板進行更新,就意味著所有設備上的 UI 都能相應得到更新。那么我們該怎么做呢?我建議大家采取下列五個步驟:
- 搭建基礎布局
- 這一步是用來“讓我們搭建需要在屏幕上顯示的內容”,以及搭建我們大多數時候想展示的布局的。
</ul>
</li>
- 選擇要重構的屏幕分類
- 卸載無關的約束
- 我們在這里將談論自動布局 (Auto Layout)。這些約束將約定了不同視圖的尺寸和位置,你可以通過“卸載”操作將不需要的約束移除。當我們選擇一個特定的屏幕分類時,我可能就想要把一些要改變的約束移走。
</ul>
</li>
- 為特定的屏幕分類添加新的約束
- 這一步將確保我們在新的屏幕尺寸中能夠得到實際想要的布局。
</ul>
</li>
- 對其他需要的屏幕分類重復上述步驟
- 最重要的事情就是不要為 iPhone 縱向、橫向布局分別創建不同的布局,然后在前往 iPad 的時候又重頭開始。這會導致你對真實布局產生混亂和困惑。最好的方法就是以基礎布局開始,然后根據不同的設備對之進行些許改變。
</ul>
</li>
</ol>
示例(13:22)
我在 GOTO Copenhagen 2015 上對這個方法寫了一個示例,你可以在文章上方看到。我解釋了如何從一個簡單的基礎布局,通過安裝新的約束從而給不同的設備添加布局的。
什么是自適應的?(25:34)
什么類型的東西是可以自適應的呢?首先第一個是約束。你可以取一個約束,然后決定是否在特定的屏幕分類中顯示它。這樣,你就可以以多種不同的方式重新排列和組織布局了,這一點非常贊。但是這不僅僅是自適應布局的完全能力。還有其他東西也可以完成自適應。
您同樣也可以改變某個約束的約束值 (constant)。如果有一個約束表示“這兩個視圖之間的間隔是 10 點”,那么我們可以讓其變成:“如果有充足的空間,那么這個間隔可以是 100 點”。我無需刪除并添加新的約束。奇怪的是,雖然你可以改變約束的倍數 (multiplier),但是為了實現這個效果,實際上你必須卸載這個約束創建一個新的才能實現這個效果。
你同樣也可以改變字體。如果對于 iPhone 和 iPad 應用來說,我想改變字體大小以讓 iPad 上的字體更大一些。我可以輕松完成這個操作。
最后就是視圖的安裝,這同樣也是非常重要的。如果你想在 iPad 上重用 iPhone 上的布局的話,你可能不想改變字體大小以及間隔距離。你可能想要創建一個新的視圖在 iPad 上顯示,這個操作同樣也是非常簡單的。
屏幕分類以及字體改變示例(27:23)
這里有一個關于如何改變屏幕分類和字體大小的示例,單擊此處來查看上面的視頻!
與代碼做抗爭(31:12)
這在代碼中該如何實現的呢?通過界面構造器非常輕松愉快,但是如果想要在代碼中實現此功能的話,很可能就要面臨著一場艱難的“戰爭”了。
public class UITraitCollection : NSObject, NSCopying, NSSecureCoding, NSCoding {
...
public var userInterfaceIdiom: UIUserInterfaceIdiom { get }
public var displayScale: CGFloat { get }
public var horizontalSizeClass: UIUserInterfaceSizeClass { get }
public var verticalSizeClass: UIUserInterfaceSizeClass { get }
@available(iOS 9.0, *)
public var forceTouchCapability: UIForceTouchCapability { get }
}</pre></div>
所有我們所需要的東西都存在這個名為 UITraitCollection 的類中,它是去年被引入的。這是我們用來尋找設備不同之處的地方,包括用戶界面風格(是 iPhone 風格還是 iPad 風格呢?)。你可以獲取展示比例 (display scale),有可能是一倍、兩倍甚至三倍,這取決于每個點所代表的像素值是多少。通過 traitCollection 實例,我們還可以找到當前的屏幕分類是什么。最后,如果在 iPhone 6s 或者 6s Plus 的設備中,你還可以找到是否可以使用 3D Touch 功能,因此就可以根據按壓屏幕的力度做出相應的操作了。
你可以通過使用協議 UITraitEnvironment 來獲取 traitCollection 實例。
public protocol UITraitEnvironment : NSObjectProtocol {
public var traitCollection: UITraitCollection { get }
public func traitCollectionDidChange(previousTraitCollection: UITraitCollection?)
}</pre></div>
UIScreen 、 UIWindow 、 UIPresentationController 、 UIViewController 以及 UIView 都實現了次協議,因此這意味著你處于這些類當中的時候,你就可以找到當前的 traitCollection ,因此你就可以知曉當前的屏幕分類。只需要通過訪問 traitCollection ,你所想知道的一切都會從中得到。
你同樣也會注意到這個 traitCollectionDidChange 函數。當 traitCollection 發生變化時此函數就會被調用,但是何時會發生呢?如果你豎直放立一臺 iPhone 然后將其旋轉,接著每個視圖控制器、每個視圖、每個展示控制器 (presentation controller)、每個屏幕以及每個窗口的都會調用此 traitCollectionDidChange 函數,因為設備發生了旋轉。您可以得到,設備從高度常規、寬度緊縮變成了高度緊縮、寬度緊縮(在 iPhone Plus 上是寬度常規)。當 traitCollection 發生改變時,你可以使用這些信息來處理旋轉或者處理任何你想做的事情。
重載屏幕分類(33:44)
你可以對屏幕分類進行重載,但是你為什么要這么做呢?
extension UIViewController {
public func setOverrideTraitCollection(collection: UITraitCollection?,
forChildViewController childViewController: UIViewController)
public func overrideTraitCollectionForChildViewController(
childViewController: UIViewController) -> UITraitCollection?
}
你可以構建一個對給定屏幕分類有著特殊布局的視圖控制器,接著你會想到:“我現在在 iPad 上,但是我已經構建了一個容器視圖控制器,然后將其他視圖控制器放到其當中去。我在寬度緊縮下定義該布局,因為這個視圖控制器太小了,因此我想要使用一個容器視圖控制器來對它進行管理”。
在這個情況下,你可以對這個特定的子視圖控制器使用 traitCollection 。我雖然位于 iPad 的巨大畫布當中,但是我的其中一個子視圖控制器非常小(比如說寬度緊縮)。
你可以使用上面的這些代碼,并且用起來很容易:你可以自行構建一個 traitCollection ,然后重寫里面你想要的屬性,然后通過 UIViewController 中的這個方法將其傳到子視圖控制器當中。
UIContentContainer (34:46)
最后一個我想要進行介紹的協議是 UIContentContainer 。與在 traitCollection 中處理轉換相比,這個方法稍微更細粒化 (fine-grained) 一些。
public protocol UIContentContainer : NSObjectProtocol {
...
public func viewWillTransitionToSize(size: CGSize,
withTransitionCoordinator coordinator:
UIViewControllerTransitionCoordinator)
public func willTransitionToTraitCollection(
newCollection: UITraitCollection,
withTransitionCoordinator coordinator:
UIViewControllerTransitionCoordinator)
}</pre></div>
當你在用 traitCollectionDidChange 的過程中,突然,你收到消息說 traitCollection 發生了變化,需要重新構建布局。那么如何確保以一種好的方式來處理動畫呢?因此,在 UIContentContainer 中你就可以在其中使用這些動畫方法了。
UIViewController 和 UIPresentationController 都實現了 UIContentContainer 方法。它們都擁有 willTransitionToTraitCollection 方法。在變化發生前,你就會被告知 traitCollection 將要發生變化,因此你就可以得到一個 transitionCoordinator 實例。
transitionCoordinator 允許你這樣做:“我想要執行一個動畫,無論系統動畫做了什么,這個動畫都將執行”。非常簡單,不是么?
這里的另外一個方法是 viewWillTransitionToSize 。問題是每個人都在問:“我的 iPad 無論什么方向都是常規/常規類型的?這簡直就是扯淡!”。 viewWillTransitionToSize 可以解決這個問題。在 iOS 9 之前,這個方法只會在旋轉時被調用,除非你自己搞了一些非常復雜的視圖控制器容器變化。當你旋轉 iPad 的時候,上面這個方法就會被調用。由于只有尺寸放生了變化,因此下面這個方法不會被調用。
被取消的旋轉方法(36:41)
extension UIViewController {
@available(iOS, introduced=2.0, deprecated=8.0)
public var interfaceOrientation: UIInterfaceOrientation { get }
@available(iOS, introduced=2.0, deprecated=8.0,
message="Implement viewWillTransitionToSize:withTransitionCoordinator: instead")
public func willRotateToInterfaceOrientation(
toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval)
@available(iOS, introduced=2.0, deprecated=8.0)
public func didRotateFromInterfaceOrientation(
fromInterfaceOrientation: UIInterfaceOrientation)
@available(iOS, introduced=3.0, deprecated=8.0,
message="Implement viewWillTransitionToSize:withTransitionCoordinator: instead")
public func willAnimateRotationToInterfaceOrientation(
toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval)
}</pre></div>
在 iOS 8 中,這些處理旋轉的好用老方法都不再贊成使用了。你不應該使用 willAnimateRotationToInterfaceOrientation 或者 didRotateFromInterfaceOrientation 方法了。那么現在應該怎么處理旋轉呢?
使用 willTransitionToSize 來代替。這里有一個該方法的使用例子:
override func viewWillTransitionToSize(size: CGSize,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
let image = imageForAspectRatio(size.width / size.height)
coordinator.animateAlongsideTransition({
context in
// 創建一個變化效果(transition),匹配上下文的持續時間(duration)
let transition = CATransition()
transition.duration = context.transitionDuration()
// 播放淡入淡出動畫
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.type = kCATransitionFade
self.backgroundImageView.layer.addAnimation(transition, forKey: "Fade")
// 設置新圖片
self.backgroundImageView.image = image
}, completion: nil)
}</pre></div>
不要將旋轉視作設備的移動。相反將其視為視圖控制器尺寸的改變,因為從用戶的角度來看,這才是真實發生的操作。這里的例子使用了一個變換協調器 (transition coordinator),調用了 animateAlongsideTransition 方法。這允許我們當旋轉發生的時候,系統在這個動畫中會獲取到你的這個視圖控制器,對其進行旋轉,然后重新進行尺寸的跳轉。
堆棧視圖(37:57)
堆棧視圖 (Stack Views) 是 iOS 9 新引入的東西。如果你此前從未使用過自動布局,現在是時候來學習它了,因為堆棧視圖可以幫助你節約很大一部分的操作。試想我有一個包含三個子視圖的空白視圖。那么我應該怎么處理它們的約束呢?
首先我需要將頂部的這個視圖與父視圖頂部、左側和右側建立關聯。我也要將底部視圖與父視圖底部建立關聯。接著在它們之間加一些約束以將分隔它們開來。我同時還有讓它們居中對齊,因此它們相互之間都是中心對齊的。最后,我想要指定它們的相關寬度,或許中間的這個視圖將會使用固定的內容尺寸。這里有很多約束存在,尤其是搭建這么一個簡單的視圖關系。
通過堆棧視圖,我可以減少至少 12 條約束。堆棧視圖本身就擁有類似的屬性,因此我告訴我所想面對的軸方向,然后設置間隔之類的東西。我讓它們保持中央對齊。我必須使用這些約束,因為我必須將這個堆棧視圖通過更寬的視圖 (wider view) 來將其定位在某個地方。如果我想要讓其尺寸更為精確的話,我甚至還可以多加一兩個約束。
在 Xcode 當中,要學會使用這個箭頭指下樓梯的這個按鈕。這將創建一個堆棧視圖。從中你就可以改變所有不同種類的東西了。
關于堆棧視圖的一個有趣的事情就是它們的自適應性 (adaptivity) 非常高。這意味著我可以重載屏幕分類來添加諸如軸 (axis) 之類的東西。比如說,我可以將豎直對齊變更為水平對齊,只需要添加一個在堆棧視圖上重載屏幕分類即可。我同樣可以通過自適應性來輕易地改變對齊分布與間隔。自適應性非常強大,值得一看。
自適應性小技巧(41:17)
- 了解自動布局
- 這個技術的學習曲線很高,但是它不是不可能學會的,并且值得去學。學起來并不如你剛看到那樣困難。
</ul>
</li>
- 使用自適應布局來構建大致框架
- 只使用這些自適應工具并不能讓你完成所有的布局。它們只是讓你的布局看上去有個大概,然后你需要深入到代碼當中,然后實現需要細粒度的玩意兒,比如說視圖變換以及尺寸變換之類的東西。
</ul>
</li>
- 從基本布局開始,然后執行重載
- 永遠,永遠不要在故事板中這樣做:“我想要一個 iPad 版本,那么我將從常規/常規類型開始構建。現在,我要針對豎屏 iPhone 布局了,因此我到常規/緊湊類型中構建”。相反,你應該從一個通用的基礎布局開始,然后才開始適配工作,好好想想針對這個特定分類你需要重構哪些東西。
</ul>
</li>
- 堆棧視圖讓生活更簡單
- 如果你可以使用 iOS 9 的話,你最好好好研究一下堆棧視圖。如果你不能的話,網上仍然有很多有類似功效的開源庫。他們可以讓布局更為簡單。如果你將堆棧視圖嵌套在一起的話,這會讓你的生活更加美好的。
</ul>
</li>
</ol>
現在是時候學習自適應了。正如我所說,這個時候你必須要構建 12 種不同的布局。不過借助自適應,你可以輕易地在各種不同的應用或者多個故事板文件之間穿梭,事情簡單而又高效。快試試看自適應布局吧,看看你能做的以及所不能的。
最后,我再強調一下,我的 推ter 帳號是 @iwantmyrealname ,你可以在 我的 Github 上找到上述我所提及的示例代碼。
問與答(43:01)
問:如何處理對齊呢?我們已經習慣了使用對齊來讓每一個像素都達到完美。
Sam:這是使用自適應布局的一大挑戰之一,這也是我認為在幾年前的 Web 界這個想法就非常活躍了。我記得當我第一次做網頁設計的時候,我花費了大量的時間讓我的每一個像素在 Firefox 和 Internet Explorer 上都達到完美,到了后來還有 Chrome、Opera 等等之類的。你在那兒一直煩惱為什么 x 值不等于 y,最后我們似乎已經進入了一個“內容為重”的階段。
但是我們完全不必要讓這里、這里以及這里的像素都達到完美級別。這在 Web 應該是有效的。我們在應用上沒有必要這么做,一切的一切都是為了簡化開發。如果你告訴你的設計師,“好的,現在你可以做一個完美像素級別的設計了,現在就給我 12 種不同尺寸的設計吧,當然越多越好”。如果你告訴他們需要為一個 APP 設計 20 中不同完美像素級別的設計的話,相信我他們會開始考慮使用自適應的。
因此,這完全取決于在設計中你想要什么樣的元素,比如說“當這個窗口變窄的時候,我們應該怎么重新組織布局呢?”因為這實際上是在 Web 上切實發生的,不是么?你將頂部的那個長長的菜單欄移除了,讓其變成了漢堡包類似的下拉菜單,就像在 iPhone 上做的那樣。這雖然可能不是一個正確的做法,但是唯一能解決的辦法就是進行嘗試。嘗試走出像素完美的世界,然后重點關注于內容,我們可以用這些方式試著讓頁面看起來棒一些。
See the discussion on Hacker News .
Sign up to be notified of new videos — we won’t email you for any other reason, ever.
</div>
</div>
來自: https://realm.io/cn/news/gotocph-sam-davies-adaptive-ui-ios/
</span></code></code></code></code></code></code></code></span>
本文由用戶
jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
sesese色