I have UITableView, and I put container view on top as a header like this:
And everything work well.
Now I would like to hide this container header view, when tap in the cell.
My first attempt was :
UIView.animate(withDuration: 0.3, animations: {
self.containerView.frame.size.height = 0
})
witch hide the view, but don't bring cells up, what it is expected and normal, because I only change frame for container view, and not for other views.
Then I try to add constraints in storyboards, but for some reason I can't set them.
Why is that? And how do I achieve to hide container view, and bring all other cells to the top.
Try this
UIView.animate(withDuration: 0.5) {
let headerView = tableView.tableHeaderView!
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
headerView.frame.size.height = 0
tableView.tableHeaderView = headerView
}
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 have created a screen in which I have a full-screen UITableView, I set UIEdgeInsets to it, which I have configured as follows:
categoriesTableView.contentInset = UIEdgeInsets.init(top: HEADER_VIEW_HEIGHT, left: 0, bottom: 0, right: 0)
where HEADER_VIEW_HEIGHT is CGFloat = 160.
This allowed me to add a "header view" to the UITableView which is getting covered when I start scrolling the UITableView (...and not getting stuck above the UITableView, as a real header view will).
The Problem: The problem is that I need to have 3 clickable views in the header view, So I designed 3 views in the storyboard and configured Tap Gesture Recognizers on them. But when I try to use them the tap gestures are not passed throw the UITableView even though I see those views on screen (as a result of the contentInset configuration). The only way I can make them tapable/clickable is if I set User Interaction Enabled to false on the UITableView (which I can't do because I need the UITableView to be draggable and clickable as well).
The Question: How would I pass the tap events to the lower "header view" clickable parts when it's not covered by the UITableView as a result of the contentInset setting?
Here is the UI image, in it you can see that there is a full-screen UITableView, behind it there a view with 3 subviews that contains 3 favorite items which I can present to the user for an easier access. when the screen starts, there is a contentInset for the UITableView hence the user can see those easy access options and press them (which he can't do right now). When the user starts scrolling, the UITableView goes on top of the layout with the 3 views and the user able to scroll the list in a full screen. kind of like a parallax effect.
I have a very ugly solution for this..
Now you have a table view and below that there is a header view right? Add one more view on top of table view which contains 3 sub views in it and all transparent in colour.
Position this newly added subview same as that of header view (Either by programmatically giving same frame as that of header view or by connecting its top, left, bottom and right constraints to the header view). Similarly position the 3 sub views of this new view as same as that of the subviews inside the header view. And give tap gesture to the subviews of this new view. So our user will think he is tapping the header view, whereas he is actually tapping in this invisible view.
And if you want to get touch in the table view once it scrolls up and cover the header view, then you can use one of these two..
Call the UIScrollViewDelegate delegate method scrollViewDidScroll() and inside if scrollView.contentOffset.y >= 160, set the User Interaction Enabled to false for this newly added view and revert that back to true if scrollView.contentOffset.y < 160.
Or from inside scrollViewDidScroll(), assign outletOfTopConstraintOfYourNewView.constant = -(scrollView.contentOffset.y), so that the new view will also move up according to the scroll, thereby changing its visible tappable area.
Another not so elegant idea is..
Instead of giving contentInset, add one more section in this table view at the 0th index with only one cell whose height is 160, user interaction enabled is false and colour is transparent. Then you don't have to worry about the logic in scrollViewDidScroll().
Update:
Below solution is not perfect but it may give you a direction to start with.
A) Put the top constraint of tableView top to bottom constraint of headerview.
B) Create IBOutlet of height constraint of headerview in your ViewController
C) Listen to tableview's ScrollViewDidScroll and put code like this
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let y = scrollView.contentOffset.y
if y > 50{
if heightConstant.constant != 0{
view.layoutIfNeeded()
heightConstant.constant = 0
UIView.animate(withDuration: 0.5, delay: 0, options: .allowUserInteraction, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}else{
view.layoutIfNeeded()
heightConstant.constant = 160
UIView.animate(withDuration: 0.5, delay: 0, options: .allowUserInteraction, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
Here 50 is a point form where it should start animating. It is somewhat similar to Collapsable Toolbar in Android. Just make sure headerView.isClipsToBounds = true.
Convert your headerview into a UITableViewCell and in change your UITableViewDataSource as
extension ViewController: UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == 0 ? 1 : yourList.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0{
// set your header view here
}else{
// Your normal cell configuration
}
}
}
With this, your headerview will become a part of table view but as a UITableViewCell thus it will not behave like sticky header like the normal one.
I have a simple UI (very similar to Uber) where the user can scroll a table view on top of the main content.
The UI and the corresponding UITableView animation bug can be displayed as:
Here is what is happening:
User taps on the table view and the table view is expanded. This is done via adding the layout constraint that makes sure that tableView.top = topMenu.bottom. The constraint that gets removed is tableView.height = 30. Everything looks good.
User then scrolls down a certain amount (55+ pixels) and the constraints are reverted back to their original states. This happens inside an animation block so that the flow looks smooth.
The bug occurs here. As far as I understand, the tableView's visible area is calculated of how it will look like after the animation ends. However, this calculation happens before the animation. Therefore, during the animation only 1-2 cells are displayed on the table view; causing the bug.
I can have a workaround here by temporarily setting the tableView's height to a large value and only setting it back to a small value after the animation ends. However, that doesn't work because the safe area on iPhoneX gets covered by the tableView.
Relevant code is here:
private func animateTheChange() {
UIView.animate(withDuration: 0.8, animations: {
self.view.setNeedsUpdateConstraints()
self.view.layoutIfNeeded()
})
}
override func updateViewConstraints() {
self.touchConstraints()
super.updateViewConstraints()
}
private func touchConstraints() {
if self.collapsed {
self.view.addConstraint(self.collapsedConstraint)
self.view.removeConstraint(self.expandedConstraint)
if UserHardware.IS_IPHONE_X {
self.bottomConstraint.constant = 0
}
}
else { // Expand
self.view.addConstraint(self.expandedConstraint)
self.view.removeConstraint(self.collapsedConstraint)
if UserHardware.IS_IPHONE_X {
self.bottomConstraint.constant = 34
}
}
}
Relevant Stackoverflow questions (that help but don't solve the issue):
UITableView frame height animation glitch
Dynamic UITableView height
UITableView frame change animation issue
One option...
Embed your tableView inside a "containing" UIView
Constrain the tableView to Top and Bottom of the containing view
Constrain the containing view Bottom to the Safe Area Bottom
Constrain the containing view Top to the Bottom of topMenu with a Priority of 250 (default low), and connect it to #IBOutlet var tableContainerTop: NSLayoutConstraint!
Constrain the Height of the containing view to 30 with a Priority of 750 (default high), and connect it to #IBOutlet var tableContainerHeight: NSLayoutConstraint!
When you want to "expand" or "collapse" your tableView, change the priorities of the containing view's constraints.
UIView.animate(withDuration: 0.8, animations: {
if self.isExpanded {
self.tableContainerHeight.priority = .defaultHigh
self.tableContainerTop.priority = .defaultLow
} else {
self.tableContainerHeight.priority = .defaultLow
self.tableContainerTop.priority = .defaultHigh
}
self.view.layoutIfNeeded()
})
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.
I'm trying to add a view to my tableview as a tableHeaderView. I drag a view from the interface builder and drop it into my tableview. The size of the header is a small, so I resize it by coding. When I run my app, it becomes like this:
If I change background colour to white colour
If I change background colour to clear idea
Here is the code I change my tableview header size:
self.myTableView.tableHeaderView?.frame = CGRectMake(0, 0, screenWidth, 290)
Any suggestion to fix it please!
Use this UItableView extension.
I was having the same issue
extension UITableView {
func setTableHeaderView(headerView: UIView?) {
// set the headerView
tableHeaderView = headerView
// check if the passed view is nil
guard let headerView = headerView else { return }
// check if the tableHeaderView superview view is nil just to avoid
// to use the force unwrapping later. In case it fail something really
// wrong happened
guard let tableHeaderViewSuperview = tableHeaderView?.superview else {
assertionFailure("This should not be reached!")
return
}
// force updated layout
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
// set tableHeaderView width
tableHeaderViewSuperview.addConstraint(headerView.widthAnchor.constraint(equalTo: tableHeaderViewSuperview.widthAnchor, multiplier: 1.0))
// set tableHeaderView height
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
tableHeaderViewSuperview.addConstraint(headerView.heightAnchor.constraint(equalToConstant: height))
}
}
Create a xib which holds the kind of header view you want to add it on TableView. Then load your xib in viewWillAppear method of the controller using [[NSBundle mainBundle]loadNibNamed:#""] method. Now add that NSBundle returned view as tableView header by using, tv.tableHeaderView = <your UI view>;