iOS 中的 UI 自適應

jopen 8年前發布 | 24K 次閱讀 iOS開發 移動開發

早些年開發 iOS 的時候(那個時候 iOS 的前身被稱為 “iPhone OS”),UI 設計是一個相當簡單的事情——因為只需要為一種屏幕尺寸設計即可,這使得構建像素級完美設計 (pixel-perfect) 成為可能。而到了今天,屏幕尺寸愈來愈多,舊有的 UI 構建方法已經完全無法適應了。不過蘋果介紹了幾種不同的技術來解決屏幕尺寸碎片化的問題,最為突出的就是自適應布局 (Adaptive Layout) 的概念了。

在本次 GOTO Conference CPH 2015 講演中,Sam Davies 將帶我們深入了解自適應布局,通過展示幾種形象的例子來講解自適應布局的理念,同時還會帶來使用界面構造器 (Interface Builder) 時的一些小技巧。他同時還討論了一些關于處理多屏幕尺寸的最佳用例,從 web、Android 以及 iOS 中汲取靈感。

See the discussion on Hacker News .

</div>

Sign up to be notified of new videos — we won’t email you for any other reason, ever.

About the Speaker: Sam Davies

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)

那么這些概念是如何與真實設備映射的呢?

</tr> </thead>

</tr>

</tr>

</tr>

</tr>

</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 都能相應得到更新。那么我們該怎么做呢?我建議大家采取下列五個步驟:

  1. 搭建基礎布局

    • 這一步是用來“讓我們搭建需要在屏幕上顯示的內容”,以及搭建我們大多數時候想展示的布局的。
    • </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)

            1. 了解自動布局

              • 這個技術的學習曲線很高,但是它不是不可能學會的,并且值得去學。學起來并不如你剛看到那樣困難。
              • </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 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
             轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
             本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
橫向(Horizontal) 縱向(Vertical)
iPad 豎屏 常規 常規
iPad 橫屏 常規 常規
iPhone 豎屏 緊湊 常規
iPhone 橫屏 緊湊 緊湊
iPhone 6(s) Plus 橫屏 常規 緊湊
  • sesese色