Xcode 6 AutoLayout Size Classes,xcodeautolayout
1、基本概念
在iPad和iPhone 5出現之前,iOS裝置就只有一種尺寸。我們在做螢幕適配時需要考慮的僅僅有裝置方向而已。而很多應用並不支援轉向,這樣的話就完全沒有螢幕適配的工作了。隨著iPad和iPhone 5,以及接下來的iPhone 6的推出,螢幕尺寸也變成了需要考慮的對象。在iOS7之前,為一個應用,特別是universal的應用製作UI時,我們總會首先想我們的目標裝置的長寬各是多少,方向變換以後布局又應該怎麼改變,然後進行布局。iOS6引入了AutoLayout來協助開發人員使用約束進行布局,這使得在某些情況下我們不再需要考慮尺寸,而可以專註於使用約束來規定位置。
既然我們有了AutoLayout,那麼其實通過約束來指定視圖的位置和尺寸是沒有什麼問題的了,從這個方面來說,螢幕的具體的尺寸和方向已經不那麼重要了。但是實戰中這還不夠,AutoLayout正如其名,只是一個根據約束來進行布局的方案,而在對應不同裝置的具體情況下的體驗上還有欠缺。一個最明顯的問題是它不能根據裝置類型來確定不同的互動體驗。很多時候你還是需要判斷裝置到底是iPhone還是iPad,以及現在的裝置方向究竟是豎直還是水平來做出判斷。這樣的話我們還是難以徹底擺脫對於裝置的判斷和依賴,而之後如果有新的尺寸和裝置出現的話,這種依賴關係顯然顯得十分脆弱的(想想要是有iWatch的話..)。
所以在iOS8裡,Apple從最初的設計哲學上將原來的方式推翻了,並引入了一整套新的理念,來適應裝置不斷的發展。這就是SizeClasses。
不再根據裝置螢幕的具體尺寸來進行區分,而是通過它們的感官表現,將其分為普通(Regular)和緊密(Compact)兩個種類(class)。開發人員便可以無視具體的尺寸,而是對這這兩類和它們的組合進行適配。這樣不論在設計時還是代碼上,我們都可以不再受限於具體的尺寸,而是變成遵循尺寸的視覺感官來進行適配。
SizeClasses有三個值:Regular,Compact和Any。Any是什麼意思呢?如果weight設為Any,height設定為Regular,那麼在該狀態下的介面元素在只要height為Regular,無論weight是Regular還是Compact的狀態中都會存在。這種關係應該叫做繼承關係,具體的四種介面描述與可繼承的介面描述如下:
| 1234 |
w:Compacth:Compact繼承(w:Anyh:Compact,w:Compacth:Any,w:Anyh:Any)w:Regularh:Compact繼承(w:Anyh:Compact,w:Regularh:Any,w:Anyh:Any)w:Compacth:Regular繼承(w:Anyh:Regular,w:Compacth:Any,w:Anyh:Any)w:Regularh:Regular繼承(w:Anyh:Regular,w:Regularh:Any,w:Anyh:Any) |
這麼多裝置(iPhone 4S,iPhone 5/5s,iPhone 6,iPhone 6Plus,iPad,AppleWatch)的尺寸,就通過SizeClasses簡單的表達出來了:
iPhone4S,iPhone 5/5s,iPhone 6
豎屏:(w:Compacth:Regular)
橫屏:(w:Compacth:Compact)
iPhone6Plus
豎屏:(w:Compacth:Regular)
橫屏:(w:Regularh:Compact)
iPad
豎屏:(w:Regularh:Regular)
橫屏:(w:Regularh:Regular)
AppleWatch(猜測)
豎屏:(w:Compacth:Compact)
橫屏:(w:Compacth:Compact)
PS:附形:
2、UITraitCollection和UITraitEnvironment(Size Classes手寫代碼)
為了表徵SizeClasses,Apple在iOS 8中引入了一個新的類,UITraitCollection。這個類封裝了像水平和豎直方向的SizeClass等資訊。iOS 8的UIKit中大多數UI的基礎類(包括UIScreen,UIWindow,UIViewController和UIView)都實現了UITraitEnvironment這個介面,通過其中的traitCollection這個屬性,我們可以拿到對應的UITraitCollection對象,從而得知當前的SizeClass,並進一步確定介面的布局。
和UIKit中的響應者鏈正好相反,traitCollection將會在viewhierarchy中自上而下地進行傳遞。對於沒有指定traitCollection的UI組件,將使用其父節點的traitCollection。這在布局包含childViewController的介面的時候會相當有用。在UITraitEnvironment這個介面中另一個非常有用的是-traitCollectionDidChange:。在traitCollection發生變化時,這個方法將被調用。在實際操作時,我們往往會在ViewController中重寫-traitCollectionDidChange:或者-willTransitionToTraitCollection:withTransitionCoordinator:方法(對於ViewController來說的話,後者也許是更好的選擇,因為提供了轉場上下文方便進行動畫;但是對於普通的View來說就只有前面一個方法了),然後在其中對當前的traitCollection進行判斷,並進行重新布局以及動畫。代碼看起來大概會是這個樣子:
| 123456789101112 |
override func willTransitionToTraitCollection(newCollection: UITraitCollection, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator){ super.willTransitionToTraitCollection(newCollection, withTransitionCoordinator: coordinator) coordinator.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext!) -> Void in if (newCollection.verticalSizeClass == UIUserInterfaceSizeClass.Compact) { //To Do: modify something for compact vertical size } else { //To Do: modify something for other vertical size } self.view.setNeedsLayout() }, completion: nil) } |
在兩個To Do中,我們應該刪除或者添加或者更改不同條件下的AutoLayout約束(當然,你也可以幹其他任何你想做的事情),然後調用-setNeedsLayout來在上下文中觸發轉移動畫。如果你堅持用代碼來處理的話,可能需要面臨對於不同SizeClasses來做移除舊的約束和添加新的約束這樣的事情,可以說是很麻煩(至少我覺得是麻煩的要死)。但是如果我們使用IB的話,這些事情和代碼都可以省掉,我們可以非常方便地在IB中指定各種SizeClasses的約束(稍後會介紹如何使用IB來對應SizeClasses)。另外使用IB不僅可以節約成百上千行的布局代碼,更可以從新的Xcode和IB中得到很多設計時就可以即時監視,查看並且調試的特性。可以說手寫UI和使用IB設計的時間消耗和成本差距被進一步拉大,並且出現了很多手寫UI無法實現,但是IB可以不假思索地完成的任務。從這個意義上來說,新的IB和SizeClasses系統可以說無情地給手寫代碼判了個死緩。
另外,新的API和體系的引入也同時給很多我們熟悉的UIViewController的有關旋轉的老朋友判了死刑,比如下面這些API都棄用了:
| 123456 |
@property(nonatomic, readonly) UIInterfaceOrientation interfaceOrientation - willRotateToInterfaceOrientation:duration:- willAnimateRotationToInterfaceOrientation:duration:- didRotateFromInterfaceOrientation:- shouldAutomaticallyForwardRotationMethods |
現在全部統一到了viewWillTransitionToSize:withTransitionCoordinator:,旋轉的概念不再被提倡使用。其實仔細想想,所謂旋轉,不過就是一種Size的改變而已,我們都被Apple騙了好多年,不是嗎?
3、InterfaceBuilder中使用SizeClasses
建立一個新的通用項目。如果你想要早在一個已經建立了的Xcode6項目,你需要啟用sizeclasses選項。你可以在InterfaceBuilder中的屬性面板勾選autolayout的選項的下面找到它。
首先,讓我們在Xcode中看一下sizeclass的網格。這是一個你可以在不同的布局排列間切換的地區。當你查看storyboard的時候,看到視圖的底部,並且點擊‘wAnyhAny’字樣的標籤。你將會看到一些類似網格的畫面。
預設的,我們以一個基礎的設定開始,也就是anywidth和anyheight。很多事情都將在這裡安置和改變,包括了iphone和ipad的所有方向的預設布局。蘋果建議把大多數的設定都在這個介面中進行設定。這個是因為減少工作量而顯得特別的簡單。讓我們布局一個超級寬的按鈕在畫面的中間。給它一個綠色的背景,從而讓我們看到它真實的尺寸,給它一個約束來讓他置中。
並且給它一個誇張的固定寬度600。
好了,現在在ipad和iphone的模擬器都運行一下,你將會看到都是置中,但對於iphone的兩個方向都太寬了,(這裡你設定了頁面中button的寬度但並沒有馬上更新是因為你在做添加約束的時候沒有更新圖形,導致了如的情況,storyboard裡面沒有更新,而在模擬器運行時候更新了,左邊大綱欄目裡面也有警告說明,可以直接點擊警告裡面的黃色三角來更新畫面其實就是UpdataFrame)
讓我們使用sizeclasses來修正吧。回到剛才那個第一張圖的網格選擇iphone的縱向(portrait)設定,就是緊湊的寬度+常規的高度。網格中的紅色矩形.
你將會注意到你在網格中選中之後底部的bar改變為藍色。那是在警告你:“Hey,你並不是在一個基礎的設定,有些改變將會只在你啟動並執行時候顯示。所以這個bar現在是藍色的!”我所說的一些改變是因為有四項你能改變的sizeclasses:1約束常數,2字型,3約束的開/關,4子視圖的開/關。
前兩個是不言而喻的,但是讓我來告訴你如何讓後兩者工作。在當前的sizeclass(compactwidth和regularheight)狀況下讓我們試著把一個約束關閉。在文檔的提綱欄裡,點擊設定在我們的button的CentreX校準約束:
現在看一下我們的屬性檢查欄,在底部我們可以看到帶標記的一個單詞“Installed”,並且左側有額外的加號按鈕。點擊額外的加號並且點選'CompactWidth|RegularHeight'(當前的就是)。
現在你將會看到2個標記物,把剛剛添加的哪一個取消勾選(wChR)
現在我們的約束不再安置並且做任何事情來配置sizeclasses。就像你看到的,Xcode正在控訴我們的約束太混亂了(左邊的大綱會有錯誤提示表示你缺少了約束-譯者),如果你這時候運行app在iphone的模擬器上的話,按鈕不在X方向置中了。但是在ipad的上面還是置中的,因為約束仍然安置在基本的設定裡面。這個約束將會一直配置著除非我們把它取消勾選。你甚至能夠旋轉你的iphone模擬器,並且發現button將會神奇的回到置中,因為iphone的橫向是不同的sizeclass配置,好了,讓我們把勾選回來,讓button回到置中。
現在讓我們改變我們設定在button寬度的約束,選擇button,並且來到Size的屬性檢查欄,下拉到底部,我們可以看到所有的約束。點擊Width原本是600的使用Edit設定為100:
在iPhone的模擬器上運行,你將會看到button已經具備了正確的寬度。運行在ipad的模擬器的時候卻展示了600的寬度,因為我們沒有改變基本設定裡面的寬度。但是,在iphone的橫向landscape仍然看著不怎麼樣,因為iphone的橫向設定來自基本的AnyAny的設定。讓我們修正一下。在網格裡面我們選擇compactWidth和CompactHeight。也就是第一張圖的藍色網格。
現在我們在這個設定下改變width的約束,就像我們為了compactxregular改變的一樣。給予一個400的寬度。運行一下iphone的模擬器,並且旋轉到橫向,按鈕有了400的寬度,看上去很棒。達到了我們的預想。有一點很好就是你能看到一個所有的約束的列表,這些都是不同的設定的。僅僅選擇你想要在文檔大綱裡面看到的約束,然後來到屬性檢查欄,他們整齊的排列在初始的常數下面。它標註了每一個基於它所應用的設定。
即使我們決定我們想要只在iphone橫向landscape模式下button消失,使用sizeclasses我們只要反向安置views就像我們反向安置一個約束。選擇我們的UIbutton,滾動到屬性偵測器的底部。通過點擊加號按鈕給我們當前的設定添加一個新的安置選項,然後取消勾選它。
就像你看到的,那個view立馬消失了,因為我們在設定裡面反向安置了它,我們立馬就能看到。運行app,你能看到它在縱向的portraitiphone上消失了,但是當你旋轉到橫向的landscape的時候又回來了。當然它也一直安置在ipad上面因為ipad仍然使用的是基本的設定。
4、SizeClasses和ImageAsset及UIAppearence
ImageAsset裡也加入了對SizeClasses的支援,也就是說,我們可以對不同的SizeClass指定不同的圖片了。在ImageAsset的編輯面板中選擇某張圖片,Inspector裡現在多了一個Width和Height的組合,添加我們需要對應的SizeClass,然後把合適的圖拖上去,這樣在運行時SDK就將從中挑選對應的Size的圖進行替換了。不僅如此,在IB中我們也可以選擇對應的size來直接在編輯時查看變化。
實際做起來實在是太簡單了..但拿個demo說明一下吧,比如下面這個實現了豎直方向Compact的時候將笑臉換成哭臉--當然了,一行代碼都不需要。
另外,在iOS7中UIImage添加了一個renderingMode屬性。我們可以使用imageWithRenderingMode:並傳入一個合適的UIImageRenderingMode來指定這個image要不要以Template的方式進行渲染。在新的Xcode中,我們可以直接在ImageAsset裡的RenderAs選項來指定是不是需要作為template使用。而相應的,在UIApperance中,Apple也為我們對於SizeClasses添加了相應的方法。使用+appearanceForTraitCollection:方法,我們就可以針對不同trait下的應用的apperance進行很簡單的設定。比如在上面的例子中,我們想讓笑臉是綠色,而哭臉是紅色的話,不要太簡單。首先在ImageAsset裡的渲染選項設定為TemplateImage,然後直接在AppDelegate裡加上這樣兩行:
| 12 |
UIView.appearanceForTraitCollection(UITraitCollection(verticalSizeClass:.Compact)).tintColor=UIColor.redColor()UIView.appearanceForTraitCollection(UITraitCollection(verticalSizeClass:.Regular)).tintColor=UIColor.greenColor() |
完成,只不過拖拖滑鼠,兩行簡單的代碼,隨後還能隨喜換色,果然是大快所有人心的大好事。