翻譯自raywenderlich上的一篇教程:Adaptive Layout Tutorial in iOS 11: Getting Started。水平有限,如有謬誤,還請斧正。以下為譯文:
Update 6/20/17: 本教程由József Vesza更新至iOS 11,Xcode 9,Swift 4。原始教程由Sam Davies撰寫。
對iOS App設計師來說,Adaptive Layout的引入,導致「套路」變化甚多。現在,你設計App時,可以只創建一個layout,就能在所有iOS設備上工作——無須多餘的特定平台代碼。
本教程介紹Adaptive Layout。你會學到通用storyboards、size classes、layout、自定義字體,還有經過改良的Interface Builder。
本教程你會創建一個簡單天氣類App的用戶介面——從頭開始創建。如果對Auto Layout不感冒,不要緊;我們第一步會以一種很簡單的方式用Auto Layout構建介面。一行代買都沒寫就搞定了,相信會給你帶來足夠的驚喜。
Universal Storyboards是走向Adaptive Layout的第一步。現在storyboard可以用於iPads和iPhones。不再需要保持每個設備storyboard彼此同步——一個無聊的、充滿坑的過程。
打開Xcode,選擇File\New\Project….
選擇 iOS\Application\Single View App,然後點Next:
Product Name設置為AdaptiveWeather,Language選Swift。所有復選框都無需勾選,然後點擊Next:
創建完項目後,會在Project Navigator看到如下檔案:
無論設備是什麼尺寸,Main.storyboard是唯一的一個storyboard。打開storyboard,可以看到其中包含了一個視圖控制器,目前的尺寸顯示的是iPhone 7屏幕的大小。
選擇storyboard,打開File Inspector,可以找到Use Trait Variations,勾選這個新格式,如下圖片所示(譯者:要選擇View Controller才能顯示如下菜單):
在用新版Xocde創建的新工程中,這個復選框默認是勾選的。如果你打開的是舊項目,可以手動勾選,以便使用新版的storyboard。
打開Main.storyboard,從Object Library中拖拽一個Image View到視圖控制器畫布中。在Size Inspector,設置X:37,Y:20。Width:300,Height:265。
接著,拖拽一個View對象放到剛剛的image view下面,在Size Inspector,設置X:37,Y:340。Width:300,Height:265。
選中剛剛拖進來的view,打開Identity Inspector並在Document下的文本框輸入TextContainer。注意,Document面板有可能被折疊起來了,點擊Show按鈕即可展開。這樣給view起一個名字,可以方便在Document Inspector找到你想要的視圖。最後的city、temperature labels,都會放進剛剛拖進來的view。
通常拖進來的對象,背景顏色都和視圖控制器一樣都是白色,導致難以辨認。選擇視圖控制器的view,打開Attributes Inspector然後設置背景顏色為#4AABF7,可以解決這一問題。(譯者:不懂如何自定義顏色的朋友,參考:)
接著,選擇TextContainer view並設置背景顏色為#3780BA。
視圖控制器看起來應該像以下截圖:
視圖控制器的view,目前只有這兩個views;現在應該為它們設置約束(constraints)了。
選擇image view,點擊layout 工具欄的Align按鈕。勾選Horizontally in Container復選框,確認數值為0,然後點擊Add 1 Constraint。
然後點擊Add New Constraints按鈕,添加頂部間距,約束為20,如下圖所示:
點擊Add 1 Constraint按鈕。
上面添加的兩個約束,確保了image view 距頂部有固定的間距,並且從左到右居中。現在需要設置的是image view和text container view的間距。從image view Ctrl-drag(譯者: 按著Ctrl並拖拽) 到text container view,如下:
接著會顯示約束菜單。選擇Vertical Spacing:
這個約束,限定了image view 底部和TextContainer view頂部的垂直間距。
選擇image view並打開Size Inspector可以看到現在的約束設置情況:
可以看到剛剛添加的三個約束;在Size Inspector,可以很方便地設置每條約束。點擊Bottom Space To: TextContainer約束的Edit按鈕;會彈出一個設置約束屬性的對話框。把Constant設為20。
點擊其他地方,關閉對話框。
現在已經設置了TextContainer view底部離image view有20個點(point)的間隙,但是還需要給其他三個方向添加約束。
選擇TextContainer view,點擊底部的Add New Constraints按鈕彈出對話框。在Spacing to nearest neighbor部分,設置left、right、bottom離父視圖的間距為0。並確保Constrain to margins復選框沒有被勾選。
如下圖所示:
點擊Add 3 Constraints為視圖添加約束。這樣就會把text container view固定在視圖控制器左側、右側、底部邊緣。
你的storyboard現在應該像如下截圖:
你會看到視圖中有一些橙色和紅色的約束;這表示約束有一些錯誤是需要你注意的。當然,storyboard可以自動更新contained view的frames來滿足這些約束,但是現在自動更新的話,image view會被收縮到看不見了(zero size)。
這是因為你的image view還沒有任何內容——也就是說這個視圖的固有(intrinsic)寬高為0。如果沒有給視圖的約束指定物理上(physical)的寬高,Auto Layout就會依賴固有(intrinsic)的寬高。
在Project Navigator,打開Assets.xcassets。下載cloud image.zip並解壓。裡面有三個文件。在Finder(譯者:最新的macOS,把Finder翻譯成「訪達」)中選擇這三個文件,並拖到asset目錄的右邊空白處。
這樣就創建了一個新的image set,並分配好3種尺寸:
現在就可以用image set中的圖片來填充之前的image view了。回到Main.storyboard並選擇image view。切到Attributes Inspector,在Image框內輸入cloud_small,在Content Mode的下拉菜單,選擇Aspect Fit,如下:
現在的storyboard應該長這樣:
謝天謝地,目前好像一切正常;視圖控制器會自動重新組織視圖去匹配新的約束。
如果是以前,現在你就要在不同的版本的模擬器build和run你的工程——還有不同方向(譯者:橫屏、竪屏)——以便預覽、測試。這個過程費時費力,不過現在Xcode 9提供了一個更好的方式進行預覽。
打開Main.storyboard,然後點擊幕布底部的View as按鈕。就會展開選擇菜單:
在Devices,選擇iPhone 4s(Device區域最右邊的按鈕)。
幕布就會切換到另一個形狀:4寸的iPhone屏幕(譯者:iPhone 4s屏幕應為3.5寸,這裡應該是原作者筆誤),如下圖:
要預覽橫屏模式,在Orientation區域,選擇Landscape:
與在多個模擬器中預覽相比,Xcode 9的這個改進簡直不要太方便:點擊一個按鈕,就可以檢查佈局在不同的設備是不是都能工作。
有沒有注意到上面的橫屏預覽有個怪怪的地方?沒錯,那朵「雲」太大了。我們需要往image view增加一個新的約束來修正這個問題。
回到storyboard。從image view上Ctrl-drag到視圖控制器的view,創建一個新的約束。在彈出來的菜單中選擇Equal Heights:
這時候storyboard上的約束會變成紅色。這是因為剛剛加的約束,和已經存在的約束產生了衝突,具體的衝突是:image view要和視圖控制器的view有相同的高度,又要保持之前設定的垂直間距。這兩個約束不可能同時滿足。
在Document Outline(譯者:就是storyboard左邊的導航欄)選擇剛剛添加的約束,打開Attribute Inspector。如果First Item不是cloud_small.Height,就在First Item下拉菜單選擇Reverse First and Second Item,如下:
接著,Relation選擇Less Than or Equal,Multiplier設置為0.4,如下圖:
這就是說,cloud image要麼是本身的尺寸(intrinsic size),要麼就是屏幕高度的40%,以較小者為準。
可以看到,更新了約束之後,幕布的佈局就自動更新了,如下:
完美!
因為這是一個天氣app,現在需要添加一些labels來顯示城市的名字和氣溫。
在Main.storyboard,切換到Portrait iPhone 7,從Object Library拖拽兩個Labels到TextContainer view,位置大概如下:
選擇最上面的label,利用Align和Add New Constraints菜單來將label水平居中,並將頂部間距設置為10(離最近的視圖間距是10),如下:
接著,在Attribute Inspector裡,文本框輸入Cupertino,Color設置為White Color,字體設置為System Thin,Size是150。
可能你會發現文字無法辨認,這是因為label的frame限制導致的——後面會解決。
選擇選擇另一個label,用相同的方法讓其水平居中,再設置label離底部間距為10。Size Inspector現在如下圖:
在Attributes Inspector,輸入28C,顏色設置為Whilt Color,字體是System,Thin,size是250。
storyboard中的兩個labels都超出了邊界,並且重疊,顯然不是我們要的那樣子。不過,在修正之前,在不同的設備上預覽一下,在iPad Pro 9.7"上看起來不錯:
可以想像到,對於iPhone來說,目前的字號太大了:
在下一章節,會修正這個字號的問題。
Universal(通用) storyboard很好用,不過就像你上面發現的問題一樣,只用一種佈局來適配所有屏幕,非常困難。還好Adaptive Layout有一些工具和技巧來解決這些問題。
Adaptive layout的一個核心概念,就是size class,size class可以應用在所有view或者view controller,表示的是:在給定的水平或垂直維度中,能顯示的內容量。
Xcode 提供了兩種size classes:Regular和Compact。雖然這兩種size class和具體的物理尺寸相關,但同時也表示view「語義學」上的尺寸。(譯者:semantic size,我理解就是抽象意義上的尺寸,用Regular和Compact表示「常規」和「緊湊」兩種「尺寸」)。
下面的表格,顯示了size class 如何應用到不同的設備和方向。
Size class意味著什麼?雖然app知道size calss,但是你的layout對於size class不可知的——換言之,layout在所有size class中,都是一樣的。
在進行自適應佈局時,這是一個重要的觀念。應該先構建一個基本佈局,然後根據需要,自定義每個size class。不要把每個size calss都看作是完全獨立、分離的。把adaptive layout想像成一個層次結構,將大家可以共享的設計放在父級中,然後在子size classes中進行必要的修改。
到目前為止,我們都沒有提到為特定的設備進行佈局。這是因為adaptive layout的一個核心理念,就是將size class從特定的設備中抽象出來。這就意味著,支持adaptive layout的的view,可以用在全屏的視圖控制器、容器視圖控制器都可以正常工作,儘管外觀不一樣。(譯者:作者應該是說無論在什麼情況下,layout都是我們想要的)
這對Apple也大有益處,因為這樣可以擴展設備的屏幕,而開發人員和設計人員又無需重新構建他們的apps。
下面將使用size classes來自定義iPhone的橫屏佈局,以為當前的layout,並不滿足要求。
接下來介紹trait variations,確保自己選擇了Compact Height這個設置(譯者:預覽)(比如橫屏的iPhone SE),然後點擊右邊的Vary for Traits按鈕。
這裏可以選擇一種size class來进行定製(基於寬度、高度)。
Note:這裏術語上有一點差異。Size classes一直使用「水平/horizontal」、「垂直/vertical」。但是,IB使用的是「寬度/width」和「高度/height」。這裏是一致的((width = horizontal; height = vertical),只是對同一個概念的不同表述方法。
你現在的layout,在compact heights這種情況下不能正常工作。下面來進行修正,在Vary for Traits菜單(底部右邊)選擇Height復選框:
可以看到底部工具欄馬上變成深藍色。這就表示,你正在處理的,是特定size calss的layout。
為了能夠改變layout,現在需要臨時改變一些約束。在Auto Layout中,有專門的術語來表示:installing和uninstalling約束。如果一個約束是有效的,就是installed(安裝)了,一個uninstalled(卸載了)的約束,就表示在當前size class下是無效的、未激活的。
選擇image view,打開Size Inspector。可以看到對這個view起作用的所有約束:
單擊選擇Align Center X to: Superview約束,然後按下鍵盤的Delete鍵,這樣就在當前size class下卸載了該約束。可以看到,在storyboard上,約束馬上消失了,在Document Outline和Size Inspector該約束變為灰色。(譯者:其實在Size Inspector也是消失了——除非選擇的是All)
Note:如果需要查卸載的約束,可以將Size Inspector中的This Size Class切換到All。
雙擊Size Inspector中卸載的約束。可以看到底部還有一行,如下:
這表示這個約束是用於基礎layout的,但並不適用於Compact Height下的佈局。(譯者:以前一直沒搞懂)
用同樣的方法,把image view的其他三個約束也卸載掉。你的document outline 和 view的Size Inspector介面類似如下:
現在就可以為此size class下添加必要的約束了。使用Align和Pin菜單的Vertically Center in the Container,再設置離左邊的間距為10:
從image view Ctrl-drag到視圖控制器的view,然後在彈出菜單選擇Equal Widths。
打開image view的Size Inspector,然後雙擊Equal Width to: Superview這條約束。如果First Item不是cloud_small.Width,選擇下拉菜單的Reverse First and Second Item。並更新Multiplier為0.45。
現在image view的約束,在所有size class下都能正常工作了(譯者:就是將image view從豎屏的水平居中,在橫屏下改為了垂直居中;並且將寬度設為整個view寬度的45%),但是,txet container在現在的size class下,還需要修改約束,以便讓label能移動到合適的位置。
TextContainer view有內部(internal)約束來定位labels,labels可以按照原來的約束工作。但是,有三條約束——左、右、底部——並不能正常工作。為了將label固定在右下角,需要卸載掉左側的約束。
在document outline選擇TextContainer,在size inspector卸載掉Leading Space。可以在document outline選擇這條約束,size inspector上可以確認是否已經卸載掉:
現在需要為TextContainer添加兩條約束以便正確將它定位。TextContainer的寬度應該設置為視圖控制器的view寬度的一半,並固定在頂部。
理論上,你可以像以前一樣,從TextContainer拖拽到視圖控制器的view上進行設置。然而,在實踐中,經常會有其他元素在view裡面,導致很難選中要拖拽的view。而使用document outline,則簡單得多。
在docuemnt outline中從TextContainer Ctrl-drag到視圖控制器的view:
在彈出菜單中,Shift-click,並選擇Vertical Spacing to Top Layout Guide 和Equal Widths。再點擊Add Constraints創建新的約束:(譯者:就是按著Shift進行多選。如果沒有看到Vertical Spacing to Top Layout Guide字樣,選第三個即可)
打開TextContainer的Size Inspector,更新剛剛添加的兩條約束的值:
storyboard現在應該長這樣:
Layout已經改完了,現在離成品不遠了。字體的字號還有一些問題需要修正——在下一章節會解決這些問題。
在regular size classes(iPad)中,TextContainer當前的字號看起來很好。但是對compact size classes來說,字號太大了。不過不要擔心,在size class中,也可以重新設置字體的大小!
Note:不像重設layout(譯者:約束),更改字體設置,也會影響base layout。所以,修改字體設置,不是在當前size class中重新設置,而應該使用下面的方法。
先點擊右下角的Done Varying按鈕,完成之前的工作。底部工具欄變回灰色,表示回到base layout狀態。
選擇Cupertino文本label並打開Attributes Inspector。點擊Font左邊的小加號:
接著會彈出一個菜單,用於選擇size class——就是你要重新設置字體屬性的size class。Width選擇Compact,Height選擇Any,如下:(譯者:再點擊Add Variation)
接著Xcode會創建另一個字體選擇框,這裏的設置將應用在指定的size class中。我們將這裡的字號設置為90:
現在選擇temperature這個label,重複上面的步驟,在Compact Width,Any Height這個size class下,字號設置為150。
Interface Builder會自動更新,顯示修改後的效果:
這樣看起來好一點了,不過Cupertino這個label被裁切了一點。Cupertino這個城市名太長了,另外還有更長的,要怎麼辦呢?
Auto Layout再次「救場」!這裡只需要簡單地限制label的寬度要匹配TextContainer的寬度即可。從Cupertino label Ctrl-drag 到TextContainer,選擇Equal Widths。
在temperature label重複上面的步驟。幕布更新效果如下:
嗯,有字符被省略了,顯然不是我們想要的效果。當字符多於顯示空間時,這是label的默認行為。不過,我們可以選擇自動調整字號,進行自適應。
選擇Cupertino label並打開Attribues Inspector。在AutoShrink下拉菜單,選擇Minimum Font Scale,並確保值是0.5。同時設置Text Alignment 為居中。如下:
對temperature label重複上面的步驟。
看看Interface Builder的幕布,現在感覺好多了:
在Xcode預覽非常方便,但是我們也要在iPhone的屏幕上看看效果是否OK:
恭喜!你已經學會Adaptive Layout的基本功了。
這裡有完成的項目。
想想看,我們只用一個storyboard,就在所有設備、橫屏豎屏都工作得很好了!
如果沒有人相信Adaptive Layout是未來的發展方向,請考慮這樣一個現實:我們現在的layout,即使在還沒發佈的iOS設備上,也可以正常工作,無需額外適配。
從本教程可以看出,作為一名開發人員,你需要重新考慮app的設計方法。我們應該考慮屏幕上UI元素之間的關係,而不是基於像素的layout。
如果你想學習更多關於Adaptive Layout的知識,可以查看我們的Adaptive Layout video tutorial series,可以讓你從一個Adaptive Layout的初學者成長為大師。看看WWDC2016的Part 1、Part 2也很有用。
畢!