I Need to adjust a child collection view height when I've calculated what it's parent scrollView should be. The height calculation produces the correct value but I can't get the "Grand child" collectionView to adjust itself.
All the constraints inside the storyBoard are tied to the scrollView container view height so when I adjust it's value they all should follow in line and adjust themselves.
The flow is:
UIScrollView -> UICollectionView -> UICollectionView
My calculation is:
var count: CGFloat = 370
guard let currentLegs = active.legs![cell].feet else { return }
for _ in currentLegs {
count += 70
}
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
if count > 540 {
self.scrollView.contentSize.height = count
} else {
self.scrollView.contentSize.height = 540
}
}, completion: nil)
print(count)
The above calculates the height and defaults to another value if it's smaller than I need.
I've tried setting the scrollview.layoutIfneeded() & setNeedsLayout() but these cause the collectionView to redraw itself resulting in a crash.
Is it possible to adjust a "grand child" collection view from the top level?
Thanks
Try this
#IBOutlet weak var scrollViewHeightConstarint: NSLayoutConstraint!
.....
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.scrollViewHeightConstarint.constant = <#WhatYouNeed#>
self.view.layoutIfNeeded()
}
Related
My situation:
I have a horizontal ScrollView containing a StackView.
Inside this StackView there are some Views, that can be expanded/collapsed.
When I want to expand one of these Views, I first unhide some subViews in the View. After that I need to change the height of the ScrollView based on the new height of this View.
But this is not working...
I try this code:
UIView.animate(withDuration: 0.3) { [self] in
// Toggle hight of all subViews
stackView.arrangedSubviews.forEach { itemView in
guard let itemView = itemView as? MyView else { return }
itemView.toggleView()
}
// Now update the hight of the StackView
// But here the hight is always from the previous toggle
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
print(height)
heightConstraint.constant = height
}
This code nicely animates, but always to the wrong height.
So the ScrollView animates to collapsed when it should be expanded and expanded when it should be collapsed.
Anyone with on idea how to solve this?
The problem is that, whatever you are doing here:
itemView.toggleView()
may have done something to change the height a view, but then you immediately call:
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
before UIKit has updated the frames.
So, you can either track your own height property, or...
get the frame heights after the update - such as with:
DispatchQueue.main.async {
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
print("h", height)
self.scrollHeightConstraint.constant = height
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
I need to update constraints (height of my CollectionView) when request is done and I have images from server, so height of View also will change.
Result screen
what I need screen
My code:
DispatchQueue.main.async {
self.cvActivity.alpha = 0
self.collectionView.reloadData()
self.collectionView.heightAnchor.constraint(equalToConstant: self.cellWidth * 2).isActive = true
self.collectionView.setNeedsUpdateConstraints()
self.collectionView.setNeedsLayout()
self.view.layoutIfNeeded()
}
Well, basic idea by #J. Doe is correct, here some code explanation (for simplicity i used UIView, not UICollectionView):
import UIKit
class ViewController: UIViewController {
#IBOutlet var heightConstraint: NSLayoutConstraint! // link the height constraint to your collectionView
private var height: CGFloat = 100 // you should keep constraint height for different view states somewhere
override func updateViewConstraints() {
heightConstraint.constant = height // set the correct height
super.updateViewConstraints()
}
// update height by button press with animation
#IBAction func increaseHight(_ sender: UIButton) {
height *= 2
UIView.animate(withDuration: 0.3) {
self.view.setNeedsUpdateConstraints()
self.view.layoutIfNeeded() // if you call `layoutIfNeeded` on your collectionView, animation doesn't work
}
}
}
Result:
Define a height for your collectionView, create an outlet from that constraint and increase the constant of that constraint and call layoutifneeded in an animation block
You need to make object of your Height constraint from storyboard
#IBOutlet weak var YourHeightConstraintName: NSLayoutConstraint!
YourConstraintName.constant = valueYouWantToGive
---------OR--------
collectionViewOutlet.view.frame = CGRect(x:
collectionViewOutlet.frame.origin.x , y:
collectionViewOutlet.frame.origin.y, width:
collectionViewOutlet.frame.width, height:
yourheightValue)
I have a VC as shown in the image
It has a UICollectionView on top, and a UITableView at the bottom.
CollectionView has 1:3 of the screen and TableView has 2:3 of the screen(set using equalHeight constraint).
I want to change the height of the UICollectionView when the tableView is scrolled.
When the tableView is scrolled up,I want to change the multiplier of equalHeights constraint to like 1:5 and 4:5 of collectionView and tableView respectively.This will ensure that height of tableView increases and collectionView decreases
When the tableView is scrolled down, the multiplier of equalHeights constraint should reset to default.
I've tried adding swipe and pan gestures to tableView, but they are unrecognised.
How to achieve this functionality?
P.S: Would love to achieve this functionality by using a pan gesture, so that dragging up and down changes the heights progressively.
This is the view hierarchy
EDIT/UPDATE
This is the code that I'm using.
class MyConstant {
var height:CGFloat = 10
}
let myConstant = MyConstant()
MainScreenVC
override func viewWillAppear(_ animated: Bool) {
myConstant.height = self.view.frame.size.height
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (self.lastContentOffset < scrollView.contentOffset.y) {
NotificationCenter.default.post(name: Notifications.decreaseHeightNotification.name, object: nil)
self.topViewConstraint.constant = -self.view.frame.height / 6
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
} else if (self.lastContentOffset > scrollView.contentOffset.y) {
NotificationCenter.default.post(name: Notifications.increaseHeightNotification.name, object: nil)
self.topViewConstraint.constant = 0
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
}
self.lastContentOffset = scrollView.contentOffset.y
}
Cell.Swift
override func awakeFromNib() {
super.awakeFromNib()
NotificationCenter.default.addObserver(self, selector: #selector(decreaseHeight), name: Notification.Name("decreaseHeightNotification"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(increaseHeight), name: Notification.Name("increaseHeightNotification"), object: nil)
self.contentView.translatesAutoresizingMaskIntoConstraints = false
heightConstraint.constant = (myConstant.height / 3) - 10
widthConstraint.constant = heightConstraint.constant * 1.5
}
#objc func decreaseHeight() {
heightConstraint.constant = (myConstant.height / 6) - 10
widthConstraint.constant = (heightConstraint.constant * 1.5)
self.layoutIfNeeded()
}
#objc func increaseHeight() {
heightConstraint.constant = (myConstant.height / 3) - 10
widthConstraint.constant = (heightConstraint.constant * 1.5)
self.layoutIfNeeded()
}
Now when I scroll both simultaneously, the screen freezes. Also is there a better way of resizing the collectionViewCell size?
I haven't tested it but you can do this. Use Autolayout in this view. it will work better with that.
Set the tableview constraint as Top, Left, Bottom, Right => 0, 0, 0, 0 with the main view and put collectionView under the tableview with constraint as Top,Left,Right, height => 0, 0, 0, x with the main view.
Note: Tableview is on top of the collectionView.
Connect your height constraint outlet of CollectionView and also define your defaultOffset variable
#IBOutlet weak var defaultHeightConstraint: NSLayoutConstraint
var defaultOffSet: CGPoint?
In viewDidLoad,
override func viewDidLoad() {
super.viewDidLoad()
tableView.contentInset = UIEdgeInsetsMake(collectionView.size.height, 0, 0, 0)
}
In viewDidAppear, write
override func viewDidAppear(_ animated: Bool) {
defaultOffSet = tableView.contentOffset
}
In ViewDidScroll
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = tableView.contentOffset
if let startOffset = self.defaultOffSet {
if offset.y < startOffset.y {
// Scrolling down
// check if your collection view height is less than normal height, do your logic.
let deltaY = fabs((startOffset.y - offset.y))
defaultHeightConstraint.constant = defaultHeightConstraint.constant - deltaY
} else {
// Scrolling up
let deltaY = fabs((startOffset.y - offset.y))
defaultHeightConstraint.constant = defaultHeightConstraint.constant + deltaY
}
self.view.layoutIfNeeded()
}
}
Hope it helps.
Since UITableView is a subclass of UIScrollView, your table view's delegate can receive UIScrollViewDelegate methods.
You don't need to use PanGesture
First implement UITableviewDelegate and in that
var oldContentOffset = CGPoint.zero
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y - oldContentOffset.y
if (self.oldContentOffset < scrollView.contentOffset.y) {
// moved to top
} else if (self.oldContentOffset > scrollView.contentOffset.y) {
// moved to bottom
} else {
// didn't move
}
oldContentOffset = scrollView.contentOffset
}
Here you can write you logic to change constant value of your constraint
UPDATE/EDIT
Another thing is don't use multiplier for tabelview height.with collection view it is fine you can give height constraint with self.view in ratio of 1/3 . for tableview just give leading ,trailing top (UICollectionView) and bottom.
So when you change height constant of UICollectionView Tableview height automatically decrease and vice-versa for decrease.
You can not change the multiplier from the outlet as it is read only. so you have to constant value to self.view.frame.height / 3 will work for you
Hope it is helpful to you
For a much cleaner approach, why don't you use a UIScrollView as the container view and add the UICollectionView and UITableView inside it and give the collectionView a height constraint while disabling tableView's scroll till the height constraint of your collectionView becomes 0.
Here's the snippet:
Extend your VC with UIScrollViewDelegate and use:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y >= 0 && scrollView.contentOffset.y <= yourLimit {
heightConstraint.constant = collectionViewHeight - scrollView.contentOffset.y
tableView.isScrollEnabled = false
} else if scrollView.contentOffset.y > yourLimit {
heightConstraint.constant = collectionViewHeight
tableView.isScrollEnabled = true
}
}
Try this snippet after customizing small things in it. It should work mostly, if it doesn't there is another approach which I'd share once this doesn't work.
Why don't you just add Custom Table Cell with your collection view inside, and when indexPath.row == 0 , return your custom table cell.
I am trying ot troubleshoot a tableView header (pink) that is animating a collapse. As the tableViewHeader height is shrinking the table view cells should pull up with the top of their tableView (orange). The beginning and end states are correct, but somehow the table view cells are animating up at a different rate. Something is clearly wrong here, I just can't seem to pinpoint what it is.
It appears to have something to do with the fact that I am using self sizing table view cells and tableView.rowHeight = UITableViewAutomaticDimension. If I use fixed height cells everything is fine.
Beginning State:
Middle State (Note cells already sliding under header):
Final State (Final state of layout is correct):
Here is the code that animates the collapse.
func collapseHeader() {
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn, animations: {
self.tableView.beginUpdates()
if let header = self.tableView.tableHeaderView as? TopicTableHeaderView {
header.setHeaderState(state: .collapsed)
}
self.sizeHeaderToFit()
self.view.layoutIfNeeded()
self.tableView.endUpdates()
}) { (bool) in
print("collapse completed")
}
}
func sizeHeaderToFit() {
if let headerView = tableView.tableHeaderView {
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
var frame = headerView.frame
frame.size.height = height
if headerView.frame.height != height {
headerView.frame = frame
tableView.tableHeaderView = headerView
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
}
}
}
And the problem was simply where I was calling self.tableView.beginUpdates(). I moved that to the line directly above self.tableView.endUpdates() and that solved the problem.
If you like to try the source code (which you are very welcome to do), have a look at my Bitbucket repository.
I have a popover dialogue that shows a list of settings. These settings a listed inside multiple UITableViews. The UITableViews shall not be scrollable, for the overall settings view already is. Furthermore, the popover dialogue shall take as much screen vertically as it needs but shall be horizontally compressed.
Thus, I conceived the following structure:
UIView => MySettingsViewController
- UIScrollView
- UIView (Content View)
- Container View1
- UITableView (embedded) => MyTableViewController
- Container View2
- UITableView (embedded)
The structure is assembled via Interface Builder and Autolayout is used for the sizing.
I have both the Scroll View, the Content View (I started with just one) and the Container View to their respective superviews (or layout guides). I constrained the size of the content view in the following manner:
contentView.width == (topmost) UIView.width
contentView.height == 200 // removed at build time
Additionally, I set the size of the table view to its content size, because otherwise the popover appears to be empty:
class MyTableViewController: UITableViewController {
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// this is Cartography syntax - the intention should be clear
layout(view, replace: ConstraintGroup()) { [unowned self] view in
view.width == self.tableView.contentSize.width
view.height == self.tableView.contentSize.height
}
view.setNeedsLayout()
}
}
The settings popover is filled with content, but its size is not quite right:
To fix this, I tried the following approach which does not work:
class MySettingsViewController: UIViewController {
override var preferredContentSize: CGSize {
get {
let compressedSize = view.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
// this is always (0, 0) because the subviews are not resized, yet
return compressedSize
}
set {
super.preferredContentSize = newValue
}
}
}
To conclude: The compression does not work.
So I just fixed the problem myself as you can see when looking at the Bitbucket repository.
The layout is now fixed both in MyTableViewController and MySettingsViewController. The former one now looks like this:
class MyTableViewController: UITableViewController {
var heightConstraint: NSLayoutConstraint?
var tableViewEdgesConstraints: [NSLayoutConstraint]?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if let container = tableView.superview where tableViewEdgesConstraints == nil {
layout(tableView, container, replace: ConstraintGroup()) { [unowned self] tableView, container in
self.tableViewEdgesConstraints = tableView.edges == inset(container.edges, 0)
}
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let heightConstraint = heightConstraint {
if Int(heightConstraint.constant) != Int(tableView.contentSize.height) {
heightConstraint.constant = self.tableView.contentSize.height
}
} else {
layout(view, replace: ConstraintGroup()) { [unowned self] view in
if (self.tableView.contentSize.height > 0) {
self.heightConstraint = view.height == self.tableView.contentSize.height
}
}
}
}
}
So basically, I constraint the height of the table to its content's height and change the constraint if the content's height changes. This is done as soon as the table is laid out. Furthermore, the nested table view is pinned by its edges to the edges of the container view. I think that this is mandatory because I could not find out how to constrain two views of different scenes right in Interface Builder.
In MySettingsViewController the scrollview's size is set to the size of the content view's frame (which is accessible via an outlet) as soon as this size is known. Furthermore, to make the popover compress, the preferredContentSize of the settings controller is adapted accordingly, when the height changes (if you omit the condition you might get yourself in a layout endless loop. Furthermore I did 3 things to make it possible to have a navigation controller wrapped around MySettingsViewController:
The width of the popover is set to a fixed value (otherwise it would sometimes expand to the full width).
The presentedViewController's preferredContentSize needs to be set equally.
I had to set the insets of the scrollView to 0 to avoid an ugly vertical offset - this solution is sub-optimal because it breaks the scroll view experience a bit. But it works.
Here is the code:
class MySettingsViewController: UIViewController {
#IBOutlet weak var contentView: UIView!
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentSize = contentView.frame.size
if (preferredContentSize.height != scrollView.contentSize.height) {
let newSize = CGSize(width: 400, height: scrollView.contentSize.height)
preferredContentSize = newSize
presentingViewController?.presentedViewController?.preferredContentSize = newSize
scrollView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
}
}
}
And this is the result: