自動收起展開組件
前情
當我們頁面中或者一個cell中有多個或一個View需要靈活展示灌侣,且我們用的是AutoLayout布局(包括Snapkit 和Masonry)唆香,我們隱藏或者展示一個View會很麻煩,如以下布局:
我們有這樣一個cell布局嗤攻,其中tag行中的tag Label是靈活展示的判帮,也就是說根據(jù)服務(wù)返回各種標簽判斷標簽是否展示净宵,有可能只展示tag1 和 tag4, 也有可能全部展示拒炎,也有可能一個都不展示挪拟。
如果按照常規(guī)思路,比如tag2和tag3不展示击你,我們需要將tag2寬度設(shè)置為0玉组, 并且需要將左邊距重置為0, tag3亦是如此丁侄。
如果tag1 不展示惯雳,我們需要將其寬度設(shè)置為0, 且右邊距也設(shè)置為0鸿摇。多種情況都有可能存在石景,可想代碼多復(fù)雜且非常難維護。
有沒有一種方式能簡化代碼、且易懂鸵钝、代碼更好維護糙臼,成為開發(fā)此庫的目標。
思路
早之前還在使用XIB布局的時候恩商,曾經(jīng)使用過一個三方庫叫做FDCollapsibleConstraints, 它思路為 將 NSLayoutConstaints 加入到一個Array中变逃,記錄每個Constaints的原始值, 如果fd_collapsed設(shè)置為YES, 則就會將所有加入到Array中的約束設(shè)置為 0怠堪, 否則則還原至之前的關(guān)聯(lián)的原始值揽乱,并且作者將fd_collapsibleConstraints聲明為IBOutletCollection 也就是XIB中可直接進行連線約束的,對于XIB約束或者系統(tǒng)約束確實很方便粟矿。
但是由于XIB占用內(nèi)存大凰棉、沖突難解決的問題,一般項目中已經(jīng)禁止使用了陌粹。所以我們的項目亦是如此撒犀,那么我們通過Snapkit或者Masonry布局者因無法直接獲取到NSLayoutConstraints,也就無法直接加入到自動折疊約束Array中掏秩,如果變換思路取或舞,也會很麻煩。
所以我們也需要遺棄FDCollapsibleConstraints 以及它的思路蒙幻。
我們實現(xiàn)的思路為:
當給UIView調(diào)用一個實例方法時候映凳,傳入顯示且需要恢復(fù)的邊距 或者 隱藏且需要隱藏的邊距 或者 只需要隱藏
根據(jù)UIView的constraints 遍歷尋找到需要隱藏或者顯示的約束
重置約束
實現(xiàn)
import Foundation
extension NSLayoutConstraint {
private struct AssociationKey {
static var key = 0
}
func clear() {
if constant != 0 {
objc_setAssociatedObject(self, &AssociationKey.key, NSNumber(floatLiteral: self.constant), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
self.constant = 0
}
}
func restore() {
guard let oldConstraint = objc_getAssociatedObject(self, &AssociationKey.key) as? NSNumber else {
return
}
self.constant = oldConstraint.doubleValue
objc_setAssociatedObject(self, &AssociationKey.key, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
首先給NSLayoutConstraints 增加兩個方法(不管是Snapkit還是Masonry 其實最后都是NSLayoutConstraints):
1、clear() 清除約束 && 保留原始約束值
2邮破、restore 恢復(fù)約束(恢復(fù)clear存儲下來的原始值)
public extension UIView {
struct Direction: OptionSet {
public var rawValue: Int
public static let left = Direction(rawValue: 1 << 0)
public static let right = Direction(rawValue: 1 << 1)
public static let top = Direction(rawValue: 1 << 2)
public static let bottom = Direction(rawValue: 1 << 3)
public static let all: Direction = [.left, .right, .top, .bottom]
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
enum VisibleType {
case visible(direction: Direction)
case invisible
case gone(direction: Direction)
}
func setVisible(_ visible: VisibleType) {
switch visible {
case .visible(let direction):
guard let superview = self.superview else {
return
}
if direction.contains(.left) {
restoreConstraint(with: .left, on: superview)
restoreConstraint(with: .leading, on: superview)
}
if direction.contains(.right) {
restoreConstraint(with: .right, on: superview)
restoreConstraint(with: .trailing, on: superview)
}
if direction.contains(.top) {
restoreConstraint(with: .top, on: superview)
}
if direction.contains(.bottom) {
restoreConstraint(with: .bottom, on: superview)
}
restoreConstraint(with: .width, on: self)
restoreConstraint(with: .height, on: self)
case .gone(let direction):
guard let superview = self.superview else {
return
}
if direction.contains(.left) || direction.contains(.right) {
clearConstraint(with: .width, on: self)
if direction.contains(.left) {
clearConstraint(with: .leading, on: superview)
clearConstraint(with: .left, on: superview)
}
if direction.contains(.right) {
clearConstraint(with: .right, on: superview)
clearConstraint(with: .trailing, on: superview)
}
}
if direction.contains(.top) || direction.contains(.bottom) {
clearConstraint(with: .height, on: self)
if direction.contains(.top) {
clearConstraint(with: .top, on: superview)
}
if direction.contains(.bottom) {
clearConstraint(with: .bottom, on: superview)
}
}
case .invisible:
self.isHidden = true
}
}
private func clearConstraint(with attribute: NSLayoutAttribute, on view: UIView) {
guard let constraints = findConstraints(in: view, attribute: attribute) else {
return
}
constraints.forEach { constraint in
constraint.clear()
}
}
private func restoreConstraint(with attribute: NSLayoutAttribute, on view: UIView) {
guard let constraints = findConstraints(in: view, attribute: attribute) else {
return
}
constraints.forEach { constraint in
constraint.restore()
}
}
private func findConstraints(in view: UIView, attribute: NSLayoutAttribute) -> [NSLayoutConstraint]? {
return view.constraints.filter { constraint in
guard let firstItem = constraint.firstItem as? NSObject else {
return false
}
if firstItem == self && constraint.firstAttribute == attribute {
return true
}
guard let secondItem = constraint.secondItem as? NSObject else {
return false
}
return secondItem == self && constraint.secondAttribute == attribute
}
}
}
設(shè)置一個UIView的分類诈豌,方便調(diào)用
聲明邊距類型:Direction, 分別為上抒和、下矫渔、左、右摧莽、 全部
聲明顯示隱藏類型:visible(direction: Direction) 顯示 && 顯示的邊距蚌斩, invisible 只隱藏, gone(direction: Direction) 隱藏且隱藏邊距
聲明方法 func setVisible(_ visible: VisibleType)范嘱, 內(nèi)部根據(jù)visible類型送膳,進行filter約束 且 重置為0或者恢復(fù)
-
為什么private func findConstraints(in view: UIView, attribute: NSLayoutAttribute) -> [NSLayoutConstraint]? 返回的是約束數(shù)組, 因為以下兩種情況:
當一個帶有intrinsicContentSize UIView或者子類丑蛤,比如Label叠聋、 Button, 系統(tǒng)會自動增加NSLayoutConstraintsIntrinsicWidth 和 NSLayoutConstraintsIntrinsicHeight, 如果你還增加了高度寬度約束受裹,這時候其實寬度高度各兩個碌补。
相關(guān)聯(lián)的約束可能有多個
總結(jié)
感謝開源庫https://github.com/MotokiMiyagi/UIViewVisibility/tree/master 提供的思路虏束。
當前實現(xiàn)的為swift版本,如果需要使用Objective-c的可以使用以上庫厦章,但是需要注意實現(xiàn)中的第5點镇匀,此庫是有問題的。
需要使用三方庫的童鞋袜啃,可以直接到https://github.com/HaoXianSen/HRViewVisible 集成使用