简体   繁体   中英

Dynamic Constraint for UIView & UICollection

On the top of my screen will show one of two UIViews. One is the minimized version and the other is the maximized verison.

The minimized view is 30 in height while the maximized version is 250.

Underneath this there is a UICollectionView.

I want it so that when the minimized version of the UIView is showing, the UICollectionView's topAnchor will be connected to the UIViews bottomAnchor.

When I click on each of the UIViews they will become hidden and make the other one visible.

Here are some screenshots to help visualize:

Default在此处输入图片说明

Minimized在此处输入图片说明

Attempt to maximize在此处输入图片说明

So when I show the maximized UIView I want the UICollectionViews topAnchor to be connected to that ones bottomAnchor and so forth.

Currently everything is working except the proper resizing of the UICollectionView. It will resize up when I minimize, but will not resize down when I maximize.

My viewDidLoad calls both of these functions:

    private func configureMaxView() {

    view.addSubview(maxView)
    maxView.layer.cornerRadius = 18
    maxView.backgroundColor = .secondarySystemBackground
    maxView.translatesAutoresizingMaskIntoConstraints = false
    maxView.isHidden = isMaxViewHidden

    let gesture = UITapGestureRecognizer(target: self, action: #selector (self.minimizeAction (_:)))
    self.maxView.addGestureRecognizer(gesture)

    let padding: CGFloat    = 20

    NSLayoutConstraint.activate([

        maxView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
        maxView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
        maxView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding),
        maxView.heightAnchor.constraint(equalToConstant: 250),

    ])
}


private func configureMinView() {

    view.addSubview(minView)
    minView.layer.cornerRadius = 9
    minView.backgroundColor = .secondarySystemBackground
    minView.translatesAutoresizingMaskIntoConstraints = false
    minView.isHidden = !isMaxViewHidden

    let gesture = UITapGestureRecognizer(target: self, action: #selector (self.expandAction (_:)))
    self.minView.addGestureRecognizer(gesture)

    let padding: CGFloat    = 20

    NSLayoutConstraint.activate([

        minView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
        minView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
        minView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding),
        minView.heightAnchor.constraint(equalToConstant: 30),

    ])
}

Here are the functions that are called when you click on one of the UIViews:

    @objc func minimizeAction(_ sender:UITapGestureRecognizer){
    minView.isHidden = false
    maxView.isHidden = true
    // without this call the hiding and unhiding works fine - But the collectionview won't move
    resizeCollectionView()
    isMaxViewHidden = !isMaxViewHidden
  }

@objc func expandAction(_ sender:UITapGestureRecognizer){
    minView.isHidden = true
    maxView.isHidden = false
    // without this call the hiding and unhiding works fine - But the collectionview won't move
    resizeCollectionView()
    isMaxViewHidden = !isMaxViewHidden
  }

My viewDidLoad will also call this after setting up the uiviews in order to set up the collectionView:

    private func configureCollectionView() {

    collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UIHelper.createThreeColumnFlowLayout(in: view))
    view.addSubview(collectionView)
    collectionView.delegate = self
    collectionView.backgroundColor = .systemBackground
    collectionView.register(CustomCell.self, forCellWithReuseIdentifier: CustomCell.resuseID)

    collectionView.translatesAutoresizingMaskIntoConstraints = false


    let bottomAnchor = isMaxViewHidden ? minView.bottomAnchor : maxView.bottomAnchor


    NSLayoutConstraint.activate([

        collectionView.topAnchor.constraint(equalTo: bottomAnchor),
        collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

    ])
}

Here is the function I call from each uiview click handler in an attempt to update the constraint:

    private func resizeCollectionView() {

    let bottomAnchor = isMaxViewHidden ? maxView.bottomAnchor: minView.bottomAnchor

    NSLayoutConstraint.activate([
        collectionView.topAnchor.constraint(equalTo: bottomAnchor),
    ])

    // This does move the collection view down, but seems hardcoded and bad
    //collectionView.frame.origin.y = 650
}

Error:

"<NSLayoutConstraint:0x600000c4ed50 UIView:0x7ffd60c135e0.top == UILayoutGuide:0x6000016f89a0'UIViewSafeAreaLayoutGuide'.top   (active)>",
"<NSLayoutConstraint:0x600000c4fa20 UIView:0x7ffd60c135e0.height == 250   (active)>",
"<NSLayoutConstraint:0x600000c4bed0 UIView:0x7ffd60c13750.top == UILayoutGuide:0x6000016f89a0'UIViewSafeAreaLayoutGuide'.top   (active)>",
"<NSLayoutConstraint:0x600000c3c000 UIView:0x7ffd60c13750.height == 30   (active)>",
"<NSLayoutConstraint:0x600000c3dbd0 V:[UIView:0x7ffd60c135e0]-(0)-[UICollectionView:0x7ffd62819600]   (active)>",
"<NSLayoutConstraint:0x600000c20cd0 V:[UIView:0x7ffd60c13750]-(0)-[UICollectionView:0x7ffd62819600]   (active)>"

)

Will attempt to recover by breaking constraint

I was wondering if you need a call to update your layout. Maybe called from the resizeCollectionView()?

let bottomAnchorMinContraint: NSConstraint = collectionView.topAnchor.constraint(equalTo: minView.bottomAnchor)
let bottomAnchorMaxContraint: NSConstraint = collectionView.topAnchor.constraint(equalTo: maxView.bottomAnchor)
if isMaxViewHidden {
 bottomAnchorMinContraint.isActive = true
 bottomAnchorMaxContraint.isActive = false
}
if !isMaxViewHidden {
 bottomAnchorMinContraint.isActive = false
 bottomAnchorMaxContraint.isActive = true
}
view.layoutIfNeeded()

You're activating conflicting constraints on top of each other. If you want to go between a minimized and maximized view, just change the constant value on the height constraint itself.

I whipped up a sample MyViewController in playgrounds with 2 views, the first of which's height and color changes when you tap it. myView is like your collapsable view, and myOtherView is like your collectionView.

Let me know if you have any questions!

class MyViewController : UIViewController {
    private enum ViewState {
        case min
        case max

        var height: CGFloat {
            switch self {
            case .min:
                return 30
            case .max:
                return 250
            }
        }

        var color: UIColor {
            switch self {
            case .min:
                return .red
            case .max:
                return .green
            }
        }
    }

    private var myViewState: ViewState = .min {
        didSet {
            viewStateToggled()
        }
    }

    private let myView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    private let myOtherView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .yellow
        return view
    }()

    private lazy var myViewHeightConstraint = NSLayoutConstraint(
        item: myView,
        attribute: .height,
        relatedBy: .equal,
        toItem: nil,
        attribute: .notAnAttribute,
        multiplier: 1,
        constant: myViewState.height
    )

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .white
        self.view = view
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        configureMyViews()
    }

    private func configureMyViews() {
        view.addSubview(myView)

        myView.backgroundColor = myViewState.color

        NSLayoutConstraint.activate([
            myView.topAnchor.constraint(equalTo: view.topAnchor),
            myView.leftAnchor.constraint(equalTo: view.leftAnchor),
            myView.rightAnchor.constraint(equalTo: view.rightAnchor),
            myViewHeightConstraint
        ])

        let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
        myView.addGestureRecognizer(tap)

        view.addSubview(myOtherView)
        NSLayoutConstraint.activate([
            myOtherView.topAnchor.constraint(equalTo: myView.bottomAnchor),
            myOtherView.leftAnchor.constraint(equalTo: view.leftAnchor),
            myOtherView.rightAnchor.constraint(equalTo: view.rightAnchor),
            myOtherView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }

    private func viewStateToggled() {
        myViewHeightConstraint.constant = myViewState.height
        UIView.animate(withDuration: 0.4) {
            self.myView.backgroundColor = self.myViewState.color
            self.view.layoutSubviews()
        }
    }

    @objc func handleTap(_ sender: UITapGestureRecognizer? = nil) {
        myViewState = myViewState == .min ? .max : .min
    }
}
"<NSLayoutConstraint:0x600000c4ed50 UIView:0x7ffd60c135e0.top == UILayoutGuide:0x6000016f89a0'UIViewSafeAreaLayoutGuide'.top   (active)>",
"<NSLayoutConstraint:0x600000c4fa20 UIView:0x7ffd60c135e0.height == 250   (active)>",
"<NSLayoutConstraint:0x600000c4bed0 UIView:0x7ffd60c13750.top == UILayoutGuide:0x6000016f89a0'UIViewSafeAreaLayoutGuide'.top   (active)>",
"<NSLayoutConstraint:0x600000c3c000 UIView:0x7ffd60c13750.height == 30   (active)>",

What's happening in here is that, even though the view is being hidden, the constraints are still there, so assigning numerous top constraints on a single view will result to a conflict.

If you want to have a sort of dynamic constraint, just modify the constraint's constant or multiplier values. That way, you wont need to worry about constraint objects.

Wilson's solution is right. What I can just add from that is just have a single containerView containing your min and max views. and just toggle the visibility of your min and max view inside your containerView.

Here's my (simple) take on it: everything is called in viewDidLoad() as you do, and you also need to define a constraint object. private var containerViewHeight: NSLayoutConstraint!

    private func configureContainerView() {
        containerView = UIView()
        containerView.backgroundColor = .systemGreen
        containerView.translatesAutoresizingMaskIntoConstraints = false
        containerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(containerViewTapped)))
        view.addSubview(containerView)

        containerViewHeight = containerView.heightAnchor.constraint(equalToConstant: 250)
        containerViewHeight.isActive = true

        NSLayoutConstraint.activate([
            containerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
        ])
    }

    @objc private func containerViewTapped() {
        isViewTapped.toggle()
        containerViewHeight.constant = isViewTapped ? 30 : 250

        innerMinView.isHidden = !isViewTapped
        innerMaxView.isHidden = isViewTapped
    }

    private func configureCollectionView() {

        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionViewFlowLayout())
        view.addSubview(collectionView)
        collectionView.backgroundColor = .systemYellow
        collectionView.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: containerView.bottomAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

        ])
    }

    private func configureInnerMinView() {
        innerMinView = UIView()
        innerMinView.backgroundColor = .systemRed
        innerMinView.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(innerMinView)

        NSLayoutConstraint.activate([
            innerMinView.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.8),
            innerMinView.heightAnchor.constraint(equalTo: containerView.heightAnchor, multiplier: 0.8),
            innerMinView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
            innerMinView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor)
        ])
    }

    private func configureInnerMaxView() {
        innerMaxView = UIView()
        innerMaxView.backgroundColor = .systemBlue
        innerMaxView.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(innerMaxView)

        NSLayoutConstraint.activate([
            innerMaxView.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.8),
            innerMaxView.heightAnchor.constraint(equalTo: containerView.heightAnchor, multiplier: 0.8),
            innerMaxView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
            innerMaxView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor)
        ])
    }

side note: btw. I think I know what project this is. :) SA-GH.F

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM