自動布局那些事

Auto Layout是蘋果在iOS 6中引進的新技術(shù),這是一種基于約束系統(tǒng)的布局規(guī)則,它的出現(xiàn)顛覆了開發(fā)人員創(chuàng)建界面的方式,同時我們也發(fā)現(xiàn)在較新版本的Android Studio中,很多通過模板創(chuàng)建的應(yīng)用程序也默認(rèn)采用了constraint-layout,可見基于約束規(guī)則來創(chuàng)建移動軟件界面的方式已經(jīng)被大家普遍認(rèn)可。

Autoresizing系統(tǒng)

  • 說到Auto Layout,我們有必要先了解一下Autoresizing,所謂Autoresizing,就是當(dāng)父視圖的bounds發(fā)生變化時,會根據(jù)子視圖設(shè)置的autoresizing mask自動調(diào)整子視圖。
  • 比如圖1是tableView中的某兩行,有一個處于右下角的控件
圖1
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        /* 此處略去部分代碼*/
        print("width:\(contentViewWidth) height:\(contentViewHeight)")
        //width:320.0 height:44.0
        
        let label = UILabel(frame: CGRect(x: contentViewWidth - lableWidth - margin, y: contentViewHeight - fontsize - margin, width: lableWidth, height: fontsize))
        label.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin]
        contentView.addSubview(label)
}
  • 如果沒有使用Auto Layout,我們需要獲取cell的寬度做一些簡單的運算確定起始點的位置,為了進一步說明問題,我們在初始化方法中打印了contentView的寬高(320*44),很明顯這個size并不正確,如果根據(jù)這個size去布局將會得到一個錯誤的位置。

  • 我們可能會這么做,定義一個全局的屏幕寬度,通過這個常量去計算確實可以保證在豎屏條件下的正確性,但是如果你的頁面也出現(xiàn)在iPad中,而且iPad還支持旋轉(zhuǎn)和分屏,那么你的布局依然還是錯誤的。

  • 我們還可能這么做,在以下方法中設(shè)置frame,當(dāng)然這個方法中取到的父視圖的大小是正確的。

override func layoutSubviews() {
        super.layoutSubviews()
        //label.frame = ...
}
  • 另外一種方法是前面提到的Autoresizing,比如以上的代碼中l(wèi)abel.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin],表明該控件距離右邊的距離保持不變,距離左邊的距離跟隨父視圖變化,距離底部的距離保持不變,距離頂部的距離跟隨父視圖變化。所以當(dāng)你使用了Autoresizing,你就可以不在乎cell創(chuàng)建時錯誤的初始值了,因為它會自動調(diào)整,就算是橫屏或者分屏,它都表現(xiàn)良好。

  • 很明顯對于以上的約束,涉及的加加減減雖然不至于太復(fù)雜,但也太過繁瑣。

  • 盡管Auto Layout大有取代Autoresizing之意,但Autoresizing在某些情況下依然不失為一個好的方法,下面的代碼用到了自適應(yīng)的寬高來創(chuàng)建一個安全區(qū)域內(nèi)全屏的tableView,不僅不受導(dǎo)航欄、tabBar影響,而且簡潔、明了。

let tableView = UITableView(frame: view.bounds, style: .plain)
tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

開始使用Auto Layout

場景

  • iPad上有一個廣告輪播圖(可以同時看到左中右三個廣告,廣告在滾動過程中寬度一直在變化,當(dāng)滾動到中間時廣告最大),我們需要在右上角添加一個廣告標(biāo)簽以提示用戶。很明顯,在這個場景里,如果你不選用Auto Layout或是Autoresizing,你很可能需要根據(jù)UIScrollViewDelegate的一些方法來實時的調(diào)整這個廣告標(biāo)簽的位置。
  • 多個控件整體居中。不使用自動布局的情況下,你需要計算所有控件寬度的和。
  • iPad旋轉(zhuǎn)、分屏適配。
  • iPhone X適配。

類似的場景還有很多,如果不使用Auto Layout來布局,你可能需要一些看似不太復(fù)雜的計算,而這些計算往往可讀性很差,通常會定義若干個常量,而且很難適應(yīng)各種屏幕。

Auto Layout

Intrinsic Content Size

視圖內(nèi)容的大小通過每個視圖的intrinsicContentSize屬性表達(dá),它描述了數(shù)據(jù)未經(jīng)壓縮或裁剪的情況下表達(dá)視圖全部內(nèi)容所需的最小空間。

  • 使用自動布局,依然需要使用約束來定義視圖的位置和大小,然而一些視圖會根據(jù)所給內(nèi)容擁有一個固有大小,比如UIKit里的UILabel,UIButton,UISwitch,UITextField,UIImageView等,此時我們可以免去寬高的約束。
  • 如果我們想讓自定義的視圖也擁有這個特性,我們可以在UIView的子類重寫intrinsicContentSize,返回一個固有大小
override var intrinsicContentSize: CGSize {
        return CGSize(width: 44, height: 44)
}

UILayoutFittingCompressedSize
@available(iOS 6.0, *)
public let UILayoutFittingCompressedSize: CGSize
  • 這個常量多在tableViewCell自動計算行高時看到,然而它也可以在普通視圖中使用,比如我們通過在自定義的view中添加約束,然后借助UILayoutFittingCompressedSize來實現(xiàn)多控件整體居中這么個需求。
  • 圖2中找回密碼|緊急凍結(jié)|更多選項整體居中
圖2
  • 要完成圖2這么一個需求,我們需要定義一個容器,這個容器的寬高根據(jù)內(nèi)部控件的擺放規(guī)則自動計算。
  • 首先把要添加約束的控件設(shè)置translatesAutoresizingMaskIntoConstraints屬性為false

translatesAutoresizingMaskIntoConstraints : A Boolean value that determines whether the view’s autoresizing mask is translated into Auto Layout constraints.(是否要把autoresizing轉(zhuǎn)成約束)

    //找回密碼按鈕
    findPwdButton.translatesAutoresizingMaskIntoConstraints = false
    //緊急凍結(jié)按鈕
    freezeButton.translatesAutoresizingMaskIntoConstraints = false
    //更多選項按鈕
    moreButton.translatesAutoresizingMaskIntoConstraints = false
    //兩條分割線
    separatorLine1.translatesAutoresizingMaskIntoConstraints = false
    separatorLine2.translatesAutoresizingMaskIntoConstraints = false
  • 我們選用VisualFormatLanguage的方式來實現(xiàn)以上的約束,它可以在一行代碼中實現(xiàn)多個約束。
    let viewsDictionary = ["findPwdButton" : findPwdButton, "freezeButton" : freezeButton, "moreButton" : moreButton, "separatorLine1" : separatorLine1, "separatorLine2" : separatorLine2]
    let metric = ["separatorHeight" : 16]
    let constraints = [
        NSLayoutConstraint.constraints(
            withVisualFormat: "H:|[findPwdButton]-[separatorLine1(1)]-[freezeButton]-[separatorLine2(1)]-[moreButton]|",
            options: [.alignAllTop, .alignAllBottom],
            metrics: nil,
            views: viewsDictionary
        ),
        NSLayoutConstraint.constraints(
            withVisualFormat: "V:|[separatorLine1(separatorHeight)]|",
            options: [],
            metrics: metric,
            views: viewsDictionary
        )
    ]
    NSLayoutConstraint.activate(constraints.flatMap{ $0 })
VisualFormatLanguage 分析
  • H:|[findPwdButton]-[separatorLine1(1)]-[freezeButton]-[separatorLine2(1)]-[moreButton]|

  • H:表明這是一個關(guān)于水平方向的約束,這五個控件自左向右依次排開;

  • |表示父視圖的邊界,[]表示具體控件,而|[findPwdButton]則表明控件[findPwdButton]緊貼父視圖的左邊界(相當(dāng)于|-0-[findPwdButton]);

  • -表示控件間的標(biāo)準(zhǔn)寬度,也可以用具體的數(shù)字(比如-10-)來定義控件間的距離。

  • "V:|[separatorLine1(separatorHeight)]|"

  • V:表明這是一個關(guān)于豎直方向的約束,該方向只有一個控件。

  • 括號內(nèi)的separatorHeight在metrics里定義,當(dāng)然也可以直接在括號里面填寫數(shù)字。

  • 參數(shù)options表示子控件的擺放規(guī)則,[.alignAllTop, .alignAllBottom]意思是這五個控件頂部和底部對齊。正因為有這個約束,當(dāng)我們指明其中的一個分割線的高度為16時,就意味著其他的幾個控件也都是16的高度。至此我們完成了這個容器內(nèi)所有控件的約束。

VisualFormatLanguage小結(jié)
  • 表示控件的字符串要嚴(yán)格與參數(shù)views定義的鍵值匹配(表示控件寬度或高度的字符串也要嚴(yán)格與參數(shù)metrics定義的鍵值匹配),以保證約束可以被正確解析。
  • 由于button有intrinsicContentSize(根據(jù)按鈕標(biāo)題自動計算),我們并沒有指明按鈕的寬度。
  • 選用合適的options可以減少約束的數(shù)量。

最后我們選用了上述的intrinsicContentSize屬性,同時返回UILayoutFittingCompressedSize,表明視圖將根據(jù)容器內(nèi)部的約束(距離上下左右四個方向的約束缺一不可),選用一個最小的size來剛好包括這幾個控件。

override var intrinsicContentSize: CGSize {
        return UILayoutFittingCompressedSize
    }
  • 免去了寬高約束,我們很容易就可以讓這個控件居中
override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        loginBottomView.translatesAutoresizingMaskIntoConstraints = false
        
        if #available(iOS 11.0, *) {
            loginBottomView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
            loginBottomView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true
        } else {
            NSLayoutConstraint(item: loginBottomView, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
            view.addConstraint(NSLayoutConstraint(item: loginBottomView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: -10))
        }
    }
NSLayoutAnchor分析
  • 以上if內(nèi)部的自動布局寫法從iOS 9開始支持,相比于else內(nèi)部的那種寫法更為簡潔、易懂。
  • 底部容器控件的水平方向中心等于父視圖的水平方向中心。
  • 底部容器控件的底部距離屏幕底部10(iOS 11為距離安全區(qū)域底部)
  • 此處還用到激活(isActive,iOS 8)約束,類似于addConstraint,如果是多個約束的激活,則選用NSLayoutConstraint.activate
UINavigationBar的isTranslucent
  • iOS 7以后導(dǎo)航欄的isTranslucent屬性默認(rèn)為true,這個最明顯的體會是導(dǎo)航欄有半透明的磨砂效果,可以隱約的看到有tableView從頂部穿過,這種方式一般是通過barTintColor的方式去設(shè)置的。
  • 另外一種則呈現(xiàn)為半透明不帶磨砂的效果,這種方式則是通過BackgroundImage的方式去設(shè)置背景,如果你的圖片是帶alpha通道的,或者設(shè)置了isTranslucent為true,你都可以很明顯的看到tableView穿透導(dǎo)航欄的效果。
UINavigationBar.appearance().setBackgroundImage(image, for: .default)
UINavigationBar.appearance().barTintColor = UIColor.white
  • 對于帶系統(tǒng)導(dǎo)航欄的頁面來講,如果設(shè)置了navigationBar的isTranslucent為true,你的頁面布局的起點就從屏幕頂部開始算起,如果設(shè)置為false則從導(dǎo)航欄底部開始算起

  • 當(dāng)然如果你的controller添加了以下這行代碼,意味的你的子控件將不自動延伸,你的布局起點就從導(dǎo)航欄底部開始算起。

self.edgesForExtendedLayout = []
  • 這么說來,該怎么布局能不受導(dǎo)航欄的isTranslucent的影響呢?


    圖3.png
override func viewDidLayoutSubviews() {
        topToolbar.translatesAutoresizingMaskIntoConstraints = false
        
        if #available(iOS 11.0, *) {
            topToolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
            topToolbar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
            topToolbar.heightAnchor.constraint(equalToConstant: 44).isActive = true
            topToolbar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        } else {
            // Fallback on earlier versions
            let viewsDictionary = ["topToolbar" : topToolbar, "topLayoutGuide" : topLayoutGuide ] as [String : Any]
            let constraints = [
                NSLayoutConstraint.constraints(
                    withVisualFormat: "V:[topLayoutGuide][topToolbar(44)]",
                    options: [],
                    metrics: nil,
                    views: viewsDictionary
                ),
                NSLayoutConstraint.constraints(
                    withVisualFormat: "H:|[topToolbar]|",
                    options: [],
                    metrics: nil,
                    views: viewsDictionary
                )
            ]
            NSLayoutConstraint.activate(constraints.flatMap{ $0 })
        }
    }

topLayoutGuide

  • 沒錯就是topLayoutGuide,在以上的例子中你可以把它當(dāng)作是導(dǎo)航欄,當(dāng)然在iOS 11你還可以使用safeAreaLayoutGuide,盡管這兩個屬性并非UIView子類,但使用起來與UI控件很像。

safeAreaLayoutGuide

相信前面的內(nèi)容已經(jīng)讓你對安全區(qū)域有了一定的了解,接下來我們來介紹它


圖4.png
  • 從上圖可以看出扣除劉海、圓角等區(qū)域剩下的即為安全區(qū)域(圖4中青色矩形區(qū)域),如果你的頁面還有導(dǎo)航欄、tabBar,安全區(qū)域?qū)⑦M一步縮小,由于非安全區(qū)域會影響控件的交互,所以適配iPhone X要做的事情就是調(diào)整控件的位置,讓其處在安全區(qū)域內(nèi),盡管tableView、collectionView可以在非安全區(qū)域活動,但我們總可以通過滾動讓其靜止在安全區(qū)域內(nèi)。

iPhone X底部控件適配

圖5.png
    override func viewDidLayoutSubviews() {
        bottomToolbar.translatesAutoresizingMaskIntoConstraints = false
        
        if #available(iOS 11.0, *) {
            //左側(cè)緊貼父視圖左側(cè)
            bottomToolbar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
            
            //底部貼緊安全區(qū)域底部
            bottomToolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
            
            //右側(cè)緊貼父視圖右側(cè)
            bottomToolbar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
            
            //高度為60
            bottomToolbar.heightAnchor.constraint(equalToConstant: 60).isActive = true
            
        } else {
            // Fallback on earlier versions
            NSLayoutConstraint(item: bottomToolbar, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0).isActive = true
            NSLayoutConstraint(item: bottomToolbar, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0).isActive = true
            NSLayoutConstraint(item: bottomToolbar, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: 0).isActive = true
            NSLayoutConstraint(item: bottomToolbar, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 44).isActive = true
        }
    }

約束分析

  • 以上代碼中,我們給了三個方向(左、右、底)的約束和一個高度約束。
  • 由于高度約束屬于自身的約束,在else內(nèi)部,toItem我們設(shè)置為nil,attribute則為notAnAttribute
  • 代碼中的bottomToolbar是UIToolbar的子類,仔細(xì)看iOS 11的約束我們會發(fā)現(xiàn)其實我們并沒有讓topToolbar緊貼屏幕底部,只是讓其緊貼安全區(qū)域底部。然而事實是topToolbar自動延伸到底部了。
  • 注意:由于iOS 11以后,UIToolbar頂部覆蓋一層_UIToolbarContentView會導(dǎo)致添加到UIToolbar的子控件無法響應(yīng)事件,如果想利用以上自動延伸的特性的同時又能保證子控件可以正常響應(yīng)事件的話可以如下處理。
override func layoutSubviews() {
        super.layoutSubviews()
        
        for view in subviews {
            if view .isKind(of: NSClassFromString("_UIToolbarContentView")!) {
                view.isUserInteractionEnabled = false
            }
        }
    }
  • 然而我們面對的更多是普通的UIView子類,我們嘗試修改一下繼承關(guān)系,讓上面底部那個控件bottomToolbar直接繼承于UIView
圖6.png
  • 如果不修改約束,我們獲得了圖6的效果?,F(xiàn)在我們嘗試修改約束
override func viewDidLayoutSubviews() {
        bottomToolbar.translatesAutoresizingMaskIntoConstraints = false
        
        if #available(iOS 11.0, *) {
            //左側(cè)緊貼父視圖左側(cè)
            bottomToolbar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
            
            //底部貼緊安全區(qū)域底部
//            bottomToolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
            //底部改為貼緊屏幕底部
            bottomToolbar.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
            
            //右側(cè)緊貼父視圖右側(cè)
            bottomToolbar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
            
            //去掉高度為60的約束
            //bottomToolbar.heightAnchor.constraint(equalToConstant: 60).isActive = true
            //改為頂部距離安全區(qū)域底部為60
            bottomToolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -60).isActive = true
            
        } else {
            // Fallback on earlier versions
            
        }
    }
  • 對比以上兩個約束,我們?nèi)サ袅烁叨燃s束,添加了頂部約束,同時修改了底部約束。用頂部距離安全區(qū)域底部的距離來模擬高度約束,同時底部直接貼緊屏幕底部。
圖7.png
  • 上圖我們又看到了新的問題,就是底部控件居中的參照物似乎不對。我們需要在那一塊不帶圓角區(qū)域的區(qū)域居中,所以這就要求子控件要在安全區(qū)域內(nèi)布局。
override func layoutSubviews() {
        super.layoutSubviews()
        
        
        button.translatesAutoresizingMaskIntoConstraints = false
        
        if #available(iOS 11.0, *) {
            button.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor).isActive = true
            button.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -12).isActive = true
        } else {
            button.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
            button.rightAnchor.constraint(equalTo: rightAnchor, constant: -12).isActive = true
        }
        button.widthAnchor.constraint(equalToConstant: 120).isActive = true
        button.heightAnchor.constraint(equalToConstant: 40).isActive = true
    }
  • 子控件改為在安全區(qū)域內(nèi)布局,我們得到了圖5正確的樣式。

iPhone X簡易聊天輸入框的適配

  • 由于輸入框的位置受鍵盤的影響,這是一個自下而上的動畫。
  • 下面我們從靜態(tài)的輸入框來逐步實現(xiàn)這個稍微復(fù)雜的效果。
圖8.png
  • 上圖中底部控件暫且稱為TextInputToolbar,我們放了兩個子視圖textField和button,這兩個都有intrinsicContentSize,會根據(jù)自身的內(nèi)容(textField根據(jù)placeholder,button根據(jù)title)計算出自身的高度
  • 為了引入自動布局一個新的概念,我們并不打算給它們倆寬度,而是讓它們倆自己算。
  • 根據(jù)前面的內(nèi)容,我們寫出如下代碼
override func layoutSubviews() {
        super.layoutSubviews()
        
        for view in subviews {
            if view .isKind(of: NSClassFromString("_UIToolbarContentView")!) {
                view.isUserInteractionEnabled = false
            }
        }
        
        textField.translatesAutoresizingMaskIntoConstraints = false
        button.translatesAutoresizingMaskIntoConstraints = false
        
        let viewsDictionary = ["textField" : textField, "button" : button] as [String : Any]

        let constraints = [
            NSLayoutConstraint.constraints(
                withVisualFormat: "H:|-[textField]-[button]-|",
                options: [.alignAllCenterY],
                metrics: nil,
                views: viewsDictionary
            )
        ]
        NSLayoutConstraint.activate(constraints.flatMap{ $0 })
        
        NSLayoutConstraint(item: textField, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 6).isActive = true
        NSLayoutConstraint(item: textField, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: -6).isActive = true

    }
  • 將TextInputToolbar在controller上布局后,我們發(fā)現(xiàn)了如下結(jié)果
圖9.png
  • 由于這兩個控件都可以自己算寬度,這就引入一個優(yōu)先級的問題,很明顯,左邊的控件似乎先滿足。我們嘗試把這兩個控件的位置對調(diào)("H:|-[button]-[textField]-|")
圖10.png
  • 很明顯除了方向相反以外,其他的似乎是我們要的

內(nèi)容吸附

內(nèi)容吸附約束限制視圖允許自身伸展和填充視圖的程度。如果內(nèi)容吸附優(yōu)先級較高,則將視圖的框架與內(nèi)在內(nèi)容相匹配。

  • 內(nèi)容吸附優(yōu)先級默認(rèn)為UILayoutPriorityDefaultLow(250),而且會優(yōu)先滿足左邊。
  • 回到圖9的狀態(tài),我們嘗試降低一下textField內(nèi)容吸附的優(yōu)先級,我們設(shè)置一個具體的值249
textField.setContentHuggingPriority(UILayoutPriority(rawValue: 249), for: .horizontal)
  • 我們得到了圖8正確的布局,當(dāng)然除了讓左邊控件內(nèi)容吸附優(yōu)先級降低外,我們也可以嘗試讓右邊控件內(nèi)容吸附優(yōu)先級升高,比如
button.setContentHuggingPriority(UILayoutPriority(rawValue: 251), for: .horizontal)

壓縮阻力

壓縮阻力約束阻止視圖剪切其內(nèi)容。高壓縮阻力優(yōu)先級可確保顯示出視圖的完整內(nèi)在內(nèi)容。

  • 壓縮阻力優(yōu)先級默認(rèn)值為UILayoutPriorityDefaultHigh(750)
  • 當(dāng)我們嘗試往輸入框輸入文字時,而且文字過長時,我們又看到了異常,發(fā)送按鈕被擠壓直至消失。
圖11.png
  • 類似于內(nèi)容吸附優(yōu)先級的做法,我們嘗試提高右邊控件的壓縮阻力優(yōu)先級
button.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 751), for: .horizontal)
  • 至此我們完成了輸入框自身的布局,接下來我們來看與鍵盤的交互
  • 首先我們先添加監(jiān)聽鍵盤的一些方法
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: .UIKeyboardWillHide, object: nil)
  • 我們把約束設(shè)置為當(dāng)前controller的屬性
var hideConstraint: NSLayoutConstraint?
var showConstraint: NSLayoutConstraint?
//鍵盤消失時底部貼緊安全區(qū)域底部
hideConstraint = textInputBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
hideConstraint?.isActive = true
            
//鍵盤出現(xiàn)時底部貼緊屏幕底部,該約束并未激活
showConstraint = textInputBar.bottomAnchor.constraint(equalTo: view.bottomAnchor)
showConstraint?.isActive = false
  • 由于鍵盤的高度從屏幕底部起算,我們添加的底部也是參照屏幕底部做計算
    @objc func keyboardWillShow(_ notification: NSNotification) {
        let userInfo = notification.userInfo
        
        adjustTextFieldByKeyboardState(state: true, keyboardInfo: userInfo!)
    }
    
    @objc func keyboardWillHide(_ notification: NSNotification) {
        
        let userInfo = notification.userInfo
        adjustTextFieldByKeyboardState(state: false, keyboardInfo: userInfo!)
    }
    
    func adjustTextFieldByKeyboardState(state: Bool, keyboardInfo: [AnyHashable : Any]) {
        
        if state {
            let keyboardFrameVal = keyboardInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue
            
            let keyboardFrame = keyboardFrameVal.cgRectValue
            
            let height = keyboardFrame.size.height
            
            showConstraint?.constant = -height
            hideConstraint?.isActive = false
            showConstraint?.isActive = true
            
        } else {
            hideConstraint?.isActive = true
            showConstraint?.isActive = false
        }
        
        let animationDurationValue = keyboardInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber
        let animationDuration = animationDurationValue.doubleValue
        
        UIView.animate(withDuration: animationDuration) {
            self.view.layoutIfNeeded()
        }
    }
  • 我們通過切換兩個約束同時改變常量的方式來實現(xiàn)這個動畫
  • constant是NSLayoutConstraint的屬性,通過修改constant的值可以調(diào)整一些距離,最后類似于frame的動畫,我們只要調(diào)用父視圖layoutIfNeeded()系統(tǒng)便會幫我們完成動畫。
  • 最后,我們得到下圖的效果
圖12.png

UIStackView初見

  • iOS 9不僅帶來了更加簡潔的Anchor布局,也帶來了UIStackView,這大大方便了我們在垂直或水平方向布局多個子視圖,有點類似于Android的線性布局。盡管UIStackView是UIView的一個子類,但它僅作為容器使用,并不會被渲染。
  • 下面我們將利用UIStackView來實現(xiàn)一個簡易的tabBar
圖13.png
  • 對于tabBar的每個item其實是前面提到的多控件居中,只不過這次為豎直方向的居中,在此不再贅述。
lazy var stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .horizontal
        stackView.distribution = .fillEqually
        stackView.alignment = .fill
        stackView.spacing = 0
        return stackView
    }()
  • axis:很明顯由于我們要堆疊的是水平方向上的5個tab

  • distribution:子視圖的分布比例,由于tabBar是等分的,此處設(shè)置為fillEqually

  • alignment:對齊方式

  • spacing:間距

  • 在看子視圖的添加(addArrangedSubview),我們并不需要為這幾個子控件添加約束,一切都是UIStackView幫我們實現(xiàn)的。

for _ in 1...5 {
            let bottomTabBarItemView = BottomTabBarItemView()
            stackView.addArrangedSubview(bottomTabBarItemView)
}

Demo下載

Demo下載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容