通用的Storyboard
通用的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
文件時就需要你手動進行勾選了。
設置你的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 圖片,按下空格:
現在將 cloud_small@2x.png 圖片拖至 2x 圖片區域,將 cloud_small@3x.png 圖片拖至 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的大小以滿足約束:
預覽助手編輯器(Preview Assistant Editor)
一般情況下,在這個時候我們應該會在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 確實是一項重大改進,是程序員和設計人員的福音!
由於本文的示例是一個天氣應用,所以光有天氣圖標不行,我們還得加上城市和溫度才行。
給TextContainer中添加內容
打開 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下不出所料的無法直視,字體太大了:
接下來讓我們解決這個重疊和字體大小的問題。
Size Classes
通用的storyboard
文件固然很好,但是你想真正把它玩轉還是得花功夫去研究它,這是一件很有挑戰性的工作,當然我們也要懂得運用一些現有的工具來幫助我們。Xcode6就為我們提供了一些工具和技巧,幫助我們更好的實現自適應布局。
自適應布局有一個很重要的概念就是 Size Classes。它並不代表真正的尺寸,而是我們從感官上感覺尺寸的種類,通過這種種類的組合,表示出不同屏幕尺寸設備的橫屏及豎屏。
Xcode6為我們提供了兩個種類:普通(Regular)和緊湊(Compact)。雖然它們涉及到視圖的物理尺寸,但一般它們只代表視圖的語義尺寸,即不是真正的尺寸,而是我們從感官上分出的尺寸種類。
下面這個表格向同學們展示了size classes和各個尺寸設備豎屏、橫屏之間的關系:
上表中的這些size classes組合都是我們在開發應用中經常碰到的。然而你也可以在視圖的任何一個層級中覆蓋這些size classes。當以后我們開發Apple Watch應用時顯得尤其有用。
Size Classes與開發者
何為設計應用的UI?雖然現在你們的應用已經知道要使用Size Classes,並且你們在storyboard
文件中設計應用界面時已經拋開了具體尺寸大小的束縛。但是你們難道沒有發現在所有尺寸的設備中,不管是豎屏還是橫屏,應用的界面布局都是一致的嗎?只是自適應了尺寸大小而已。這還遠遠不是設計。
當你們決心要設計自適應的界面並已經開始設計的時候,有一點很關鍵。那就是要知道界面在不同的Size Classes要有繼承的關系。你們應該首先設計一個基礎的界面,然后根據不同尺寸的橫豎屏在基礎的界面上進行自定義。千萬不要把不同的Size Classes當做獨立的屏幕尺寸去設計UI。應該在你們的腦海中建立起界面的一個繼承關系的思想,也就是大多數的設備使用基礎界面,然后特別的尺寸及橫豎屏再根據情況基於基礎界面修改。
在本文中,一直沒有像大家介紹過如何設置特殊設備的布局,那是應為自適應布局的核心概念Size Classes本身就是由各種特殊設備的特點抽象而來的。也就是說一個Size Classes就意味着一種特殊設備的布局特點,其實普通情況也是特殊情況中的一種。所以說我們可以組合不同的Size Classes來滿足各種特殊的布局情況,比如一個支持自適應的視圖,它可以在應用的父視圖控制器中自適應,也可以在某一個功能的視圖控制器容器中自適應。但是兩者自適應后的布局卻不相同。
這種改進對Apple本身也是有益的,因為他們不斷的改變移動設備的尺寸,但從來沒有強迫開發者和設計者重新開發和設計他們的應用以適應新尺寸的設備。這就不會讓開發者和設計者對Apple不斷改變設備尺寸這件事有抗拒心理。
接下來,我們將自定義Size Classes以適應iPhone橫屏的時候,因為現在的布局在橫屏時用戶體驗很糟糕。
使用Size Classes
回到 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 中設置剛剛添加的兩個約束:
- 將 Top Space to: Top Layout Guide 約束的值設置為0。
- 將 Equal Width 約束的 Multiplier 的值設置為0.5。這里要注意 First Item 和 Second Item 這兩個屬性的值。前者應為TextContainer view,后者為view controller view。如果不一致,那么點擊任意一個輸入框,選擇 Reverse First and Second Item。
現在點擊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 Height size 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下的橫屏、豎屏是多么的完美: