Animation in UITableViewCell breaks after cell deallocation - ios

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)
}
}

Related

Sticky Menu in iOS Table/List

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.

How to animate UIStackView hiding inside UITableViewCell properly?

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.

UICollectionView cell is disappearing when dragging out of initial bounds (Swift3, CollectionView, Drag and Drop)

I have a problem, when dragging cell out of initial bounds.
On drag, I take value from UILongPressGestureRecognizer location and set cell "center" property to this location.
The problem occurs when I scroll down, below initial bounds, the cell doesn't show on the view (although there are cells existing down, probably loaded after first view appearing). When I scroll little up, within same take (press), cell shows up again.
I tried several things
cashing cells, so view alway get the same cell instance on reload
override func viewDidLayoutSubviews which lead in endless loop
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myCollection.collectionViewLayout.invalidateLayout()
}
collectionView?.clipsToBounds = false
None of these worked for me. Could you help me? Thanks!!!

Stop TableView / TableViewCell animation (probably on willdisplaycell)

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:

How to make UITableView appear from bottom to top when scroll?

I'm looking just for general scenario.The idea is when app launches user can see only first cell of UITableView at the bottom of UIViewController. When user scrolls up full table appears and when scrolls down only first cell is displayed again. Something similar like keyboard in Facebook messenger app, but with tableView. For now I added tableView as subview to scrollView, but problem is tableView appears from top to bottom, and I'm looking for solution how to make this work upside down.. So, tableView have to appear from bottom to top of UIViewController.
My idea would be:
Give your UITableView the desired frame at viewDiDLoad (probably the height of 1 cell, at the bottom of your UIView)
Let your UIViewController implement UIScrollViewDelegate
At - (void)scrollViewDidScroll:(UIScrollView *)scrollView of UIScrollViewDelegate check which element is scrolling (if its your UITableView) and also check which direction user is scrolling
Change the frame of the UITableView as you wish, you will also have to come up with some logic to block further changing of the UITableView's frame (a BOOL would do good here I guess)
Not sure I understand your correctly but, maybe, this will be helpful
I believe you can try to add zero cell (or first section header view of your UITableView) with transparent background. So that your first cell will be placed on the bottom of screen and UITableView height will be equal to screen height.
In this case, you will have only one scrolling view (UITableView, in particularly). Following method can be used to perform expandable animation and scroll table to top to hide zero cell when user taps on first cell:
[UITableView scrollRectToVisible:animated:]
After that you can leave UITableView as it is and "constrict" your table whenever you need in the same way as it was expanded before.
I understand what you want to do.
In general, UITableView shows cells from top to bottom.
You can add transform in tableview:
tableView.transform = CGAffineTransform(scaleX: 1, y: -1)
Then your tableview will show the cells from bottom to top and you can scroll tableview from bottom to top.
But cells will be transformed as tableview, so you have to add same transform to cells also.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell")
**cell.transform = tableView.transform**
return cell
}
All done. Hope to helpful!

Resources