今天這是第六篇筆記,現在回過頭去看,我也沒有想到自己能夠更新到第六篇。我算是一個比較懶的人了,現在已經不太喜歡動手敲代碼了。在寫這幾篇筆記的時候,我需要一邊看英文的文檔,一邊測試代碼,還得考慮怎麼能夠寫明白。這裡有點說明,我的英語水平只是四級,語文水平只能用呵呵評價了,文章中的語句難免會有不通順的地方,希望能夠把語義表述清楚。
閒話多了,回到正題,這篇文章介紹UIStackView和一些Auto Layout的改變。
UIStackView我個人理解是為了解決使用Storyboard添加的約束需要經常變化的情況。我想我們可能都在開發中遇到過修改約束的情況,一般是把約束與一個outlet的約束link起來,然後代碼修改,但是這個操作起來是不方便的。UIStackView通過修改一些簡單的屬性,例如alignment, distribution, and spacing,從而讓UIStackView根據我們的修改自動調整內部的顯示。
Auto Layout的改變主要是介紹layout anchors和layout guides。
打開本章的配套的工程VacationSpots,在iPhone 6模擬器上運行,能夠看到APP有一些UI的問題,不要擔心,在後面將會修復這些問題。簡單梳理一下問題如下:
1. 圖中標出的內容沒有在垂直方向居中
點擊列表中的London Cell進入詳情頁面,最下面的三個按鈕沒有平均分配空間:
點擊WEATHER旁邊的hide按鈕,內容是被隱藏了,但是留下了一塊空白,下面的內容沒有移動上來:
WHAT TO SEE 部分在WHY VISIT的下面會更加合理一點。
現在已經了解了這些問題,下面開始用UIStackView來修改這些問題。打開Main.storyboard,查看如下的Controller scene:
能夠注意到上面的每個對應的控件都有背景顏色,這個只是為了幫助我們查看這些屬性的變化。這些背景顏色在運行的時候都會被去掉,通過如下代碼,如果你想讓它們在運行的時候也顯示注釋掉這些代碼就可以:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjwvYmxvY2txdW90ZT4NCjxwcmUgY2xhc3M9"brush:java;">
// SpotInfoViewController.swift override func viewDidLoad() { super.viewDidLoad() // Clear background colors from labels and buttons for view in backgroundColoredViews { view.backgroundColor = UIColor.clearColor() } ........ }
在Storyboard中的的控件都通過outlet與SpotInfoViewController.swift中對應的屬性進行了關聯。在storyboard中顯示的名字對應SpotInfoViewController.swift對應的變量。
Your first stack view
我們先用Stack View解決我們問題列表中底部按鈕的問題。使用UIStackView能夠在一個坐標抽上分配位置和控件之間的空間。幸運的是將Views嵌入到UIStackView中並沒有太難。在Storyboard中的Spot Info View Controller選擇如下三個按鈕控件:
選擇好三個按鈕後,在Storyboard中點擊如下的按鈕:
當Views被嵌入UIStackView之後,Views的約束都被移除了,同時需要設置UIStackView的約束。選中UIStackView,然後按照如圖添加約束:
這裡有個選擇UIStackView的技巧,由於UIStackView是在按鈕的後面,很不好選中,我們可以按住Shift,右擊,在出現的菜單中列出來當前點擊位置所有的View,我們可以選擇UIstackView。另一種方法我們可以在outline view中選擇。
設置好約束後,能夠看到按鈕顯示如下,第一個按鈕被拉伸了,填充滿了UIStackView的剩余空間。UIstackView有一個Distribution屬性,用於控制Views怎麼在UIStackView中顯示,現在設置的是Fill,即將會填充滿UIStackView。為了這個目的,UIstackView將會根據View的ccontent hugging優先級去拉伸View,最低的將會被拉伸。如果優先級一樣,將會拉伸第一個。
我們的目的是讓View間的距離相等,在Attributes inspector中修改Distribution為Equal Spacing。
運行APP,能夠看到我們的按鈕顯示正確了:
一些思考
思考一下你使用Auto Layout,通過約束實現上面的要求,那將是一種什麼令人”愉悅”的行為。可能你很熟悉Auto Layout,認為這些東西都很簡單,那麼你在考慮一下如果我們後面有需求添加一個按鈕,刪除一個按鈕呢,怎麼辦呢?約束刪除了重新添加嗎?如果使用UIStackView,這些將變得比較簡單,只要我們添加或者刪除View,其他的工作UIstackView就會幫我們做了。
UISTackView的更加深入的講解,將會在下一篇文章中繼續介紹。這裡先介紹一下Auto Layout的新特性:layout anchors和layout guides。
Layout anchors
Layout anchors提供了我們一種簡單的創建約束的方式。
想象一下我們在iOS 9之前創建一個約束,簡直就是天書,但是在iOS 9中使用Layout anchors將會簡單好多,下面是兩種的對比:
// iOS9以前 let constraint = NSLayoutConstraint(item: topLabel, attribute: .Bottom, relatedBy: .Equal, toItem: bottomLabel, attribute: .Top, multiplier: 1, constant: 8) // iOS 9 let constraint = topLabel.bottomAnchor.constraintEqualToAnchor(bottomLabel.topAnchor, constant: 8)
Layout anchors不僅理解起來簡單,而且寫起來也簡單了。
對應於我們在iOS 9以前添加約束時候的attribute,基本都有與之對應的anchor,例如top對應topAnchor,bottom對應bottomAnchor等。Layout Anchor都是直接或者間接繼承自NSLayoutAnchor,上面只是演示了一下相等的情況,我們都知道在約束中又大於小於等,下面列出NSLayoutAnchor的接口文件,從接口文件中能夠清楚的了解到對應的方法:
import Foundation import UIKit /* NSLayoutAnchor.h Copyright (c) 2015, Apple Inc. All rights reserved. */ /* An NSLayoutAnchor represents an edge or dimension of a layout item. Its concrete subclasses allow concise creation of constraints. Instead of invoking +[NSLayoutConstraint constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:] directly, you can instead do something like this: [myView.topAnchor constraintEqualToAnchor:otherView.topAnchor constant:10]; The -constraint* methods are available in multiple flavors to support use of different relations and omission of unused options. */ @available(iOS 9.0, *) public class NSLayoutAnchor : NSObject { /* These methods return an inactive constraint of the form thisAnchor = otherAnchor. */ public func constraintEqualToAnchor(anchor: NSLayoutAnchor!) -> NSLayoutConstraint! public func constraintGreaterThanOrEqualToAnchor(anchor: NSLayoutAnchor!) -> NSLayoutConstraint! public func constraintLessThanOrEqualToAnchor(anchor: NSLayoutAnchor!) -> NSLayoutConstraint! /* These methods return an inactive constraint of the form thisAnchor = otherAnchor + constant. */ public func constraintEqualToAnchor(anchor: NSLayoutAnchor!, constant c: CGFloat) -> NSLayoutConstraint! public func constraintGreaterThanOrEqualToAnchor(anchor: NSLayoutAnchor!, constant c: CGFloat) -> NSLayoutConstraint! public func constraintLessThanOrEqualToAnchor(anchor: NSLayoutAnchor!, constant c: CGFloat) -> NSLayoutConstraint! } /* Axis-specific subclasses for location anchors: top/bottom, leading/trailing, baseline, etc. */ @available(iOS 9.0, *) public class NSLayoutXAxisAnchor : NSLayoutAnchor { } @available(iOS 9.0, *) public class NSLayoutYAxisAnchor : NSLayoutAnchor { } /* This layout anchor subclass is used for sizes (width & height). */ @available(iOS 9.0, *) public class NSLayoutDimension : NSLayoutAnchor { /* These methods return an inactive constraint of the form thisVariable = constant. */ public func constraintEqualToConstant(c: CGFloat) -> NSLayoutConstraint! public func constraintGreaterThanOrEqualToConstant(c: CGFloat) -> NSLayoutConstraint! public func constraintLessThanOrEqualToConstant(c: CGFloat) -> NSLayoutConstraint! /* These methods return an inactive constraint of the form thisAnchor = otherAnchor * multiplier. */ public func constraintEqualToAnchor(anchor: NSLayoutDimension!, multiplier m: CGFloat) -> NSLayoutConstraint! public func constraintGreaterThanOrEqualToAnchor(anchor: NSLayoutDimension!, multiplier m: CGFloat) -> NSLayoutConstraint! public func constraintLessThanOrEqualToAnchor(anchor: NSLayoutDimension!, multiplier m: CGFloat) -> NSLayoutConstraint! /* These methods return an inactive constraint of the form thisAnchor = otherAnchor * multiplier + constant. */ public func constraintEqualToAnchor(anchor: NSLayoutDimension!, multiplier m: CGFloat, constant c: CGFloat) -> NSLayoutConstraint! public func constraintGreaterThanOrEqualToAnchor(anchor: NSLayoutDimension!, multiplier m: CGFloat, constant c: CGFloat) -> NSLayoutConstraint! public func constraintLessThanOrEqualToAnchor(anchor: NSLayoutDimension!, multiplier m: CGFloat, constant c: CGFloat) -> NSLayoutConstraint! }
在上面的接口文件中,我們能夠清楚的了解到NSLayoutAnchor有三個子類:NSLayoutXAxisAnchor,NSLayoutYAxisAnchor,NSLayoutDimension。下面列出了UIView的Anchor都是對應的那種類型:
extension UIView { /* Constraint creation conveniences. See NSLayoutAnchor.h for details. */ @available(iOS 9.0, *) public var leadingAnchor: NSLayoutXAxisAnchor { get } @available(iOS 9.0, *) public var trailingAnchor: NSLayoutXAxisAnchor { get } @available(iOS 9.0, *) public var leftAnchor: NSLayoutXAxisAnchor { get } @available(iOS 9.0, *) public var rightAnchor: NSLayoutXAxisAnchor { get } @available(iOS 9.0, *) public var centerXAnchor: NSLayoutXAxisAnchor { get } @available(iOS 9.0, *) public var topAnchor: NSLayoutYAxisAnchor { get } @available(iOS 9.0, *) public var bottomAnchor: NSLayoutYAxisAnchor { get } @available(iOS 9.0, *) public var firstBaselineAnchor: NSLayoutYAxisAnchor { get } @available(iOS 9.0, *) public var lastBaselineAnchor: NSLayoutYAxisAnchor { get } @available(iOS 9.0, *) public var centerYAnchor: NSLayoutYAxisAnchor { get } @available(iOS 9.0, *) public var widthAnchor: NSLayoutDimension { get } @available(iOS 9.0, *) public var heightAnchor: NSLayoutDimension { get } }
上面UIView的Anchor屬性被分成了三類,同樣我們在設置屬性的時候,也要求同類的屬性的才能設置,比如ViewA和ViewB之間的約束,ViewA-topAnchor和ViewB-bottomAnchor是可以的,ViewA-topAnchor和ViewB-leftAnchor就是不允許的,如果這樣的話編譯器會警告,運行時也會報錯。
注意: whyVisitLabel.topAnchor.constraintEqualToAnchor(whatToSeeLabel.leftAnchor) 這個按照上面的說法應該會報錯的,但是我在運行的時候也沒有報錯,可能是我這裡只是隨便寫出一個做測試的原因,後續我會繼續試驗一下這個知識點,然後再改正。
Layout guides
有時候我們想設置兩個View之間的空間,需要在兩個View之間添加一個不可見的View(dummy view),然後在設置約束。Layout guide可以理解為一個隱形的不可見View,我們能夠使用它的矩形邊緣來布局,我們可以像我們使用View一樣設置約束。使用Layout guide的好處是輕量,而且不會在view的層級中,也不會參與事件的響應過程。layout guide也包含除了firstBaselineAnchor和lastBaselineAnchor之外的View所有的Anchor。
Fixing the alignment bug
下面就利用layout guide來修復列表頁面文字內容上下不居中的問題。看下圖是我們設置的約束,我們設置label距離上面的距離是15,當下面的label顯示一行的時候是正常的,如果要是兩行了,由於上面的約束是固定的,最終就變成了不居中的效果了。
在iOS 9之前,我們想解決這個問題可以把兩個label放置到一個container view容器中,設置這個container view為劇中,這裡面的container view就是不可見的view即dummy view。現在在iOS 9上我們可以使用layout guide來代替這個view。
目前只能通過代碼來添加layout guide。打開VacationSpotCell.swift文件,修改對應代碼:
override func awakeFromNib() { super.awakeFromNib() // TODO: Add layoutGuide code here to center the name and locationName labels vertically // 創建layou guide let layoutGuide = UILayoutGuide() contentView.addLayoutGuide(layoutGuide) // 設置layout guide的約束 let topConstraint = layoutGuide.topAnchor.constraintEqualToAnchor(nameLabel.topAnchor) let bottomConstraint = layoutGuide.bottomAnchor.constraintEqualToAnchor(locationNameLabel.bottomAnchor) let centerConstraint = layoutGuide.centerYAnchor.constraintEqualToAnchor(contentView.centerYAnchor) // 激活layout guide的約束 NSLayoutConstraint.activateConstraints([topConstraint, bottomConstraint, centerConstraint]) }
運行APP發現,部分文字被截斷了:
處理截斷問題
這個原因是我們設置layout guide居中,但是nameLabel(上面的)與super view top的約束還存在,造成了下面的label被擠壓了,這時候我們只要刪除掉這個約束就可以了。但是刪除了之後storyboard會提示錯誤,這時候我們可以使用占位約束,這個主要是為了使storyboard不報錯,在運行的時候並不會使用。
最近一直在加班,斷斷續續整理了好久終於整理完了這篇文章了,可能會有錯誤,還請大家指出。