I'm trying to show a UICollectionView in a UITableViewCell. Initially the CollectionView shouldn't be shown, but when the users presses a button the CollectionView should become visible with an animation. I got this working however the first time the CollectionView becomes visible it looks like the cells get zoomed out, if I hide the CollectionView and expand it again, the animation looks correct:
http://g.recordit.co/DBhZCmJKPj.gif
This is the code for animation the change:
func expand() {
tableView?.beginUpdates()
UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveLinear, animations: {
self.imageViewDisclosureIndicator.setImage(UIImage(named: "arrow-up"), for: .normal)
self.collectionViewHeight.constant = self.collectionView.intrinsicContentSize.height
self.layoutIfNeeded()
self.isExpanded = true
}, completion: nil)
tableView?.endUpdates()
}
func collapse() {
tableView?.beginUpdates()
UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveLinear, animations: {
self.imageViewDisclosureIndicator.setImage(UIImage(named: "arrow-down"), for: .normal)
self.collectionViewHeight.constant = CGFloat(0.0)
self.layoutIfNeeded()
self.isExpanded = false
}, completion: nil)
tableView?.endUpdates()
}
Any help would be appreciated!
Try putting self.layoutIfNeeded outside the animation.
Based off your code, I don't see why you need that line of code, but it is probably the cause for your problem. I think that the content would even load fine without that line, because the size of the content view of the collection view is independent of the height of the collection view.
Related
I use the second right bar button item as an indicator when there is a successful CloudKit sync. However, if the tableView is held in scroll (with items now under the navigation bar) when the indicator appears, the tableView bounces in sync with the animation. This does not happen if the user is not interacting with the tableView.
Here is a GIF demonstrating the effect.
The other UIBarButtonItems are set up in the storyboard. The one for my iCloud sync indicator is set up in code in viewDidLoad():
var cloudIndicator = UIImageView()
cloudIndicator.frame = CGRect(x: 0, y: 0, width: 25, height: 25)
cloudIndicator.contentMode = .center
cloudIndicator.transform = CGAffineTransform.identity
// Get existing right bar button item which was set up in storyboard
var rightButtonItems = self.navigationItem.rightBarButtonItems ?? []
let customButtonItem = UIBarButtonItem(customView: cloudIndicator)
rightButtonItems.append(customButtonItem)
self.navigationItem.rightBarButtonItems = rightButtonItems
This is the method that animates the cloud sync indicator:
func cloudLabelImageAlert(_ image: UIImage, tintColor: UIColor = .darkGray) {
DispatchQueue.main.async {
self.cloudIndicator.alpha = 0
self.cloudIndicator.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
self.cloudIndicator.tintColor = tintColor
self.cloudIndicator.image = image
// Animate icon appearing
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 10, options: [], animations: {
self.cloudIndicator.alpha = 1
self.cloudIndicator.transform = CGAffineTransform.identity
}, completion: { didFinish in
// Animate icon disappearing
UIView.animate(withDuration: 0.4, delay: 2.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: [], animations: {
self.cloudIndicator.alpha = 0
self.cloudIndicator.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
}, completion: nil)
})
}
)
Presumably this problem relates to the frame of the image view changing during the animation, but it seems strange that it only happens while the tableView is being interacted with.
Is there a way to prevent this happening, or a better way to animate an image view as a bar button item?
Edit
Thanks to advice in the comments, it turns out this is due to reloading the tableview and nothing to do with the animation.
I found the problematic code, which is called after a CloudKit sync:
if let index = self.tableView.indexPathForSelectedRow {
self.tableView.deselectRow(at: index, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
self.tableView.reloadData() // This is delayed as it was causing problems with autoselection (e.g. after coming from Spotlight or notification)
let image = UIImage(named: "cloudTick")!
self.cloudLabelImageAlert(image, tintColor: self.colors[0])
}
} else {
self.tableView.reloadData()
let image = UIImage(named: "cloudTick")!
self.cloudLabelImageAlert(image, tintColor: self.colors[0])
}
Commenting out the self.tableview.reloadData() lines stopped the glitch but the animation continued as expected.
I need to update the data at this point for the user. Is there a better way to do this?
It seems for some mysterious reason your navigation bar is showing "large title" for a moment which leads to contentInset change of the tableView.
So try to manually disable large titles at all in viewDidLoad:
if #available(iOS 11.0, *) {
navigationItem.largeTitleDisplayMode = .never
navigationController?.navigationBar.prefersLargeTitles = false
}
As mentioned on the comment section, you where probably invoking cloudLabelImageAlert(image:, tintColor:) method while loading new table cells.
About updating the data, i would suggest invoking table view reloadData() method after the cloud animation completes.
Hi i had somwhat similar glitch not same but for me it got fixed by putting self.automaticallyScrollInsets to false in viewDidLoad and while animating the imageView for hide and show in the final UIView.animate completion block i called reloadData(). Hope this helps and thanks !
I just trying to make a custom animation when i scroll to item programmatically. So when I do not compose my animation and using default by cell do not vanishing, but when i put scrolltoItem func inside UIView.animate func the last cell first vanishing and then scrollToItem animates.
In the second picture in the uppermost collectionView the located before indie game cell firstly disappears and only then collectionView scrolls from indie game cell to the next
Why this behavior takes place? Why when i do not animating it purposefully in my way, and just calling scrollToItem with animated = true func, nothing eliminates? if someone do know what happens with the cells, please give me a clue.
UIView.animate(withDuration: 10, delay: 0, options: .curveEaseInOut, animations: {
self.appsCollectionView.scrollToItem(at: IndexPath(item: self.actualNumberOfTheCell, section: 0), at: .centeredHorizontally, animated: false)
}, completion: nil)
I think you should have to do it with self.view.layoutIfNeeded()
self.appsCollectionView.scrollToItem(at: IndexPath(item: self.actualNumberOfTheCell, section: 0), at: .centeredHorizontally, animated: false)
UIView.animate(withDuration: 10, delay: 0, options: .curveEaseInOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
Hope it will work
Your reused cells might be overlapping because of the sudden scroll. The latest cell might have not been dequeued properly and is perhaps initialized using a content of a previous cell -> which is most likely an empty cell that hasn't been properly displayed during the scroll.
Try to reset your cell content using:
prepareForReuse() inside your cell's controller.
You should reset your UI to a default state here.
Here's the docs link to it
Don't forget to call a UI display/setup method for your cell, which should display your UI elements.
Hope it helps!
I think the issue is caused by collectionView recycling the cell too early, as you are using scrollToItem with animation = false.
I tried to split the animation to several smaller steps, It works, but the scroll is not smooth. Here is the code:
// self is extended from UICollectionView
scrollTo(x: 100, duration: 0.6, count: 4) { (finished) in
}
private func scrollTo(x:CGFloat, duration:TimeInterval, count:Int, completion:#escaping (Bool)->Void)
{
let xOffset = (x - contentOffset.x)/CGFloat(count)
let durationPart = duration/TimeInterval(count)
scrollToPart(xOffset: xOffset, duration: durationPart, count: count, completion: completion)
}
private func scrollToPart(xOffset:CGFloat, duration:TimeInterval, count:Int, completion:#escaping (Bool)->Void)
{
UIView.animate(withDuration: duration, animations: {
self.contentOffset.x += xOffset
}) { (finished) in
if count <= 1
{
completion(finished)
}
else
{
self.scrollToPart(xOffset: xOffset, duration: duration, count: count-1, completion: completion)
}
}
}
I used scrollToItem with animated = true at last.
I have an UIViewController which contains a table view and a simple view. Both of them are at the same level.
At startup my view starts hidden at the bottom and when I press a button I want my view to slide up. When I do this only 1/4 of the view is shown and not the complete view.
This worked okay before adding the table view, but now I don't understand why it doesn't fully show.
Here is the code to show and hide my view:
func showPicker(date: Date?) {
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut], animations: {
self.timePickerView.transform = CGAffineTransform(translationX: 0, y: 0)
}, completion: { _ in
})
}
func hidePicker() {
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut], animations: {
self.timePickerView.transform = CGAffineTransform(translationX: 0, y: self.timePickerView.frame.size.height)
}, completion: { _ in
})
}
And here is a screenshot with the view (below the buttons there should be an UIDatePicker which is not shown):
Someone know what the issue is ? I am trying to do this from the storyboards.
edit:
This is what I have right now, but it still doesn't work. It doesn't animate and it also shows just a part of the view. Apparently if I increase the height the view is shown even more, so somehow it says that the shown part is exactly 220 height, which is strange :/
func hidePicker() {
self.pickerBottomConstraint.constant = -220
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut], animations: {
self.timePickerView.layoutIfNeeded()
}, completion: { _ in
})
}
func showPicker(date: Date?) {
self.pickerBottomConstraint.constant = 0
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut], animations: {
self.timePickerView.layoutIfNeeded()
}, completion: { _ in
})
}
If you're using autolayout, I bet you do and you should, then the easiest way to do what you wanna do is to toggle the constraint of your view, see the gif I added below.
First is to have a reference to your either top or bottom constraint of your view you wanna show and hide. Then modify the constant of the constraint to adjust its position, in that way, you get the illusion that the view is hidden and shown. The demo below uses tableView too.
Hope this helps.
See a demo here showHide that accomplish what you want
Rather then transform, change your views center y position.
ex:
#IBOutlet weak var viewToAnimateOutlet: UIView!
#IBAction func showViewButtonAction(_ sender: Any) {
UIView.animate(withDuration: 1.5) {
self.viewToAnimateOutlet.center.y -= self.viewToAnimateOutlet.frame.height
}
}
#IBAction func hideViewButtonAction(_ sender: Any) {
UIView.animate(withDuration: 1.5) {
self.viewToAnimateOutlet.center.y += self.viewToAnimateOutlet.frame.height
}
}
What i did:
I used autolayout and provided constraint for ViewToAnimate View is
ViewToAnimates.leading = safeArea.leading "constant = 8"
ViewToAnimates.trailing = safeArea.trailing "constant = 8"
This constraint will place ViewToAnimate view outside of the main views bottom. so view will not visible until showViewButtonAction method called.
ViewToAnimates.top = safeArea.bottom "constant = 0"
ViewToAnimates.height = 130
I'm facing a bugging issue concerning the insertRowsAtIndexPaths function and precisely it's withRowAnimation parameter.
With the help of the stack community i've been able to modify the effect of insertion from fast fade to a longer fade (by overriding the function), but the whole effect completely disappears if i scroll the table view upwards, hide the cells from the view and scroll back again.
Because the cells are being reused the effect is rendered totally useless.
I've tried using scrollToRowAtIndexPath function (doesn't work in this case - it initiates after the cells have already loaded, only then it scrolls down and the effect has already gone away) as well as setting the content offset to tableView.contentSize.height - the result is somewhat satisfactory, but again doesn't prevent vanishing of the insertion effect when the user scrolls.
Also willDisplayCell is not an option, because it animates the cell every single time the user scrolls and i need the animation to run just once. Urrgh)
So far i'm out of ideas of how to prevent this issue from happening.
UPDATE:
Added the suclass code
class CustomTable: UITableView{
override func insertRowsAtIndexPaths(indexPaths: [NSIndexPath], withRowAnimation animation: UITableViewRowAnimation) {
self.endUpdates()
self.beginUpdates()
for indexPath:AnyObject in indexPaths{
let cell:UITableViewCell? = (super.cellForRowAtIndexPath(indexPath as! NSIndexPath));
if cell != nil {
cell!.alpha = 0
let animationBlock = { () -> Void in
cell!.alpha = 1;
}
if UIView.respondsToSelector(Selector("animateWithDuration(duration: , delay: , usingSpringWithDamping dampingRatio: , initialSpringVelocity velocity: , options: , animations: , completion: ")){
UIView.animateWithDuration(3, delay: 3.0, usingSpringWithDamping: 1.5, initialSpringVelocity: 0.0, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: animationBlock, completion: nil)
}else{
UIView.animateWithDuration(3.3, delay: 5.0, options:UIViewAnimationOptions.TransitionCrossDissolve, animations: animationBlock, completion: nil)
}
}
}
}
}
I am hiding my navigation bar and a UIView under it that acts as a extension bar to it when I scroll my page.
My app is built like:
VC that holds a container view with an embedded table view.
From the table view I have delegates that notify VC1 once a user scrolls up or down.
My problem now is that the animation dont looks that good. What I am trying to do is to animate the extension bar to animate up or down with a fade in or fade out effect as well. When that occurs I also update the top contraint on my container view so that the table view will fill the whole screen. (I am not sure if I use layoutneeded() right or if something else should be used when updating constraints)
My code:
func ContainerTableViewControllerScrolledUp(controller: ContainerTableViewController) {
self.navigationController?.setNavigationBarHidden(false, animated: true)
println("UP")
UIView.animateWithDuration(
1.5,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5,
options: nil,
animations: {
self.extensionV.alpha = 1
self.tableVConst.constant = 0
}, completion: { finished in
self.view.layoutIfNeeded()
}
)
}
func ContainerTableViewControllerScrolledDown(controller:ContainerTableViewController) {
self.navigationController?.setNavigationBarHidden(true, animated: true)
println("DOWN")
UIView.animateWithDuration(
1.5,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5,
options: nil,
animations: {
self.extensionV.frame.origin.y = CGFloat(-10)
self.tableVConst.constant = -41
self.extensionV.alpha = 0
}, completion: { finished in
self.view.layoutIfNeeded()
}
)
}
extensionV is the extension view
tableVConst is the top constraint for my container view that holds my table view
So how should I edit my code in order to get the extension view to animate up/down with a fade in/fade out effect?
Instead of calling self.view.layoutIfNeeded() in the completion block, try calling it inside the animation block on the last line before it returns.