通用的stroyboard
文件是通向自適應布局光明大道的第一步。在一個storyboard
文件中適配iPad和iPhone的布局在iOS8中已不再是夢想。我們不必再為不同尺寸的Apple移動設備創建不同的storyboard
文件,不用再苦逼的同步若干個storyboard
文件中的內容。這真是一件美好的事情。
我們打開Xcode,新建一個項目:
選擇iOS\Application\Single View Application
創建一個單視圖應用:
設置項目名稱AdaptiveWeather,語言選擇Swift,設備選擇Universal:
創建好項目後,我們在項目目錄結構中可以看到只存在一個storyboard
文件:
Main.storyboard文件就是一個通用的storyboard
文件,它可以適配目前所有屏幕尺寸的Apple移動設備。打開該文件,同學們會看到一個View Controller,以及一個我們不太熟悉的界面尺寸:
同學們不要吃驚,沒錯,你們看到的就是一個簡單的、有點大的正方形!大伙都知道,在上一個版本的Xcode中,storyboard
裡的屏幕尺寸都對應著我們所選的目標設備的尺寸,但是這樣無法讓我們達到“用一個storyboard
搞定所有設備”的宏偉目標。所以在iOS8中,Apple將storyboard
中屏幕的尺寸進行了抽象處理,也就是說我們看到的這個正方形是一個抽象的屏幕尺寸。
我們接著往下走,選中Main.storyboard
文件,然後在右側工具欄中選擇File Inspector頁簽,然後勾選Use Size Classes選項:
在新的iOS8項目中,該選項默認是勾選的。但當你使用老版本的項目創建新的storyboard
文件時就需要你手動進行勾選了。
首先,我們打開Main.storyboard
文件,從組件庫(Object Library)中選擇Image View拖拽到View Controller中。選中剛剛拖入的Image View,在右側工具欄選擇Size Inspector頁簽,設置X坐標為150,Y坐標為20,寬為300,高為265。
然後再拖入一個View組件,設置X坐標為150,Y坐標為315,寬為300,高為265。
選擇你剛才拖入的View,在右側工具欄中選擇Identity Inspector頁簽,在Document面板中的Label屬性輸入框中輸入TextContainer。這個屬性的作用就是給View起一個名字,方便我們辨認。這裡要注意一下,Document面板有可能是隱藏的,我們需要點擊它後面的Show按鈕來顯示它。我們拖入的這個View最後是顯示城市和溫度Label的容器。
完成上面的設置後,同學們可能會發現剛才拖入的View貌似看不到,這是因為它的背景色和View Controller的背景色是相同的,都是白色,所以我們不太容易辨別。我們來解決這個問題,選中View Controller的View,然後在右側工具欄中選擇Attribute Inspector頁簽,設置背景色為紅:74,綠:171,藍:247。然後再選擇TextContainer,就是我們拖入的View,設置背景色為紅:55,綠:128,藍:186。此時Main.storyboard
文件中應該是這番景象:
到目前為止,我們在View Controller中添加了兩個組件Image View和View,這也是僅有的兩個組件,接下來我們就要給它們添加一些布局約束了。
選擇image view,點擊底部自動布局工具欄中的Align按鈕,勾選Horizontal Center in Container選項,將後面的值設置為0,點擊Add 1 Constraint按鈕添加第一個約束。
這個約束的意思是讓image view在它的容器(View Controller的View)中保持居中。
然後再點擊底部自動布局工具欄中的Pin按鈕,添加一個image view頂部與容器頂部間距的約束,我們設置為0:
上面這兩個約束使image view處於容器居中的位置,並且它的頂部與容器頂部有一個固定的間距。現在我們需要添加image view和text container view之間的約束。同學們先選中image view,然後按住Ctrl鍵和鼠標左鍵,從image view往text container view移動鼠標:
松開鼠標左鍵後會彈出一個約束菜單,我們選擇Vertical Spacing:
這個約束決定了image view底部和text container view頂部之間的距離。
現在選中image view然後點擊右側工具欄中的Size Inspector頁簽,同學們會發現這裡在Xcode6中和之前的Xcode版本有所不同:
你會看到之前添加的三個布局約束,你可以在Size Inspector中很方便的修改這些布局約束。比如點擊Bottom Space To: TextContainer約束後的Edit按鈕,會彈出約束屬性編輯框,我們讓Constant的值等於20:
然後點擊該彈出框之外的任意地方關閉該彈出框。
你先已經將TextContainer view頂部與image view底部的間距調整到了20,我們還需要添加TextContainer view另外三個邊的間距約束。
繼續選擇TextContainer view,點擊底部的Pin按鈕彈出Add New Constraints窗口,在Spacing to nearest neighbor面板中設置左、右、底部的約束,將值設置為0,然後點擊Add 3 Constraints按鈕添加約束。這裡要注意的是,在設置約束時要將Constrain to margins選項的勾去掉,這樣可以避免TextContainer view產生內邊距:
這三個約束會讓TextContainer view的左、右、底部三個邊與容器的左、右、底部的間距始終為0。
現在Main.storyboard中應該是這番景象:
此時同學們應該會注意到在view上有幾個橘黃色的約束線,這意味著還有一些約束上的問題需要我們注意。不過在運行時storyboard
會自動更新view的大小來滿足它與容器的約束條件。我們也可以點擊底部Resolve Auto Layout Issues按鈕,在彈出框中選擇All Views in View Controller/Update Frames來修復提示的約束問題,但是如果我們這樣做,那麼image view的尺寸就會壓縮成零,也就是會看不到image view。
這是因為我們的image view還有沒有任何內容,但是它有一個缺省的高和寬,並且值為0。進行自動布局的時候,如果被約束的view沒有實際的高和寬,那麼會依照缺省的高和寬來滿足約束條件。
我們接著學習,在項目結構中打開Images.xcassets,然後點擊左下角的+號,在彈出菜單中選擇New Image Set:
雙擊左上角的Image標題將其改為cloud:
我們剛才新建的這個image set其實就是若干圖片文件的一個集合,其中的每一個圖片都會對應一個特定的應用場景,也就是針對與不同分辨率的Apple移動設備。比如說,一個圖片集合可能會包含針對非視網膜、視網膜、視網膜高清三種分辨率的圖片。自從Xcode中的資源庫與UIKit完美結合後,在代碼中引入圖片時我們只需要寫圖片的名稱,程序在運行時會根據當前運行的設備自動選擇對應分辨率的圖片。
注意:如果你以前使用過通過資源庫管理圖片,那麼你可能會發現在Xcode6中會有所不同。那就是3x圖片是怎麼回事?
這個新的分片率是專為iPhone 6 Plus提供的。這意味著每一個點是由3個像素點組成,也就是說3x的圖片比1x圖片的像素多9倍。
目前你的圖片集合中還是空的,同學們可以在這裡下載需要的圖片cloud_images.zip,然後將圖片拖入剛才創建的名為cloud的圖片集合中,將cloud_small.png圖片拖到1x圖片區域:
由於我們的圖片背景顏色是透明的,所以在圖片集合中看到的都是白色的圖片。你可以選中某一個圖片,然後按下空格鍵來預覽圖片。比如選中1x圖片,按下空格:
現在將[email protected]圖片拖至2x圖片區域,將[email protected]圖片拖至3x圖片區域。和之前情況一樣,我們看到的只是白色的圖片,但我們可以通過空格鍵來預覽圖片集合中的圖片。
現在你就可以在image view中設置圖片了。我們回到Main.storyboard中,選中image view,在右側工具欄中選擇Attribute Inspector頁簽,將Image View面板中的Image屬性設置為cloud,然後將View面板中的Mode屬性設置為Aspect Fit:
現在你的Main.storyboard中應該是這番景象:
我們看到storyboard
中一直有橘黃色的約束提示,是時候讓我們來修復它們了。首先選中view controller的view:
然後點擊底部的Resolve Auto Layout Issues按鈕,在彈出菜單的All Views in View Controller面板中選擇Update Frames:
這時,storyboard
會自動根據約束條件重新計算view的大小以滿足約束:
一般情況下,在這個時候我們應該會在iPad、iPhone4s、iPhone5s、iPhone6、iPhone6 Plus這幾個不同尺寸的設備上編譯運行程序,以便測試通用的storyboard
是否能在不同尺寸的設備上正確的自適應。但這確實是個體力活,一遍一遍的更改設備、編譯、運行,多麼苦逼。但上天總是會眷顧我們這些苦逼的程序員,Xcode6提供了Preview Assistant Editor,能在一個界面上顯示出不同尺寸設備的程序運行情況,是否有問題一目了然。
我們打開Main.storyboard,然後選擇View\Assistant Editor\Show Assistant Editor,這時編輯區會分隔為兩部分。再點擊頂部導航欄中的Automatic,在彈出菜單中選擇Preview,最後選擇Main.storyboard (Preview):
現在在Assistant Editor區域會顯示一個4寸的界面:
我們還可以點擊預覽界面底部,名字(比如圖中的iPhone 4-inch)旁邊的地方讓屏幕翻轉為橫屏:
這無疑是針對檢查不同尺寸設備的自適應情況的一項重大改進,但還遠遠不止於此!點擊預覽界面左下角的+按鈕,會彈出當前storyboard
文件支持的各種尺寸的設備,可供我們預覽:
分別選擇iPhone 5.5-inch和iPad,此時我們在預覽界面就可以同時顯示三種尺寸的屏幕:
此時同學們是否注意到4寸的橫屏顯示有點別扭呢?沒錯,它的那朵元太大了,我們可以通過對image view添加其他的約束條件來改善這個問題。
回到Main.storyboard,選擇image view,然後按住Ctrl建和鼠標左鍵,拖動鼠標到View Controller的View上,松開鼠標後會彈出一個菜單,我們選擇Equal Heights:
這時會出現一些紅色的約束提示,這是因為我們剛才加的這個約束條件與之前加過的約束條件有沖突。因為之前我們添加過image view和TextContainer view之間的垂直間距(Vertical Margins)約束,所以image view的高度不可能等於它容器(View Controller的View)的高度。
讓我們來修復該問題,首先在storyboard
的結構目錄中選擇我們剛才添加的Equal Heights約束,然後選擇右側工具欄中的Attribute Inspect頁簽,如果First Item屬性不是cloud.Height,那麼在下拉菜單中選擇Reverse First and Second Item這一項讓First Item的值成為cloud.Height:
接下來將Relation屬性的值設置為Less Than or Equal,將Multiplier的值設置為0.4:
這一系列設置的作用是讓cloud這張圖片的高度要麼等於它自身的高度,要麼等於屏幕高度的40%,最後呈現的效果選擇這兩者中較小的一個高度。
現在你應該注意到了在預覽面板中,4寸的橫屏顯示即時的對你剛才的約束改動做出了響應:
你看看其他尺寸的預覽自動更新了麼?答案那是必須的,所以說Preview Assistant Editor確實是一項重大改進,是程序員和設計人員的福音!
由於本文的示例是一個天氣應用,所以光有天氣圖標不行,我們還得加上城市和溫度才行。
打開Main.storyboard,從組件庫(Object Library)中拖拽兩個Label組件到TextContainer中,位置可以隨意擺放:
先選擇靠上的Label,然後點擊底部的Align按鈕,添加一個Horizontal Center in Container約束,再點擊Pin按鈕,添加一個Top Spacing to nearest neighbor約束,設置其值為10:
然後選擇右側工具欄中的Attribute Inspector頁簽,將該Label的Text屬性設置為Cupertino,Color屬性設置為White,Font屬性設置為Helvetica Neue, Thin,Size屬性設置為150。
這時同學們可能會發現基本看不到剛才設置的文字的全貌,這是因為Label大小的原因。別著急,我們很快就會解決這個問題。
現在選擇另一個Label,按照上述的方法給它也添加一個Horizontal Center in Container約束以及一個Bottom Spacing to nearest neighbor約束,將其值設置為10。打開右側工具欄中的Size Inspector看看:
然後選擇Attribute Inspector將該Label的Text屬性設置為28C,Color屬性設置為White, 將Font屬性設置為Helvetica Neue, Thin, 將Size屬性設置為250。
現在是時候解決Label大小的問題了。選中view controller的view,點擊底部的Resolve Auto Layout Issues按鈕,在彈出菜單中選擇All Views\Update Frames,現在看看storyboard
中發生了什麼:
我們看到了剛才設置的城市和溫度,但是他們有一點點重疊,這可不是我們想要的結果。在我們修改這個問題之前,先看看預覽編輯區的顯示情況。我們發現在iPad下顯示貌似還挺完美:
但是在iPhone下不出所料的無法直視,字體太大了:
接下來讓我們解決這個重疊和字體大小的問題。
通用的storyboard
文件固然很好,但是你想真正把它玩轉還是得花功夫去研究它,這是一件很有挑戰性的工作,當然我們也要懂得運用一些現有的工具來幫助我們。Xcode6就為我們提供了一些工具和技巧,幫助我們更好的實現自適應布局。
自適應布局有一個很重要的概念就是Size Classes。它並不代表真正的尺寸,而是我們從感官上感覺尺寸的種類,通過這種種類的組合,表示出不同屏幕尺寸設備的橫屏及豎屏。
Xcode6為我們提供了兩個種類:普通(Regular)和緊湊(Compact)。雖然它們涉及到視圖的物理尺寸,但一般它們只代表視圖的語義尺寸,即不是真正的尺寸,而是我們從感官上分出的尺寸種類。
下面這個表格向同學們展示了size classes和各個尺寸設備豎屏、橫屏之間的關系:
上表中的這些size classes組合都是我們在開發應用中經常碰到的。然而你也可以在視圖的任何一個層級中覆蓋這些size classes。當以後我們開發Apple Watch應用時顯得尤其有用。
何為設計應用的UI?雖然現在你們的應用已經知道要使用Size Classes,並且你們在storyboard
文件中設計應用界面時已經拋開了具體尺寸大小的束縛。但是你們難道沒有發現在所有尺寸的設備中,不管是豎屏還是橫屏,應用的界面布局都是一致的嗎?只是自適應了尺寸大小而已。這還遠遠不是設計。
當你們決心要設計自適應的界面並已經開始設計的時候,有一點很關鍵。那就是要知道界面在不同的Size Classes要有繼承的關系。你們應該首先設計一個基礎的界面,然後根據不同尺寸的橫豎屏在基礎的界面上進行自定義。千萬不要把不同的Size Classes當做獨立的屏幕尺寸去設計UI。應該在你們的腦海中建立起界面的一個繼承關系的思想,也就是大多數的設備使用基礎界面,然後特別的尺寸及橫豎屏再根據情況基於基礎界面修改。
在本文中,一直沒有像大家介紹過如何設置特殊設備的布局,那是應為自適應布局的核心概念Size Classes本身就是由各種特殊設備的特點抽象而來的。也就是說一個Size Classes就意味著一種特殊設備的布局特點,其實普通情況也是特殊情況中的一種。所以說我們可以組合不同的Size Classes來滿足各種特殊的布局情況,比如一個支持自適應的視圖,它可以在應用的父視圖控制器中自適應,也可以在某一個功能的視圖控制器容器中自適應。但是兩者自適應後的布局卻不相同。
這種改進對Apple本身也是有益的,因為他們不斷的改變移動設備的尺寸,但從來沒有強迫開發者和設計者重新開發和設計他們的應用以適應新尺寸的設備。這就不會讓開發者和設計者對Apple不斷改變設備尺寸這件事有抗拒心理。
接下來,我們將自定義Size Classes以適應iPhone橫屏的時候,因為現在的布局在橫屏時用戶體驗很糟糕。
回到Main.storyboard,點擊底部的w Any h Any,你就可以看到Size Classes的選擇器了:
在這個由9個方格組成的網格中,你就可以選擇你想在storyboard
中顯示的Size Class。一共有9種組合方式:3種垂直的也就是豎屏的(任意尺寸(Any),普通(regular),緊湊(compact))選擇和3種水平的也就是橫屏(任意尺寸(Any),普通(regular),緊湊(compact))的選擇。
注意:這裡有一點需要大家注意。在Size Classes中,有兩個重要的概念叫做水平(Horizontal)和垂直(Vertical)。但是在IB中叫做寬(Width)和高(Height)。但他們是等價的,所以大家記住這個概念有兩種叫法就可以了。
目前我們的布局在緊湊高度(Compact Height)時顯示的很糟糕,也就是iPhone橫屏時。我們來解決這個問題,在Size Classes選擇器中選擇Any Width | Compact Height的組合:
這時你會發現在storyboard
中會立即出現2個變化:
storyboard
中的view controller變成了我們剛才設置的size class。storyboard
底部會出現藍色的長條區域,並顯示出當前我們正在使用的size class。為了在該size class下改變布局,我們要臨時改變一些之前設置好的約束。在自動布局中這種操作有個術語叫做裝配(installing)和卸載(uninstalling)約束。當一個約束在當前的size class中是適用的,我們就將該約束裝配在當前的size class中,如果不適用,我們就卸載它。
選擇image view,在右側工具欄中選擇Size Inspector。你可以看到在image view上添加的所有約束:
單擊鼠標左鍵選擇Align Center X to: Superview約束,然後按下鍵盤上的Delete鍵來卸載該約束。這時我們可以看到在storyboard
中這個約束就立即消失了,並且在storyboard
的結構目中和 Size Inspector中該約束都變成了灰色:
注意:你可以在Size Inspector中點擊All來查看當前size class卸載掉的約束。
鼠標雙擊剛才卸載的那條約束,我們可以看到在約束編輯界面的底部出現了額外的2個選項:
這兩個選項的意思就是這條約束在基礎布局中是可用的,但在當前的Any Width | Compact Height布局中是不可用的。
按照上面的步驟卸載掉image view上的另外3個約束:
現在你就可以添加適合當前size class的約束了。我們添加一個Align/Vertical Center in Container約束,再添加一個Pin/Left Spacing to nearest neighbor約束,其值設置為10:
選擇image view,按住Ctrl鍵從image view上拖拽至view controller的view上,在彈出的菜單中選擇Equal Widths約束。
打開右側工具欄中的Size Inspector頁簽,雙擊Equal Width to: Superview打開該約束的屬性編輯界面。如果First Item屬性的值不是cloud.Width,那麼點擊輸入框,在下拉菜單中選擇Reverse First and Second Item。然後將Multiplier屬性的值設置為0.45。
現在image view在所有的size class中顯示應該都沒有什麼問題了。但是text container view還有點問題。你需要給它添加一個約束,讓它顯示在該size class屏幕的右側。
TextContainer
view現在有兩種約束在身。一種是內部約束,它約束了兩個Label的位置,這些約束在各size class中表現的還不錯。另一種是外部的約束,它們限制了text container view的左、右、底部與它容器的左、右、底部的間距。這些約束在當前的size class中表現的就不盡如人意了。如果想使text container view在當前size class中位於容器的右下角位置,你得卸載掉左側的約束。
選中Left Spacing to nearest neighbor約束:
按Cmd-Delete卸載該約束,和之前一樣,被卸載的約束顯示為灰色。
現在你需要再添加兩個約束將TextContainer限制在正確的位置上。一個是讓text container view的寬度為它容器(view controller的view)寬度的一半。另一個是將text container view固定在頂部。
按理來說,你現在需要選中text container view然後按住Ctrl鍵和鼠標左鍵拖動鼠標到view controller view上,然後選擇約束。但是目前的情況由於image view和text container view占滿了整個view controller,所以你很難選中view controller的view。同學們可以通過storyboard
的結構樹上進行該操作,會容易很多。
在結構樹中選中TextContainer,按住Ctrl鍵和鼠標左鍵,拖動鼠標到結構樹的View上:
彈出菜單中顯示了可用的約束,按住Shift鍵點擊Top Space to Top Layout Guide和Equal Widths約束:
然後選中TextContainer,在Size Inspector中設置剛剛添加的兩個約束:
現在點擊storyboard
界面底部的Resolve Auto Layout Issues按鈕,然後選擇All Views\Update frames。看看發生了什麼變化呢:
到目前為止,我們的布局已經越來越接近完美了,唯一一點不足的就是字體大小的自適應,我們會在下一節解決它!
目前TextContainer中的文字尺寸在iPad設備上,也就是使用普通(Regular)size class顯示還比較正常。但是當使用緊湊(Compact)size class時文字尺寸就顯得太大了,以至於都超出了視圖。不過同學們不要怕,我們照樣可以在不同的Size Classes中設置不同的文字尺寸來做到自適應。
注意:與重寫布局不同,在不同的size class中改變文字的屬性始終會影響基礎布局中的文字。它不能像布局一樣,在不同的size class中設置不同的屬性值。我們通過下面的方法來解決這一問題。
回到storyboard
文件中,將目前的size class改為最基礎的Any Width | Any Height。
選擇顯示Cupertino的Label,打開Attribute Inspector。點擊Font屬性前面的+號:
彈出的菜單內容是讓我們選擇一種size class的組合來重寫該組合下的文字屬性。我們選擇Compact Width > Any Height:
這時就會出現另外一個文字屬性下拉框,針對於我們剛才選擇的Compact Width | Any Heightsize class,我們將字體大小改為90:
再選擇顯示溫度的Label,重復剛才的操作,選擇size class組合時選擇Compact Width > Any Height。設置字體大小為150。
在預覽區域會自動更新我們剛才的設置:
現在看起來稍微好一些了,但是顯示Cupertino的Label被截掉了兩頭。同學們可能會繼續調整字體大小使Cupertino顯示完全,雖然目前看起來完美了,但是當換一個城市名稱後或許又會出現剛才的問題。比如Washington, D.C這麼長的名稱,又比如Kleinfeltersville, PA這個更長的名稱。那麼我們應該如何設計呢?
我們的救世主自動布局(Auto Layout)再次出馬。你只需要給顯示城市名稱和溫度的這兩個Label設置一個相對於TextContainer view的寬度約束即可。選中顯示Cupertino的Label,按住Ctrl鍵和鼠標左鍵,拖動鼠標到TextContainer view,在彈出菜單中選擇Equal Widths約束。對顯示溫度的Label做相同的操作。之後在預覽界面看看發生了什麼:
呃……貌似還是有問題,城市名顯示不完全。Label中的文字長度超出了允許顯示的空間。不過我們可以通過一個選項,讓Label自動判斷當前的空間可以顯示多大的字體。
選擇顯示Cupertino的Label,然後打開Attribute Inspector。將AutoShrink屬性設置為Minimum font scale,將其值設置為0.5。將Alignment屬性設置為Centered:
對顯示溫度的Label做相同的操作。
再來看看預覽區域,是不是在不同尺寸的iPhone橫屏、豎屏下顯示都比較完美了:
是時候在不同的設備上編譯運行我們的程序了。用設備來檢驗才是最保險的。iPhone下的橫屏、豎屏是多麼的完美: