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 !
Related
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 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.
I have an audio player view at the bottom of my application. I want this audio player view to hide with a slide animation at the bottom of the screen once it finishes the last item in the playlist. At the start of the application, I need this audioplayer view to be hidden until the user taps an audio file to play.
The issue I am having is that the audioplayer view won't move offscreen at the beginning of the VC loading.
What's odd is that I have a similar function that moves the audioplayer view offscreen correctly, and everything works fine. This seems like it is only an issue at load time - that initial hiding of the audioplayer view.
Code:
override func viewDidLoad(){
super.viewDidLoad()
...
footerView.backgroundColor = UIColor.clear
footerView.superview?.backgroundColor = UIColor.clear
footerView.playButton.tintColor = UIColor.red
footerView.playButton.borderColor = UIColor.red
initializeFooterView()
...
}
//print statements called #viewDidLoad, but not the translate function
func initializeFooterView(){
print("initFooterView", String(describing: footerView.superview?.frame.origin.y))
print(String(describing: footerView.frame.size.height))
footerView.superview?.frame.origin.y += footerView.frame.size.height
print("initFooterView", String(describing: footerView.superview?.frame.origin.y))
}
//Working function to show/hide audioplayer view... works during runtime
func hideShowFooterView(){
let animationOptions: UIViewAnimationOptions = .curveEaseOut
let keyframeAnimationOptions: UIViewKeyframeAnimationOptions = UIViewKeyframeAnimationOptions(rawValue: animationOptions.rawValue)
if let footerView = self.footerView{
if (footerView.superview?.isHidden)!{
footerView.superview?.isHidden = false
UIView.animateKeyframes(withDuration: 0.3, delay: 0.0, options: keyframeAnimationOptions , animations: {() in
footerView.superview?.frame.origin.y -= (footerView.superview?.frame.size.height)!
}, completion: nil)
}else{
UIView.animateKeyframes(withDuration: 0.3, delay: 0.0, options: keyframeAnimationOptions , animations: {() in
footerView.superview?.frame.origin.y += (footerView.superview?.frame.size.height)!
}, completion: { (completed) in
if completed{
footerView.superview?.isHidden = true
}
})
}
}
}
Cleaned up print statements called from initializeFooterView() that moves audioPlayer view offscreen within viewDidLoad():
initFooterView() - footerView.superview?.frame.origin.y: 0.0
initFooterView() - footerView.frame.size.height = 75.0
initFooterView() - footerView.superview?.frame.origin.y: 75
If you're wondering why I translate the view on y by footerView.frame.size.height in initializeFooterView(), but translate the view by footerView.superview.frame.size.height in the hideShowFooterView(), it's because the height of the superview at viewDidLoad is 763, for some reason, while the height of the footerView's frame is 75 (the correct amount). It translates correctly during runtime, however, so I use the footerView's superview.frame.
Hierarchy of my views:
Container view set up: (footerviewcontroller is segued from a container view... not sure if that's influential)
I have a feeling that there is a conflict in defining my footerview within a storyboard, then trying to change it programmatically during viewDidLoad(). I don't want to have to define everything about footerView programmatically, though :/
It's a little tough to see what you're doing... Are you setting constraints on the footerView but then explicitly setting the frame? If so, that might be part of the issue.
However, since you say it's working fine in hideShowFooterView(), try moving initializeFooterView() from viewDidLoad() to viewWillAppear()
I have a tableview and if a cell is tapped on the tableview, I want to create a UIView to pop up over the tableview and display the contents in the tableview cell. I want to add the code in tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) so that the UIView is opened when I select a row. I just need to know how to create a popup with UIView. I couldn't find any source which helped me out with this. A bit of explanation on how to do this will be awesome. Thanks in advance!
Step-1
create one UIView set the frame as View.bounds at the same time add the Tap gesture for hide function.
Step-2
for present the view on click, use
UIView.animateWithDuration(1.0, animations: {
yourView.alpha = 1.0
})
Step-3
on hide
UIView.animateWithDuration(1.0, animations: {
yourView.alpha = 0.0
})
finally if you want to hide use hidden or removefromsuperview
If you are using IOS 8 or later you can just create UIView in the new UIViewController with setting attribute ModalPresentationStyle with value UIModalPresentationStyle. Something like this in you didSelectRowAtIndexPath:
var popoverContent = self.storyboard?.instantiateViewControllerWithIdentifier("NewCategory") as UIViewController
var nav = UINavigationController(rootViewController: popoverContent)
nav.modalPresentationStyle = UIModalPresentationStyle.Popover
var popover = nav.popoverPresentationController
popoverContent.preferredContentSize = CGSizeMake(500,600)
popover.delegate = self
popover.sourceView = self.view
popover.sourceRect = CGRectMake(100,100,0,0)
self.presentViewController(nav, animated: true, completion: nil)
First add let vw = UIView(), then try this
vw.frame = CGRectMake(150, 150, 0, 0)
vw.backgroundColor = UIColor.redColor()
self.view.addSubview(vw)
UIView.animateWithDuration(0.5, delay: 0, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
self.vw.frame = CGRectMake(75, 75, 300, 300)
}, completion: nil)
First create a UIView, and set its X and Y point(150), then width and height (300). I just simply add a background color to show that it is really there. Then the animatedWithDuration part is to reset the frame so it looks like pop up.
add this if you want the view behind become darker.
view.backgroundColor = UIColor(white: 1, alpha: 0.5)
As for touching background view to close the pop up view. You need a tap gesture recogniser, and function for it
var tap = UITapGestureRecognizer()
tap.delegate = self
tap.addTarget(self, action: "tapped")
self.view.addGestureRecognizer(tap)
then for its function
func tapped(){
vw.removeFromSuperview()
}
I have a UITableView with a UISearchBar. I need to show/hide my searchBar, by user action (button pressing):
I tried to use this code:
if self.tableView.contentOffset.y == 0 {
self.tableView.contentOffset = CGPoint(x: 0.0, y: self.searchBar.frame.size.height)
}
from this question.
Actually I've tried all of those answers. All of them just scroll my UITableView, and each time I'm scrolling my UITableView - searchBar appears.
I tried to do something like this:
self.newsTableView.tableHeaderView = nil; //Hide
and
self.newsTableView.tableHeaderView = self.SearchBar; //Show
But UITableView doesn't want to return searchBar;
How can I resolve this problem?
I need to hide searchBar by action, not to scroll my UITableView (hide like searchBar.hidden = true) Actually, searchBar.hidden = true works, but there is a white space instead of searchBar.
Use UIView.animation for searchBar and tableView
When you start scroll table
Change position/alpha of search bar and height of tableView
UIView.animateWithDuration(1.0, animations: {
searchBar.alpha = 0.0
tableView.view.frame.height = CGRect(x: 0, y: 0, width: UIScreen.mainScreen().bounds.width, height: UIScreen.mainScreen().bounds.height)
}, completion: {
(value: Bool) in
//do nothing after animation
})
In Xcode 7.3 in the ViewController.swift file it worked properly for me
CategoryTableView.tableHeaderView = searchController.searchBar // show
CategoryTableView.tableHeaderView = nil // hide
searchController.active = false
hope this could solve your problem. call this at launch of tableview to hide the searchBar and when you scroll down it will appear
var newBounds: CGRect? = self.newsTableView.bounds
newBounds?.origin.y = 0
newBounds?.origin.y = newBounds!.origin.y + self.searchBar.bounds.size.height
and then set the new bound of the tableView