I want to animate the subviews of the TableViewCell which is StackView. When I hide the StackView, the TableViewCell height not updating. After googling, I found that I should call tableView.beginUpdates and tableView.endUpdates to notify tableView that there is a change in the cell. The problem is the hide animation and the change of tableview not sync.
Here is the view hierarchy for tableview cell
Content view - Container View (for card shadow) - Container Stack View - [Stack View for label and switch] & [StudentStackView for container of StudentView]
How can I sync the cell height and hide animation the correct way?
Here is the github repo: GitHub
Gif of the App:
You are right in using beginUpdates()/endUpdates(). Make sure you're not placing the someArrangedSubview.isHidden = true/false in an animate block since the table view and stack view will handle the animations accordingly. When the table view begins update operations, the stack view will resize any arranged subviews that you aren't removing to fill the entire space of the cell (even if you have height constraints on the arranged subview). In my case, the cell content jumped every time I wanted to collapse a cell via removing an arranged subview--so I added a dummy view between the view I wished to remain static* and the collapsible view. The static view won't resize, and the dummy view will expand/collapse as needed. Hope this helps.
*static in the sense that I didn't want the view to move when animating.
`public func setup(classRoom: ClassRoom, toggleInProcess: #escaping () -> (), toggled: #escaping () -> ()) {
containerStackView.addArrangedSubview(studentStackView)
self.nameLabel.text = classRoom.name
self.activeSwitch.isOn = classRoom.isActive
self.studentStackView.isHidden = !self.activeSwitch.isOn // Let him know his hide/unhide.
for student in classRoom.students {
let studentView = StudentView()
studentView.nameLabel.text = student.name
studentStackView.addArrangedSubview(studentView)
}
activeSwitch.addTarget(self, action: #selector(toggleShowStudents(show:)), for: .valueChanged)
self.toggleInProcess = toggleInProcess
self.toggled = toggled
setupShadow()
}`
` #objc func toggleShowStudents(show: Bool) {
UIView.animate(withDuration: 0.3, animations: {
self.studentStackView.isHidden = !self.activeSwitch.isOn
self.toggleInProcess()
self.containerView.layoutIfNeeded()
}) { _ in
self.toggled()
}
}`
your studentStackView also know his hide/unhide status while assigning values in function setup.
I left this as a comment but for anyone else experiencing this behavior, the root cause is the UILabel is expanding to fill the visible area before collapsing.
This can be fixed by doing the following 2 things:
Right below the UILabel, insert a Blank UIView
Adjust the Content Hugging Priority of the UILabel to "Required"
With these two adjustments, instead of the UILabel expanding to fill the visible area, the UIView expands instead. Visually, this appears as if the the cell just collapses.
tableView.beginUpdates and tableView.endUpdates are functions that should be called when you are about to modify rowcount or selected state of the rows.
You should try reloadData or reloadrowsatindexpaths, that should take care of the cell height adjustment.
You would better do it using performSelector API so as not to cause recursion in cellForRowAt call stack.
Related
I have an expanding view in the cell. After i press show more button it works fine. But when i scroll down and that cell deallocates from memory and then i tap on show more button again animation breaks. How can i fix it?
Project repo on Github
Collapsed view:
Expanded view:
Debugger view, before scroll:
After cell deallocation (after scroll) breaks like this, expands behind second cell:
Based on your code and comment...
Instead of expanding your cell, you're setting clipsToBounds = false and expanding the cell's contents, allowing them to extend outside the bounds of the cell.
As you scroll a UITableView, cells are - of course - added / removed as needed. But when they are re-added, the "z-order" changes (because, with "standard" table view usage, it doesn't matter).
So, after scrolling down and then back up, and then tapping the "Show more" button, that cell may be (and usually is) at a lower z-order ... and thus its expanded "out-of-bounds" content is hidden behind the cell(s) below it.
If you want to stick with that approach (as opposed to expanding the cell itself), you can try implementing scrollViewDidScroll to make sure that cell is "on the top":
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// make sure the first row is visible (not scrolled off the top of the view)
let pth = IndexPath(row: 0, section: 0)
if let a = tableView.indexPathsForVisibleRows,
a.contains(pth),
let c = tableView.cellForRow(at: pth) {
// bring it to the front -- i.e. the top of the z-order
tableView.bringSubviewToFront(c)
}
}
I'm trying to create an interface similar to the Home scene in the meetup app. You can see it in action below. I want to recreate the [All, Going, ...] menu behavior. I want it to start in the middle of the list and scroll up until it reaches the top of the list and stick there. Very similar to how section headers work in a UITableView.
Creating the menu is not the issue. My problem is creating the sticky behavior and have it work well with the rest of the list.
I've tried using a UITableView but I couldn't get the menu cell to stick. I can't put the menu in a section header because I want to use section headers for the data below the menu and UITableView's behavior is to push a section header up when the next section reaches the top of the list. I can't put the menu in the UITableView.tableHeader because the menu starts below some other data in the list.
UITableView
- UITableViewCell -> Label
- UITableViewCell -> UICollectionView of UIImageViews
- UITableViewCell -> Label
- UITableViewCell -> MyMenu (Sticky)
- UITableViewHeaderFooterView - Section 1
- UITableViewCell -> Data
- UITableViewCell -> Data
- UITableViewHeaderFooterView - Section 1
- UITableViewCell -> Data
- UITableViewCell -> Data
I've tried using a UIScrollView containing the menu and a UITableView below it but using a UITableView (which is a UIScrollView) inside a UIScrollView is painful. I couldn't get the scrolling behavior to feel natural.
UIScrollView
- UIView -> (Container)
- Label
- UICollectionView of UIImageViews
- Label
- MyMenu (Sticky)
- UITableView - Data
I'm about to try and write a UICollectionViewLayout to do what I want but I feel like I will have to recreate functionality that I get for free with UITableView.
Any idea how to approach this? Perhaps there is a reliable method to make a UITableViewCell stick and for subsequent section headers to stick under it?
One way to implement something like this is with a view hierarchy like this:
UIView
- UITableView
- UIView -> (Container)
- Label
- UICollectionView of UIImageViews
- Label
- MyMenu (Sticky)
Your container with your menu is a sibling of the table view, but it overlaps it.
In the scroll view delegate method scrollViewDidScroll(_:) you can reposition your menu container view so the menu is positioned above the table content. Then you need to tell the table view to reserve some space between the top and the first table cell. For this you can configure the contentInset of the table view.
I would use a table view.
Add an empty cell that will be where your control will be placed while it's visible, and to avoid your control covering any content.
Add your control as a subview of your table view.
Then override scrollViewDidScroll (UITableView is a subclass of UIScrollView so they share delegate methods).
In scrollViewDidScroll, which gets called at least every frame while the scroll view is scrolling, update the position of the content, like this:
let controlFrame = tableView.rectForRow(at: indexPathOfYourBlankCell)
controlFrame.origin.y = max(0, tableView.contentOffset.y - controlFrame.y)
control.frame = controlFrame
tableView.bringSubviewToFront(control)
Keep in mind that you will have to tweak the second line if your table view has a top inset, for example, if it's under a transparent navigation bar, or you're using an iPhone with a notch.
I suggest implementing it first o an notch-less iPhone simulator, with no navigation bar, and once it works you can tweak the way the y property is calculated by adding the inset.
I think something like this would work, but I'm not sure.
controlFrame.origin.y = max(0, tableView.contentOffset.y + tableView.contentInsets.top - controlFrame.y)
I implemented #EmilioPelaez's suggestion of using a separate menu view, positioning it over an empty cell and moving it as the table scrolls. To make it work I had to do the following things:
Find the frame of the empty cell so I can position the menu over it
As the empty view moves outside the visible area of the table view move the menu so it stays inside the visible area of the table view. It should look like it is docked to the top of the table view.
When the empty cell reaches the top adjust the tableView.contentInsets.top so the section headers below look like they stick to the bottom of the menu
When the table scrolls in the other direction reset the tableView.contentInsets.top
Support dynamic type and rotation changes
I ended up doing everything in viewDidLayoutSubviews because I need to handle rotation and dynamic text changes and scrollViewDidScroll isn't called on every rotation and dynamic text change. viewDidLayoutSubviews is called after almost every scrollViewDidScroll.
let menuCellPath = IndexPath(row: 1, section: 1)
var tableViewInsetCached = false
var cachedTableViewInsetTop: CGFloat = 0.0
override func viewDidLayoutSubviews() {
// Cache the starting tableView.contentInset.top because I need to change it later
// when the menu is docked to the top of the table
if !tableViewInsetCached {
cachedTableViewInsetTop = tableView.contentInset.top
tableViewInsetCached = true
}
// Get the frame of the empty cell. Use rectForRow instead of cellForIndexPath so it
// works even if the cell has been reused.
let menuCellFrame = tableView.rectForRow(at: menuCellPath)
// Calculate how far the menu must move to continue to be within the
// visible area of the scroll view. If the delta is a negative number
// the cell is within the visible area so clamp it at 0, i.e., don't move it.
// Use `tableView.safeAreaInsets.top` to take into account the notch, translucent
// UINavigationBar, and status bar.
let menuFrameDeltaY = max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y)
// Add the delta to the menu's frame
var newMenuFrame = menuCellFrame
newMenuFrame.origin.y = menuCellFrame.origin.y + menuFrameDeltaY
menuView.frame = newMenuFrame
if menuFrameDeltaY > 0 {
print("cell outside visible area -> change contentInset")
// Change the contentInset so subsequent section headers
// stick to the bottom of the menu
tableView.contentInset.top = menuCellFrame.size.height
} else {
print("cell inside visible area -> reset contentInset")
// The empty cell is inside the visible area so we should
// reset the contentInset
tableView.contentInset.top = cachedTableViewInsetTop
}
}
It's important to remember that we are dealing with a UIScrollView under the hood. The frames of its subviews don't change as the table is scrolled. Only the contentOffset changes which means that max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y) calculates the amount the menu must move to continue to be within the visible area of the table view. If the delta is less than zero the empty cell is within the visible area of the table view and I don't have to move the menu, just give it the same frame as the empty cell which is why I use max(0, x) to clamp it at zero. If the delta is greater than zero the empty cell is no longer within the visible area of the table view and the menu must be moved to continue to be within the visible area.
I've noticed this with a couple of UITableView's that I have coded, and that is when there is a lengthy list, or if I animate the displaying of the table itself, the cells have an animation, like the table is constructing the cells as I'm scrolling down.
The animation looks like the accessory view sliding from the left to the right, and I have a top-right label that does the same.
On another table that I slide in from the bottom, it looks like the buttons and text from the table expand as the table does.
How do I stop this?
I am using Swift, but if you know the answer in OBJ-C that would be okay too.
Also, if you need a preview, check this:
I've tried searching for this and I only get posts about making an animation within the table cells not removing this one!
So I found out how to stop it! It was because of the accessory view and making sure the cells have performed the layoutIfNeeded.
First, I removed the accessoryview from StoryBoard and in willDisplayCell: I used this:
UIView.performWithoutAnimation { () -> Void in
cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator
cell.layoutIfNeeded()
}
As well as:
UIView.performWithoutAnimation { () -> Void in
cell.layoutIfNeeded()
}
in cellForRowAtIndexPath:
I designed a custom view as my UITableView's header view. just like this
(I just put image link here instead of image since I don't have 10 reputations.)
http://i.stack.imgur.com/KhNbE.png
Then in my UITableViewController I use this view as tableHeaderView
override func viewDidLoad() {
tableView.tableHeaderView = headerView!
//...other things
}
I got text from a JSON to fulfill the ContentLabel. If the text is long, the headerView will overlap cells just like below image.(short text is OK)
http://i.stack.imgur.com/gtO2g.png
Section is visible but two lines of cell have been overlapped by the headerView.I'm not sure if I did wrong constraints or code on ContentLabel. Below is the code I configured the contentLabel in TopicHeaderView.swift
var content: String? {
didSet {
self.contentLabel.text = content!
self.setNeedsUpdateConstraints()
self.updateConstraintsIfNeeded()
self.setNeedsLayout()
self.layoutIfNeeded()
}
}
func setFrameHeight(height: CGFloat) {
var frame = self.frame
frame.size.height = height
self.frame = frame
}
override func layoutSubviews() {
super.layoutSubviews()
self.contentLabel.preferredMaxLayoutWidth = self.contentLabel.alignmentRectForFrame(contentLabel.frame).width
self.titleLabel.preferredMaxLayoutWidth = self.titleLabel.bounds.size.width
self.authorLabel.preferredMaxLayoutWidth = self.authorLabel.bounds.size.width
self.setFrameHeight(CGRectGetMaxY(contentLabel.frame) + 8)
}
I browsed similar questions in SO but seems I can't find a solution to fix my problem. Can anyone help on this?
EDITED:
I logged the origin CGPoint of my first tableView cell and headerView's height. It shows the right number which means the first cell is right next to the header view vertically. There is a 22 points gap because of the height of section of course.
headerheight:600.0
first cell's y: 622.0
So maybe it's the label problem that its height is too big to exceed the bounds of TableView headerView? I'm not sure.
EDITED:
Strange things happen. I logged the y value of headerView's bottom,contentLabel's bottom and first UITableViewCell's origin. Please see the image from the link in the question comment below(still need 10 reputation)
As you can see, from the value in console, the view sequence from top should be "contentLabel's bottom(value:224) - headerView's bottom bounds(value: 232) - first cell's origin(value:254)". But in simulator, the sequence is totally messed up.It turns "headerView's bottom bounds - first cell's origin - contentLabel's bottom"
I really appreciate if anyone can help on this.
Problem is, that UITableView does not automatically change positions of cells when its headerView's height changes. Thus you need to reload UITableView every time TopicHeaderView.content changes.
Select that header view, or imageView what you have there, and check Clip Subviews in Attributes Inspector tab.
This worked for me.
I'm looking to expand the height of the top cell in my collectionview with animation. The problem is when I detect a tap on the cell, I try animating the height and set the new height in the delegate: - collectionView:layout:sizeForItemAtIndexPath:indexPath. The issue I see an instant refresh after invalidating my collection flow layout and the animation happens afterwords. The effect I want is for the top cell to animate the expansion to the new height and push the rows beneath it along with the animation.
I'm only using one section with multiple rows. Should I try to use two sections and put the top cell in the first section and the remaining in the second section? Could I then animate the second section along with the height of the first section?
You need to do this three steps:
1.Invalidate your collectionViewLayout
self.collectionView.collectionViewLayout.invalidateLayout()
2.Reload desired indexPath or all the data inside a performBatchUpdates block
collectionView.performBatchUpdates({
self.collectionView.reload(at: DESIRED_INDEXPATH)
}, completion: nil)
3.Return the new height calculated in sizeForItemAtIndexpath delegate method