概述
摘要:熟悉Auto Layout
概念:Auto Layout
1. 設置
2.進階Auto Layout
3.代碼中的Auto Layout:添加限制
4.Auto Layout規則和優先級:constrainWithVisualFormat
5.總結
設置
在這個技術項目中你會學到更多關于Auto Layout這一iOS讓你用來設計布局的富有表現力的東西。我們在項目2中用它來保證國旗按鈕在正確的位置,但那個項目有個問題:如果你旋轉你的設備,國旗就不對了!
所以,我們會先修復項目2這樣它能帶來更多關于進階Auto Layout技術的知識,然后看下如何在代碼中使用它的方法。
第一步,復制項目2,重命名為項目6a,然后在Xcode中打開,好了,現在開始吧!
進階Auto Layout
當你運行這個項目時,豎直方向很OK,但是橫屏時就不行了——有個按鈕被擋住了。你有兩個選項:要么不允許橫屏,要么讓布局在兩個方向都可以使用。
不允許橫屏明顯不是個好辦法,但有時卻是正確的。大多數游戲,比如阻止橫屏就是因為兩個方向都支持沒有意義。如果你想要這樣做,按下Cmd+1顯示項目導航窗格,選擇你的項目,然后右邊會出現一個帶有“PROJECT”和“TARGETS”的窗格,中間還有些信息。
請注意:這個項目和目標列表可以被隱藏,方法是點擊項目編輯器頂部左邊的關閉按鈕(就在一個四個方格的圖標正下方),而你可能正好發現你的已經被隱藏了。我強烈建議你顯示這個列表——隱藏起來只會讓東西更難找,所以,確保你可以看見它!
現在你看到的就是項目編輯器,包含了很多能影響你的app工作的選項。你會在將來經常使用它,所以記住怎么找到它!選擇TARGETS下面的Project2,然后選擇General tab,接著往下拉,直到你看到設備方向的四個勾選框,你可以選擇你想要支持的方向。
在后續的項目中你會需要支持一些精選方向,但現在我們實現最好的解決辦法:向Auto Layout中添加額外的規則,這樣它就能在橫屏方向也能很好顯示。
在IB中打開Main.storyboard,選擇底部的國旗,然后Ctrl拖拽到它下面視圖控制器的空白區域中。拖拽的方向很重要,所以請垂直向下拖拽。
當你松手時,一個帶有“Bottom Space to Bottom Layout Guide”選項的彈窗會出現——請勾選。它創建了一個新的Auto Layout規則即國旗的底部必須距離視圖控制器的底部X點的距離,X值等于現在國旗底部跟視圖控制器的距離,無論多少。
盡管這是個有效的規則,但它還是會固定你的布局,因為我們現在有了一套完整的垂直方向上的規則:頂部的國旗距離視圖控制器頂部36點,中間的距離頂部的30點,底部的距離中間的30點,從底部國旗的底部到視圖控制器的底部距離為X。這個X對我來說現在是140,你的可能不一樣。
因為我們已經告訴Auto Layout整個空間有多大,它會把它們整個都加起來然后再把剩下的在三面國旗中間的距離以它認為最好的方式分配好。這就是,國旗現在都必須在垂直方向上被來填滿這個空間,這也是我們最不想要的。
相反的,我們會告訴Auto Layout哪些地方是靈活的,就在剛創建的新的底部規則中。底部的國旗不是非得距離底部140點——距離必須是某個特定的值讓它可以不碰到邊沿。如果還有更多空間,那很好,Auto Layout會利用起來,但我們關心的是最小值。
選中第三面旗子來看它的藍色限制條件列表,然后(仔細點!)選中我們剛添加的底部國旗的限制條件。點擊右邊的屬性觀察器,你會看到Relation設置為Equal,Constant設置為140(或其他什么值,取決于你自己的布局)。
你需要做的是把Equal改成“Greater Than or Equal”(大于等于),然后把Constant的值改成20。這樣規則就變成“距離底部至少是20,但你可以給它更多”。你在做的時候布局不會自動修正,因為結果是一樣的。但至少現在Auto Layout知道它在拉升國旗之外還有其他的靈活性可用。
我們的問題還沒修復:在橫屏時,iPhone4s只有320點的空間來操作,所以Auto Layout會根據布局規則消除一面或者甚至兩面。消除國旗不是很好,大小不一的國旗也不太好,所以我們要增加更多的規則。
選中第二個按鈕,然后Ctrl拖拽到第一個上。然后在給出的選項列表中,選擇Equal Heights。 現在在第三個按鈕到第二個按鈕上重復同樣的動作。這個規則保證三面旗子總是高度相同,所以Auto Layout再也不能通過擠壓一個按鈕來滿足規則,而是要同時均等擠壓三個。
這樣能修復部分問題,但另外一些角度來說,問題更嚴重了?,F在被壓扁的旗子從一面變成了三面!但再加上一條規則,我們就能讓旗子不再被壓扁。選中第一個按鈕,然后Ctrl拖拽到上面一點——但是還在按鈕中!當你松掉左鍵時,你會看到“Aspect Ratio”屬性,勾選它。
Aspect Ratio限制為的就是一次性解決所有的擠壓問題:它代表的是如果Auto Layout要縮減國旗的高度,它就會相應地縮減國旗的寬度,也就是說,國旗總是跟原來的看起來一樣,只是大小不同。給另外兩面旗子也添加Aspect Ratio,然后再運行你的app,它就會在兩個方向都表現不錯,感謝Auto Layout!
原文鏈接:
https://www.hackingwithswift.com/read/6/2/advanced-auto-layout
代碼中的Auto Layout:添加限制
在Xcode中新建一個Single View Application project,命名為Project6b,目標為iPhone。我們要手動創建一些視圖,然后用Auto Layout安置它們。把下面的代碼放入你的viewDidLoad()方法中:
override func viewDidLoad() {
? ? ? ?super.viewDidLoad()
? ? ? ?let label1 = UILabel()
? ? ? ?label1.translatesAutoresizingMaskIntoConstraints = false
? ? ? ?label1.backgroundColor = UIColor.redColor()
? ? ? ?label1.text = "THESE"
? ? ? ?let label2 = UILabel()
? ? ? ?label2.translatesAutoresizingMaskIntoConstraints = false
? ? ? ?label2.backgroundColor = UIColor.cyanColor()
? ? ? ?label2.text = "ARE"
? ? ? ?let label3 = UILabel()
? ? ? ?label3.translatesAutoresizingMaskIntoConstraints = false
? ? ? ?label3.backgroundColor = UIColor.yellowColor()
? ? ? ?label3.text = "SOME"
? ? ? ?let label4 = UILabel()
? ? ? ?label4.translatesAutoresizingMaskIntoConstraints = false
? ? ? ?label4.backgroundColor = UIColor.greenColor()
? ? ? ?label4.text = "AWESOME"
? ? ? ?let label5 = UILabel()
? ? ? ?label5.translatesAutoresizingMaskIntoConstraints = false
? ? ? ?label5.backgroundColor = UIColor.orangeColor()
? ? ? ?label5.text = "LABELS"
? ? ? ?view.addSubview(label1)
? ? ? ?view.addSubview(label2)
? ? ? ?view.addSubview(label3)
? ? ? ?view.addSubview(label4)
? ? ? ?view.addSubview(label5)
}
先暫時不管這些代碼有什么用,請把下面的方法加到viewDidLoad()后面某個位置:
override func prefersStatusBarHidden() -> Bool {
? ? ? ?return true
}
這個方法告訴iOS我們不想在這個視圖控制器現實iOS狀態欄——這就是告訴你現在幾點的數據位。
OK,回到viewDidLoad():代碼總共創建了5個UILabel對象,每個都有特定的文本內容和背景色。所有視圖隨后都被view.addSubview()添加到我們的視圖控制器中。同時我們還把標簽的translatesAutoresizingMaskIntoConstraints屬性設置為false,因為iOS默認根據視圖的尺寸和位置來生成Auto Layout限制條件。我們要手動完成,所以就禁止了這一功能。
如果你現在運行app,在頂部你會看到一些重疊著的彩色標簽,所以它看上去像是“LABELS ME”。這是因為我們的標簽都在它們的默認位置上(頂部居左),大小也是根據內容來的。
我們要做的是添加如下內容的限制條件:每個標簽都從父視圖的左邊沿開始到右邊沿結束。我們要用Auto Layout Visual Format Language(VFL)來做這件事,VFL類似于用鍵盤符號來畫出布局的方法。
做之前,我們需要創建一個我們想要布局的視圖的字典。這雖然是一種新的數據類型,但很好懂。你已經知道數組,也就是可以按順序讀取值的類型,比如myArray[0]讀取的是該數組的第一個元素。字典讀取值時用的不是數字,而是你制定的作為獲取方法(key)的任何對象,比如你可以通過myDictionary["name"]讀取你想要的值。
用VFL的理由很快就會出來了,但你得在addSubview()后面先加上下面的字典:
let viewsDictionary = ["label1": label1, "label2": label2, "label3": label3, "label4": label4, "label5": label5]
這里創建了一個帶字符串(key)和UILabel(value)的字典。所以,要得到label1,我們仙子阿可以用viewDictionary["label1"]。看上去可能有點多余,但一會兒你就會知道:輪到VFL了!
在剛創建的字典下面添加以下代碼:
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[label1]|", options: [], metrics: nil, views: viewsDictionary))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[label2]|", options: [], metrics: nil, views: viewsDictionary))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[label3]|", options: [], metrics: nil, views: viewsDictionary))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[label4]|", options: [], metrics: nil, views: viewsDictionary))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[label5]|", options: [], metrics: nil, views: viewsDictionary))
量很大,但實際上只是同樣的事情重復了五遍。所以我們可以改寫成循環:
for label in viewDictionary.keys {
? ? ? view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[\(label)]|", options: [], metrics: nil, views: viewsDictionary))
}
上面我們用字符串插值把key放入到了VFL。
讓我們簡化下,然后集中注意力到剩下的內容中:
view.addConstraints():向我們視圖控制器的視圖添加了一組限制條件。用一組而不是一個限制是因為VFL可以同時創建多個。
NSLayoutConstraint.constraintsWithVisualFormat()是把VFL轉換成一組限制條件的Auto Layout方法。它接受很多參數,但最重要的是第一個和最后一個。
我們給選項參數一個空數組[],給規則參數一個nil。你可以用這些選項來定制VFL的含義,但現在我們暫時不管。
這是最簡單的部分。所以,讓我們看下VFL本身:"H:|[label1]|"。這是個字符串,它描述了我們想要的布局外觀。VFL把它轉化成Auto Layout限制,然后添加到視圖中。
H:代表我們要定義一個水平方向的布局;我們很快還會做個垂直方向的。符號“|”代表視圖的邊沿。我們要把這些限制添加到視圖控制器的主視圖中,所以它很高效地表示了“視圖控制器的邊沿”。最后,我們用[label1]表示“把label1放這里”。想象“[]”表示視圖的邊沿。
所以,"H: |[label1]|"表示“水平方向上,我希望我的label1從我的視圖的左邊到右邊?!钡@里有個小插曲,什么是“label1”?是的,我們知道它是什么因為這就是我們的變量名,但變量名只是對人類有用——程序運行時變量名并不實際被保存和使用。
這就引入了我們的字典viewDictionary:我們把key設置成字符串,value設置成UILabel,然后“label1”就是我們的標簽。這個字典是跟著VFL一起的,被iOS用來找從VFL那里得來的名字。所以當它看到[label1],它會在字典中找到key“label1”,然后生成Auto Layout限制。
以下是VFL行的完整解釋:視圖中我們的每個標簽都要挨在一起放置。如果你現在運行程序,就是你看到的那種,但它同時也高亮了我們的第二個問題:我們沒有垂直方向的布局,所以雖然所有的label都是肩并肩挨在一起,但他們全都是重疊著的。
我們會用另外一組限制條件來修復,雖然只有一行但很長:
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[label1]-[label2]-[label3]-[label4]-[label5]", options: [], metrics: nil, views: viewsDictionary))
跟之前的五行一樣,除了VFL部分。這次我們指定V:,表示這些限制實在垂直方向上的。我們在VFL中有多個視圖,所以會生成大量限制。這次VFL中的新東西是“-”符號,代表“空格”。默認為10點,但你可以自定義。
記住我們的垂直VFL后面沒有一豎,所以我們不會讓最后一個標簽拉伸到視圖的邊沿。這會在最后最后一個標簽的后面留下空白,正好是我們想要的。
如果你現在運行APP,你會看到5個標簽水平方向上相互緊挨著,垂直方向排列整齊。如果要換做Ctrl拖拽操作,工作量巨大,所以我希望你可以為VFL的能力點個贊!
原文鏈接:
https://www.hackingwithswift.com/read/6/3/auto-layout-in-code-addconstraints
Auto Layout規則和優先級:constrainWithVisualFormat
現在我們有了一個可以干活的布局了,但還是非常的基礎:標簽不是很高,而且沒有應對最后一個標簽的底部可能被底邊擠壓的規則。
要修復這個問題,我們會為底邊添加一個限制條件,即最后一個標簽的底部必須離開視圖控制器的底部10點的距離。我們也會告訴Auto Layout,我們希望這5個標簽的高都是88點。用下面的代碼來替換之前的垂直限制條件:
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[label1(==88)]-[label2(==88)]-[label3(==88)]-[label4(==88)]-[label5(==88)]-(>=10)-|", options: [], metrics: nil, views: viewsDictionary))
區別在于我們現在在圓括號內有了數字:(==88)是給標簽,(>=10)是給底部的空間。注意,當指定空間的大小時,你需要在尺寸的前后使用“-”,比如,-(>=10)-。
我們指定了兩種尺寸:==和>=。第一種表示“完全等于”,第二種表示“大于等于”。所以,我們的標簽一定會是個準確尺寸,我們確保底部還有些空間的同時也讓它更加靈活——它至少會是10點,但會不會是100點甚至更多取決于環境。
實際上,等一等。標簽尺寸我不想要88點了,我想要80點。去把所有的標簽都改成80點高。
What?!這就像是你剛收到了一封來自你的IT主管的E-mail:他認為80點的標簽更苗條;他們得是64點,因為所有的好尺寸都是2的幾次方。
現在,看上去你的主觀和你的設計師要干一架了。打了一會兒之后,他們決定擱置爭議,取一個中間值:72。所以請繼續把所有的標簽都改成72點高。
已經煩了?你應該會的。這很容易變成一種像素推敲,特別是如果你的app是被一個委員會設計出來的。
Auto Layout有解決辦法,它叫做公制(metrics)。所有這些對constraintsWithVisualFormat()的調用都給了metrics參數nil值,但是時候改改了。你看,你可以給VFL一組帶有名字的尺寸,然后用VFL里面的這一組尺寸而不是硬編碼的數字。比如,我們想要我們的標簽高度為88,我們會創建一個metrics字典:
let metrics = ["labelHeight": 88]
然后,之前無論何時我們寫過的==88都可以直接寫labelHeight。所以,把現在的垂直限制改成下面的內容:
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[label1(labelHeight)]-[label2(labelHeight)]-[label3(labelHeight)]-[label4(labelHeight)]-[label5(labelHeight)]->=10-|", options: [], metrics: ["labelHeight": 88], views: viewsDictionary))
所以當你的設計師、經理、書呆子決定88點是錯的而你想要改成其他的數字,你可以只用改一個地方就完事兒了。
在我們結束之前,我們還要再做一個改動讓整體的用戶體驗更棒,因為現在它還是不太完美。具體的話就是,我們要讓所有的標簽統一高度,然后給頂部和底部加上一些限制。這在豎直方向還好,但在水平方向你可能沒有那么多空間來滿足所有的限制。
以我們現在的輪廓,你會在app畫面旋轉到橫屏時看到下面的信息:“無法同時滿足所有的限制條件?!边@意思是說你的限制只是不工作,不論給多少功屏幕空間都不做,這就是優先權的由來。你可以給任何的限制一個優先級,然后Auto Layout會盡全力讓它工作。
限制優先級是一個介于1和1000之前的值,1000表示“必需”,其他就是可要可不要。默認中,你的所有限制優先級都是1000,所以Auto Layout會沒辦法在我們當前的布局中找到解決辦法。但如果我們讓高度變得可選——甚至變成優先級999——這也意味著Auto Layout可以找到完成我們的布局的解決辦法:縮小標簽來適應即可。
重要的是要理解Auto Layout不是直接丟棄它無法滿足的規則——它還是在盡全力去滿足它們。所以在我們的例子中,如果我們讓88點的高度變得可選,Auto Layout可能會讓它們變成78或者其他數字。這就是,它還是會盡全力讓它們接近88。TL;DR:限制是按優先級從高到低的順序考慮的,但所有的都會被考慮到。
所以,我們要把標簽高度的優先級改成999。但我們也要再改動一下,就是告訴Auto Layout我們想要所有的標簽高度一致。這很重要,因為如果他們使用labelHeight時全都是可選高度,Auto Layout可能會用一個高一個低的標簽來解決布局問題。
從視圖自己的角度來說,它成功地讓有些標簽達到88高度,所以它可能對自己很滿意了,但它讓用戶界面很不平整。所以我們會讓第一個標簽使用labelHeight的優先級為999,而其他的標簽跟第一個標簽高度相同。這是新的VFL代碼:
"V:|[label1(labelHeight@999)]-[label2(label1)]-[label3(label1)]-[label4(label1)]-[label5(label1)]->=10-|"
這里的@999給了限制一個優先級,告訴Auto Layout讓他們高度相同的方法是把(label1)用在其他標簽的尺寸位置上。
好了:你的Auto Layout輪廓已經完成了,現在app可以在橫豎屏都安全運行了。
原文鏈接:
https://www.hackingwithswift.com/read/6/4/auto-layout-metrics-and-priorities-constraintswithvisualformat
總結
世界上有兩種iOS開發者:一些人用Auto Layout,另一些蠢貨。這是個略陡峭的學習曲線,但這是一種特別令人印象深刻的創建可以自適應各種設備的偉大布局的方式。
大多數人推薦你盡量在IB中完成一些任務,是因為好的理由——你可以隨意拖拽各種元素,而且可以直觀的看到所有的東西都長什么樣,而且它還會提醒你是否存在問題。但,你也看到了,在代碼中創建限制相當的簡單,這要歸功于VFL(Visual Format Language),所以你可能發現你自己混用兩種辦法來得到最好的結果。
原文鏈接:
https://www.hackingwithswift.com/read/6/5/wrap-up