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的狀態中都會存在。這種關係應該叫做繼承關係,具體的四種界面描述與可繼承的界面描述如下:
1
2
3
4
|
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進行判斷,並進行重新佈局以及動畫。代碼看起來大概會是這個樣子:
1
2
3
4
5
6
7
8
9
10
11
12
|
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都棄用了:
1
2
3
4
5
6
|
@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里加上這樣兩行:
1
2
|
UIView.appearanceForTraitCollection(UITraitCollection(verticalSizeClass:.Compact)).tintColor=UIColor.redColor() UIView.appearanceForTraitCollection(UITraitCollection(verticalSizeClass:.Regular)).tintColor=UIColor.greenColor() |
完成,只不過拖拖鼠標,兩行簡單的代碼,隨後還能隨喜換色,果然是大快所有人心的大好事。