Reputation: 68
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:
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
Upvotes: 0
Views: 1033
Reputation: 701
"<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
Upvotes: 2
Reputation: 810
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
}
}
Upvotes: 1
Reputation: 117
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()
Upvotes: 1