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()
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 was hoping someone could help.
I have two rectangles which move across the screen. The first constraints for the (rectangles) are activated in my setupViews function which is called in my viewDidLoad.
In my viewDidLayoutSubviews I have an animation function which is called moving the two rectangles.
When I click a button however I would like the rectangles to return to their initial position and begin the animation from the beginning.
How could I successfully do this?
I initially did not have my animation being called in my viewDidLayoutSubviews, just in another function (which was being called in my viewDidAppear(_ animated: Bool)), however, upon reading it seems as if calling the animation in the viewDidLayoutSubviews might be the best course of action.
I can successfully stop the animation, but getting it to restart again from its initial starting point is proving to be troublesome.
I have tried creating the constraints again in the buttons sender function. But this doesn’t seem to do anything, as I’m not sure you can change an elements constraints without reloading the viewDidLoad again.
Below is my code………
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
func setupViews() {
self.view.addSubview(backgroundImageView)
self.backgroundImageView.addSubview(contentView)
self.backgroundImageView.addSubview(slideDownTintedWhiteView)
self.backgroundImageView.addSubview(restartSlidingAnimation)
self.contentView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
self.slideDownTintedWhiteView.snp.makeConstraints { (make) in
make.top.equalToSuperview()
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
make.bottom.equalTo(backgroundImageView.snp.top)
}
self.restartSlidingAnimation.snp.makeConstraints { (make) in
make.centerX.equalToSuperview()
make..centerY.equalToSuperview()
make.width.equalTo(10)
make.height.equalTo(10)
}
self.restartSlidingAnimation.addTarget(self, action: #selector(restartAnimationFunction(_:)), for: .touchUpInside)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
UIView.animate(withDuration: 10, delay: 0, animations: {
//Red View:
self.contentView.frame = CGRect(x: self.contentView.frame.origin.x, y: self.contentView.frame.height, width: self.contentView.frame.width, height: self.contentView.frame.height)
//White View:
self.slideDownTintedWhiteView.frame = CGRect(x: self.contentView.frame.origin.x, y: 0, width: self.contentView.frame.width, height: self.contentView.frame.height)
}, completion: nil)
}
restartAnimationFunction() {
// Here is obviously where I need to somehow re-establish the constraints of the rectangles back to their original location or somehow start the animation from the beginning.
}
func stopContentViewAnimation() {
// This is where I am able to stop the animation (called from another button)
pausedTime = self.contentView.layer.convertTime(CACurrentMediaTime(), from: nil)
self.contentView.layer.speed = 0.0
self.contentView.layer.timeOffset = pausedTime
pausedTime = self.slideDownTintedWhiteView.layer.convertTime(CACurrentMediaTime(), from: nil)
self.slideDownTintedWhiteView.layer.speed = 0.0
self.slideDownTintedWhiteView.layer.timeOffset = pausedTime
}
Thanks for any help you can provide.
1- First the animation should not be inside viewDidLayoutSubviews as it's being called many times , best place for it is viewDidAppear
2- You don't set any constraints to backgroundImageView
3- To animate a child remove it's constraints from itself and from it's parent
self.backgroundImageView.removeConstraints(self.backgroundImageView.constraints)
self.contentView.removeConstraints(self.contentView.constraints)
self.slideDownTintedWhiteView.removeConstraints(self.slideDownTintedWhiteView.constraints)
self.restartSlidingAnimation.removeConstraints(self.restartSlidingAnimation.constraints)
4- Re construct the other case constraints and when ready to animate put
self.view.layoutIfNeeded()
inside the animation block
OFF course I don't recommend removing all constraints and constructing them again , but you can have a reference to the constraints you want to change and alter their constants , but for a beginner this may be good to go
How can I animate a stack view to slide up starting from x=0 up to y=500, I have the following method in the viewDidLoad() which does a growing effect.
StackView.transform = CGAffineTransformMakeScale(0.0, 0.0)
And then I added a growing effect in the viewDidAppear() method
UIView.animateWithDuration(0.4, delay: 0.0, options: [], animations: {
self.StackView.transform = CGAffineTransformIdentity
}, completion: nil)
After the viewDidLoad method executes, the stack view is minimized. When the viewDidLoad method completes, the viewDidAppear method is invoked, and the animation begins and the stack view begins to grow. The animation stops when the stack view reaches it's original size.
Although is a nice effect that's not what I want to accomplish, I want the animation to slide up from x = 0 and stops at y = 500 I tried to add the following code in the viewDidLoad to accomplish this effect, but I still get the same growing effect. Any suggestions on how to accomplish this?
StackView.transform = CGAffineTransformMakeTranslation(0, 500)
You´re almost there just make a few changes
// These values depends on the positioning of your element
let left = CGAffineTransformMakeTranslation(-300, 0)
let right = CGAffineTransformMakeTranslation(300, 0)
let top = CGAffineTransformMakeTranslation(0, -300)
UIView.animateWithDuration(0.4, delay: 0.0, options: [], animations: {
// Add the transformation in this block
// self.container is your view that you want to animate
self.container.transform = top
}, completion: nil)
Inside my Primary View Controller I have a MKMapView and TableView. The tableview is using AlamoFire to call an API of user information to populate its cells.
What I would like to happen:
When you select a cell, a second container view, with more detailed user information, will segue into the portion of the screen where the tableview was. At the same time, the Map View will annotate with that user's location.
The problem:
When the cell is selected, the second container view slides onto the screen (good), the Map Annotates (good), but then the info container view disappears again (bad). With a second tap of the originally selected cell, the info container view slides back onto the screen to stay. I need to get rid of this double tap.
It appears that the Map Annotation is negating this process for some reason. When I comment out the map annotation, the VC transition works fine... with one tap.
Any thoughts would be helpful.
Here is the offending code:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.localTableView.deselectRowAtIndexPath(indexPath, animated: true)
dispatch_async(dispatch_get_main_queue(),{
self.populateInfoContainerView(indexPath.row)
self.mapView.selectAnnotation(self.users[indexPath.row], animated: true)
if(self.userInfoContainerView.frame.origin.x == UIScreen.mainScreen().bounds.size.width){
self.showInfoView(true)
} else {
self.hideInfoView(true)
}
})
}
func showInfoView(animated: Bool){
UIView.animateWithDuration(0.33, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 1.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: ({
self.userInfoContainerView.frame.origin.x = 0
self.localTableView.frame.origin.x = -UIScreen.mainScreen().bounds.size.width
}), completion: { finished in
//animation complete
})
self.infoVisible = true
}
func hideInfoView(animated: Bool){
let xPos = UIScreen.mainScreen().bounds.size.width
if(animated == true){
UIView.animateWithDuration(0.25, animations: ({
self.userInfoContainerView.frame.origin.x = xPos
self.localTableView.frame.origin.x = 0
}), completion: { finished in
//animation complete
})
} else {
self.userInfoContainerView.frame.origin.x = xPos
}
self.infoVisible = false
}
Thank you.
I believe I have found an answer. I'm not sure why this works and the above version does not, but I'll show you what I did different.
I am now animating the constraints of the TableView and the InfoView.
First, I ctrl+click+drag the constraint from my main.storyboard into my code and created an IBOutlet to animate. I also added a let screenWidth as the constant that I would move them by.
#IBOutlet weak var infoViewLeadingEdge: NSLayoutConstraint!
#IBOutlet weak var tableViewXCenter: NSLayoutConstraint!
let screenWidth = UIScreen.mainScreen().bounds.size.width
Then I replaced the code above in showInfo():
self.userInfoContainerView.frame.origin.x = 0
self.localTableView.frame.origin.x = -UIScreen.mainScreen().bounds.size.width
with this new code:
self.infoViewLeadingEdge.constant -= self.screenWidth
self.tableViewXCenter.constant += self.screenWidth
self.view.layoutIfNeeded()
In my hideInfo() above, I replaced this:
self.userInfoContainerView.frame.origin.x = xPos
self.localTableView.frame.origin.x = 0
with this new code:
self.infoViewLeadingEdge.constant += self.screenWidth
self.tableViewXCenter.constant -= self.screenWidth
self.view.layoutIfNeeded()
Don't forget the self.view.layoutIfNeeded(). If you don't use that code, the animations won't work. Also, the += and -= on the tableViewXCenter seem opposite of what I want the table to do, however, this is the way I needed to put them in order to get the table to move the direction I wanted. It did seem counterintuitive though.
And that's it. I hope this will be helpful to someone.
I am attempting to animate a tab bar to move from below the bottom of the screen to the top while simultaneously adjusting a view's height to shrink by the height of the tab bar. Essentially, I have a "hidden" tab bar that when it unhides should animate into view and the displayView should adjust for the space the tab bar now takes up.
However, the animation is jumpy for the display view. It seems that the display view animates fine, but the subviews automatically adjust their height without any animation. Any direction on fixing this would be appreciated.
I will accept aid in either objective-c or swift, as the translation is fairly easy.
//Displays tab bar with slide up animation. If animated is false, all other params are unused
func displayTabBar(animated:Bool, duration:NSTimeInterval = 0.5, delay:NSTimeInterval = 0, options:UIViewAnimationOptions = UIViewAnimationOptions.CurveLinear, completion:((Bool) -> Void)? = nil){
if(animated){
UIView.animateWithDuration(duration, delay: delay, options: options, animations: {
self.adjustTabBarDisplayed()
}, completion: completion)
UIView.animateWithDuration(duration, delay: delay, options: options, animations: {
self.adjustDisplayViewTabDisplayed()
}, completion: nil)
}
else{
self.adjustTabBarDisplayed()
self.adjustDisplayViewTabDisplayed()
}
}
//Adjusts frame of tab bar to display tab bar
private func adjustTabBarDisplayed(){
self.tabBar.frame = CGRectMake(0,UIScreen.mainScreen().bounds.height - self.tabBar.bounds.height, self.tabBar.bounds.width, self.tabBar.bounds.height)
}
//Adjusts frame of display view to match displayed tab bar
private func adjustDisplayViewTabDisplayed(){
self.displayView.frame = CGRectMake(0,0,self.displayView.bounds.width, UIScreen.mainScreen().bounds.height - self.tabBar.bounds.height)
}
When you modify a view's size, it doesn't lay out its subviews immediately. Instead, it sets a flag indicating that it needs layout. Later, after the system has finished dispatching the event that ended up calling displayTabBar, it runs the display refresh code. The display refresh code finds views that have the needs-layout flag set and tells them to lay themselves out (by sending them layoutSubviews).
Here, you are changing your display view's size inside an animation block. Therefore change to your display view's frame will be animated. But the frames of its subviews are changing outside the animation block; they're changing later during the layout phase. You need to make them change inside the animation block.
Lucky for you, that's easy. Just call self.displayView.layoutIfNeeded() inside the animation block. Also, you only need one animation block, since all of the animation parameters are identical:
func displayTabBar(animated:Bool, duration:NSTimeInterval = 0.5, delay:NSTimeInterval = 0, options:UIViewAnimationOptions = UIViewAnimationOptions.CurveLinear, completion:((Bool) -> Void)? = nil){
if(animated){
UIView.animateWithDuration(duration, delay: delay, options: options, animations: {
self.adjustTabBarDisplayed()
self.adjustDisplayViewTabDisplayed()
// ADD THIS LINE
self.displayView.layoutIfNeeded()
}, completion: completion)
}
else{
self.adjustTabBarDisplayed()
self.adjustDisplayViewTabDisplayed()
}
}
Use the below line of code in animation block
scrollView.layoutIfNeeded()