WWDC 2017:高級開發應該掌握的自動佈局技巧

構建 app 時使用的自動佈局技術,其實就是創建視圖與視圖之間關係。而約束是創建視圖間關係的紐帶,幫助咱們的 app 能夠適應各類尺寸的屏幕,在應對花樣百出的佈局需求時遊刃有餘。

本文已收錄至 iOS 成長之路3期·WWDC17內參git

前言

若是你之前從未使用過Autolayout,如今網上已經有不少很優秀的教程,包括往屆 WWDC 中 sessions 視頻資源均可供查看學習。在本文中將再也不重複基本的使用方法,更多的去介紹一些更加複雜的場景中的應用,本文中技術結合實例使你更容易理解吸取。讓咱們一塊兒來看看與Autolayout相關的六種技術與應用,這些內容都很是實用,在平常開發中必定會常用到,相信本文必定不會讓你失望。github

1. 運行時變換佈局(Changing layout at runtime)

一般咱們不只能夠在 app 中使用約束來對視圖進行簡單的定位,也能夠組合使用以達到更復雜的效果。咱們今天要講的第一種技術點,便在運行時改變佈局。以下圖,在咱們界面的頂部,有一個滑塊區域。如今咱們須要一個將滑塊視圖上移而且最終隱藏的功能。編程

頂部滑塊區域

1.1 利用高度約束隱藏視圖

一般咱們但願約束在設置好以後不須要再次調整,儘可能讓結構清晰簡單。如今咱們來思考一下,從佈局的角度使用最簡單的方式實現這個功能,通常狀況下,咱們把這個區域視圖高度縮短至0便可。可是若是咱們真的添加上一個高度約束,而且設置爲0。咱們將在 Interface Builder 中發現一些警告。安全

頂部滑塊約束存在衝突

1.2 避免衝突

在圖片中能夠看到佈局中的這些紅線,這意味着咱們設置的約束存在着一些衝突。之因此出現衝突,是由於咱們設置的這些約束讓佈局引擎去作了一些不能同時並存的事情。而這個衝突出現是由於咱們設置了高度爲0的同時,沒法保持足夠的高度以知足該控件內部的內容顯示。bash

容器

爲了解決這個問題,咱們將 slider 和 label 所在的視圖放進一個warppingView中,如圖中橙色方框。在咱們縮短warppingView的高度時,咱們也要保證warppingView內部子視圖的高度,而且知足子視圖相關的約束,在啓用 clips ToBounds 屬性後,超出warppingView內部座標系範圍的內部控件在顯示時將被裁剪掉。這樣就達到了隱藏視圖元素的效果,以下圖效果,灰色區域將被裁剪不顯示。session

隱藏滑塊區域

讓咱們來看看在 Xcode 中是如何作到的,咱們須要在運行時控制warppingView的高度,因此咱們將爲warppingView手動建立一個高度約束zeroHeightConstraint,在運行時設置zeroHeightConstraint爲0,而且在用戶點擊 Edit 按鈕時,激活該約束。這樣咱們仍然會和以前同樣出現衝突的狀況,咱們須要將滑塊區域視圖底部到warppingView底部邊緣的約束禁用,避免了約束衝突,這樣warppingView就能夠正常縮短高度了。app

禁用底部約束

1.3 實現代碼

接下來看看完整代碼,在咱們控制器的子類中,咱們持有3個屬性:ide

  • warppingView:外部容器視圖
  • edgeConstraint:底部邊緣的約束
  • zeroHeightConstraint:一個存儲0高度約束的屬性
@IBOutlet var warppingView: UIView!
@IBOutlet var edgeConstraint: NSLayoutConstraint!
var zeroHeightConstraint : NSLayoutConstraint!
複製代碼

咱們建立了按鈕點擊事件,在響應按鈕事件函數中,咱們首先要保證zeroHeightConstraint已被建立。接着咱們還但願這一個事件讓視圖能夠在顯示和隱藏間切換,因此咱們要對一些約束作禁用和激活操做,作完這些就會獲得咱們想要的切換效果。函數

@IBAction func toggleDistanceControls(_ sender: Any) {
        if zeroHeightConstraint == nil {
            zeroHeightConstraint = warppingView.heightAnchor.constraint(equalToConstant: 0)
        }
        
        let shouldShow = !edgeConstraint.isActive
        
        if shouldShow {
            zeroHeightConstraint.isActive = false
            edgeConstraint.isActive = true
        }else{
            edgeConstraint.isActive = false
            zeroHeightConstraint.isActive = true
        }
    }
複製代碼

須要特別注意的是,在激活一個約束前務必先禁用另一個約束。在這些簡單的切換禁用和激活代碼,遵照這一點讓咱們避免了衝突,若是約束中一旦存在衝突,控制檯就會提醒咱們:嘿,我檢測到這些約束是互相沖突的😂。例如,咱們激活了zeroHeightConstraint約束,而底部約束edgeConstraint還未被禁用,這個時候咱們就會看到控制檯打印出衝突信息。佈局

1.4 加入動畫

加入這些代碼從新運行後你會發現咱們的界面正確顯示和隱藏了,可是我還想爲這個過程加上動畫,讓用戶能夠看到視圖切換過程可以提升用戶體驗。在這裏咱們使用UIView animation block來實現動畫,UIView animation將捕捉而且動畫化整個過程。

UIView.animate(withDuration: 0.25) {
    self.view.layoutIfNeeded()
}
複製代碼

這裏獲得的動畫效果,也並非我想要的最終效果,咱們還須要作最後一點調整,可是這不須要修改咱們的代碼,咱們只須要將底部邊緣的約束:edgeConstraint屬性更換成鏈接到頂部邊緣的約束,改爲一個底部對齊的效果。整個動畫效果發生改變,我確認這就是我須要的最終效果。

禁用頂部約束

具體效果能夠查看咱們的Demo(非蘋果官方),經過上面這些內容咱們能夠知道,怎樣經過運行時改變約束來動態調整咱們 app 中的佈局。

2. 跟蹤觸摸手勢(Tracking touch)

如今咱們來看看改變佈局的另外一種方法,我保證它既簡單又炫酷。咱們將用它來跟蹤觸摸手勢。咱們在咱們下圖的 app 的中央區域有一張卡片,咱們但願卡片能隨着觸摸手勢移動,隨着靠近邊緣的時候,會有一些旋轉,一旦你的手離開屏幕,卡片就會彈回屏幕中間。

卡片

2.1 frame 飲水知源

一般一個控件在屏幕上的位置由它的 frame 決定,而 frame 又源起何處呢?

  • Layout engine owns frame。當咱們使用Autolayout並使用約束控制此視圖時,佈局引擎將會持有此視圖的 frame 。
    • Value derived from constraints。frame 的值是從這些約束中計算出來的。
  • transform property offsets from frame。還有另外一個屬性會影響視圖在屏幕上的位置,那就是 transform ,在 transform 屬性源起於 frame 。
  • CGAffineTransform = translation + rotation + scale。經過CGAffineTransform,它能夠幫助咱們爲視圖加入平移 ,旋轉和縮放等變換,在從約束中計算出 frame 以後,將其應用在 transform 中。

2.2 加入監聽手勢

再回到需求上,若是咱們想要中間的卡片隨着個人手勢移動,那咱們就要加入一個手勢識別器,而且拖線鏈接到代碼中,添加監聽手勢的方法,在該方法中咱們能夠訪問手勢識別器的各類屬性。此外還將咱們要移動的卡片也經過拖線建立了屬性。

@IBOutlet weak var cardView: UIImageView!
@IBAction func panCard(_ sender: UIPanGestureRecognizer) {}
複製代碼

2.3 加入位移和旋轉

接下來咱們要經過手勢識別器監聽用戶手勢移動,獲得位移結果後轉換成 transform 應用在cardView上。在這裏有transform函數幫咱們進行了位移和輕微的旋轉,這個時候,卡片將會隨着你的手指移動伴隨着輕微的旋轉。

func transform(for translation: CGPoint) -> CGAffineTransform {
    let moveBy = CGAffineTransform(translationX:translation.x, y: translation.y)
    let rotation = -sin(translation.x/(cardView.frame.width * 4.0))
    return moveBy.rotated(by: rotation)
}
複製代碼

2.4 位置還原

可是當我放開手指時,卡片停留在原位,沒有回到屏幕中央,由於咱們並無去重置卡片的 transform 屬性。當我再次觸摸並移動,咱們會看到它會回到原來的位置,這是由於咱們開始了一個新的位移,新的位移關聯的原來的 frame 。總之這不是我想要的效果,我但願在用戶手指離開屏幕後,卡片可以當即回到屏幕中間的位置。咱們能夠經過手勢識別器的狀態來作到這一點。咱們在stateend的時候,將重置 transform 而且加入彈簧動畫。加入這部分代碼運行 app ,在我鬆開卡片後它會彈回中間的位置。

@IBAction func panCard(_ sender: UIPanGestureRecognizer) {
    switch sender.state {
    case .changed:
        let translation = sender.translation(in: view)
        cardView.transform = transform(for: translation)
    case .ended:
        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4,
initialSpringVelocity: 1.0, options: [], animations: {
            self.cardView.transform = .identity
        }, completion: nil)
    default:
        break;
    }
}
複製代碼

簡單的幾行代碼,實現了一個頗有意思的交互效果,在這些內容裏面,咱們能夠看到 frame 它不只是經過約束來計算出,也會受到 transform 的影響,視圖的 frame 中蘊含多種屬性的組合效果。

3. 動態字體(Dynamic type)

Dynamic type是 iOS 中提供了一組文本樣式,文本樣式包含了標題、副標題、正文等樣式,並且用戶能夠控制這些樣式字體大小的技術。在 iPhone 的短信消息中,若是用戶喜歡大一點的字體,經過在設置中進行設置後,咱們將看到消息界面會有所變化,字體變大了,消息氣泡和輸入文本也變大了。日曆和其餘一些地方也具有相似的功能。

改變字體前

改變字體後

相信這個時候你必定會好奇,如何才能在咱們本身的 app 實現這個功能呢?另外在調整字體大小時,若是不相應地調整咱們的佈局,容易形成視圖重疊,對用戶體驗來講是很是很差的。幸運的是,Autolayout能夠很輕鬆地幫咱們搞定這個問題。

3.1 支持 Dynamic Type

因此趕忙讓咱們來看看是怎麼實現的吧,打開 IB 界面,選中你要支持 Dynamic Type 的 label ,查看 label 的屬性,勾選automatically adjust font,若是你眼睛夠敏銳的話,你能看到上面出現了一個警告,緣由是由於automatically adjust font屬性生效,該屬性要求 label 設置指定的文本樣式。這裏要將系統默認字體更換爲caption one,該樣式和默認字體12號大小相對應。

支持Dynamic Yype

3.2 經過 Accessibility Inspector 改變字體大小

設置好這些再從新運行,你會發現和以前並無什麼不一樣,這是由於咱們尚未改變文本樣式的大小,咱們能夠在設置中調整字體大小,可是這種方式須要來回切換不夠直觀,全部咱們用另一種方法,點擊頂部導航條Xcode->Open Develop Tool->Accessibility Inspector->target切換至模擬器->選擇設置標籤,就能夠看到修改字體大小的滑塊了。這個時候滑動滑塊就能看到咱們的 label 字體在實時地改變。若是咱們鏈接了 iPhone ,咱們也能夠將target切換至咱們的 iPhone 。

Accessibility Inspector

3.3 根據字體大小動態調整佈局

在下圖中能夠看到,若是咱們把字體調整到很是大的時候,咱們的 label 就會發生重疊,接下來咱們要解決這個問題。

視圖重疊

首先咱們建立了一個文本區域,這個文本區域會隨着字體變大而增高,因此咱們只要將底部 label 被限制在底部,頂部 label 被限制在頂部,再在二者之間添加了一個垂直間距約束,使兩個標籤始終保持足夠垂直間距,避免使用固定高度約束,這樣 label 的高度會隨着字體變大而增高,接着 label 又會將文本區域給撐高。這個時候能夠打開Accessibility Inspector來測試改變咱們 app 的字體大小,你會發現咱們的文本區域會隨着字體的變化而改變高度。

根據字體大小動態調整佈局

在這中間咱們並不須要作不少處理,就能實現這樣一個很是實用的功能,特別是當你有一些須要讀者閱讀文字的的需求 ,相信 Dynamic Type 可以幫助你,讓你的 app 更增強大。

4. 安全區(Safe area)

接下來要介紹的內容在以後你可能會頻繁使用到,因此必定要搬好小板凳認真看。當你新建了一個控制器,控制器有一個導航條和一個底部標籤欄,如何保證你的內容主體不被導航條和標籤欄遮擋?可能你已經據說過在 iOS 11 上有了新的 layout guide ,稱之爲Safe Area Layout Guide

Safe Area

4.1 Safe Area Layout Guide 更易使用

這是UIView的新特性,它適用於自動佈局,它是夾在導航條和標籤欄之間的一個矩形,在這個矩形區域中你能夠放心地爲你的視圖添加約束。在這以前,你可能不得不使用UIViewControllerTop Layer GuideBottom Layer Guide,如今在Safe Area中這些都已經統統被丟棄了。

使用Safe Area

Safe Area Layout Guide使用起來更加簡單,也更容易理解,如同字面意思,能夠安全的讓你的視圖安全地呆在導航條和標籤欄中間,不被遮擋,無論是尺寸的變化和屏幕旋轉,它都會自動作相應地調整。

使用Safe Area 橫屏

4.2 如何使用 Safe Area Layout Guide

Safe Area也適用在 tvOS 上。若是要將你的 app 和內容放到 tvOS 上,你可能會遇到各類各樣的尺寸的屏幕,在某些狀況以下圖,咱們頂部的標題太靠近頂部邊緣,可能所以被遮擋掉一部分。

頂部標題被截取

這個時候咱們要調整咱們的內容,讓它處於Safe Area之中。Safe Area表明storyboard中的這塊淺綠色的區域,你只要將你視圖中的約束設置到Safe Area中,那它就安全了。

淺綠色的安全區域

而後用一張的美麗背景圖像填充剩餘視圖空間,媽媽在也不用擔憂咱們的內容被導航條和標籤欄擋住了。以下圖,它們只會聽話地呆在深色矩形方框內。

添加背景

4.3 開啓 Safe Area Layout Guide

開啓Safe Area Layout Guide也十分簡單,打開咱們的storyboard,進入 file inspector 標籤頁,而後找到Use Safe Area Layout Guides而且勾選上。你會發現每一個控制器中都會出現一個Safe Area視圖,而後你就能夠像其餘視圖同樣,將約束連向它。

開啓Safe Area

Safe Area Layout Guide是 UIView 的新特性,在以前的版本中頂部和底部的Layout Guides之間的矩形區域將與新的Safe Area相匹配,他們能夠互相轉換,若是在 iOS 11 的故事板中啓用Safe Area,在你選中afe Area Layout Guides勾選框時 Xcode 將會自動升級你的約束。總而言之,在 Xcode 9 的故事板中使用Safe Area Layout Guide,將向下兼容 iOS 老版本的。

5. 比例定位(Proportional positioning)

接下來咱們要談一談,關於如何將一個視圖定位在其 superview 的佈局技術,咱們將其稱之爲 Proportional positioning ,即按 比例定位 。在安卓的佈局技術中也有相似的功能,它的應用面普遍且實用,相信將來的開發中必定會頻繁使用到。

5.1 比例佈局

假設如今我有一個需求,要將咱們 app 中的卡片高度定位在其 superview 高度的70%。也許你會有幾種方式能夠實現上述需求,可是如今我要用一個最直接的方式來實現它,即是我如今要介紹得的使用 spacerview 的方法。

高度爲superview高度的70%

從對象庫拖出一個視圖,只是一個普通的UIView。爲它添加約束後設置隱藏,這樣就不會渲染它,讓它作一個安靜的美男子,這樣它就成爲你須要定位的視圖的參照物。並且這種技術也能夠組合使用,靈活搭配,這裏有另外一個例子,我有一個場景,要遵照1/5,2/5和2/5的比例,而後他們以這些比例填充滿整個屏幕。下面讓咱們來看看如何作到的。

比例1:2:2

5.2 構建 SpacerView

以下圖中咱們已經有一個基本的佈局。我有一個 label 和一個 image ,已經添加了基本約束。 當我選中它們,若是你仔細看,你會注意到它左邊和右邊的約束是藍色的,但頂部和底部是紅色的。這意味着咱們還須要添加一些約束來定位。不管什麼時候在 Interface Builder 畫布中看到紅色,那隻多是兩種狀況,要麼你的約束太少,位置是不肯定的,或者設置了太多的約束,其中一部分是衝突的。

出現衝突

我知道是由於我沒有對垂直方向位置進行固定。因此接下來咱們要經過建立 spacerview 來實現。拖一個UIView出來。首先咱們將其隱藏,這樣不會浪費性能進行繪製,咱們爲其添加好上方、左側以及寬度的約束後,咱們尚未設置其高度約束,咱們要爲其設置等高約束,使其高度與 superview 高度相,以下圖效果。

spacerview

接着讓咱們查看等高約束的屬性,修改比例爲70%,設置成功後你會發現spacerview的高度已經縮減了,下一步設置 Second Item 即比例參考對象視圖爲 Safe Area,這樣咱們的spacerview就已經設置好了。

設置爲 Safe Area 70%高度

5.3 對齊到 Baseline

如今咱們要將咱們卡片視圖底部與spacerview底部對齊,因此咱們添加了底部對齊的約束,若是我想要是spacerview與咱們的卡片文案的baseline對齊怎麼辦?選中約束後轉到屬性檢查器,選擇 FirstItem 選項,選中First Baseline便可。

對齊baseline

從新運行後獲得了我想要的效果。

佔比 70%

因此當你須要在 Interface Builder 中使用這個比例定位技術時,使用spacerview可以幫助你達到指望。可是必定要將這些視圖標記爲隱藏,使它們不會被渲染,但又能協助你進行佈局,使你可以定位你的內容。若是你是用編程方式進行佈局,可使用UILayout Guide來完成,你能夠將其用做等效於spacerviews

6. Stack view 自適應佈局(Stack view adaptive layout)

讓咱們一塊兒來看看最後一種咱們要佈局的視圖,以下圖,你能看到 app 中展現了一個自適應佈局的頁面,上方是一個4x4的網格排列,底部有一個 label 。

豎屏效果

當我旋轉手機的時候,出現了一些不同的東西。它仍然會顯示一個4x4的網格,但它有一個文本視圖出如今右邊的位置。

橫屏效果

6.1 豎屏佈局

這一切是怎麼作到的呢?讓咱們來看看如何對 Interface Builder 中的 stackview 進行自適應佈局。首先看下圖中最外層是一個垂直的 stackview ,從上到下分紅三行,第一行和第二行都是包含兩張圖片的水平 stackview ,第三行是一個 label 。就如你看到的,他們高度是相等的,咱們能夠經過 AlignmentDistribution,和 Spacing 等屬性來進行調整,以達到你想要的佈局。 stackview 有一個很是讚的地方,就是它能幫你管理被包含的視圖的約束,這樣你只要添加不多的約束。

三行佈局

接下來讓咱們選中全部的 stackview ,在Distribution選項中選擇fill equally 實現平均分佈,在Spacing選項中咱們能夠手動輸入咱們想要的間距,另外系統也提供了標準間距選項給咱們,點擊輸入框右邊的倒三角就會出現一個Use Standard Value選項,直接選中便可。

間距屬性

下一步我要確保這些圖像是正方形的,咱們直接選中第一張圖片,爲其添加一個寬高比爲1:1的約束,添加後你會發現出現一些衝突,這是由於在知足填充滿整個屏幕和三行平均分佈的同時,沒法保證圖片比例達到1:1。

寬高比例約束衝突

因此咱們要作一些改變,咱們將 stackview 到底部的固定約束脩改爲大於等於,這樣出現的衝突就解決了,也達到了我想要的效果。

調整底部約束

6.2 橫屏佈局

當咱們將設備旋轉到橫屏狀態,咱們預期的效果是在右邊有一個 textview ,而底部並無 label ,爲了更接近咱們預期效果咱們須要把底部的 label 先隱藏。咱們要如何才能作到在豎屏中顯示,在橫屏中隱藏呢?

橫屏

在全新的 Xcode9 中的隱藏屬性,能夠爲不一樣size class分別設置顯示或隱藏。轉到 label 的hidden屬性,你會發現勾選按鈕左邊有一個加號,它讓這一切變得輕鬆簡單。點擊後在彈出的界面中Width選擇any,在橫屏的時候,Height選擇compact,由於在橫屏的時候它的高度是緊湊的。

隱藏屬性變量

作完這些點擊add variation,而且在hidden屬性下面找到剛剛設置的隱藏屬性而且勾選中它,你會發現 label 被隱藏了,若是你切換成豎屏,又會顯示出來。

接下來繼續添加一個 textview ,爲了作到這點,咱們要在最外層套一個水平排列的 stackview ,而後將 textview 加入 stackview 中,而且爲新建的 stackview 添加約束,其中底部約束就如同以前設置爲大於等於。這樣就獲得了咱們橫屏中須要的效果。

加入textView

當咱們切換到豎屏時 textview 仍然顯示了,咱們要在豎屏時隱藏它,就像以前同樣轉到隱藏菜單,並添加一個變量,Width選擇anyHeight選擇Regular,而後將其標記爲隱藏,這樣在豎屏時, textview 就再也不顯示了,就此達到了咱們指望的效果。

豎屏時須要隱藏 textView

在使用 stackview 的時候,咱們可使用AlignmentDistributionSpacing這些屬性幫助咱們佈局。還有嵌套使用,只須要加入不多的約束,咱們僅僅須要在你對寬高比例有要求的時候,經過寬高比例約束來得到咱們想要的比例。使人驚喜的是,Xcode 9 中的隱藏屬性是可分級的,它很是適合與 stackview 搭配使用,並且隨着size class的變化的隱藏屬性是向下兼容的。

總結

到這裏咱們已經看完了Autolayout相關的的六種技術,在你構建 app 的時候有了更多的佈局手段,這些技術可以使你的界面看起來很是美觀,結構清晰,而且自適應佈局,在平常開發中會常用到。我已經火燒眉毛地想看到更多人使用這些技術了。

Demo

GitHub:FindMyDates

參考

相關文章
相關標籤/搜索