EDIT: I figured out I need to resume the layer if its paused, but am not sure how to resume the layer when the view returns. viewDidLoad doesn't seem to trigger on segue
I have an app with an animation, sound, and a webView. The animation is triggered by a button and a segue to the webView is triggered by another button. If the animation is playing when the app segues to the webView, once the app segues back, I have it set so the animation restarts when the play button is tapped. This works fine
However, when the animation is paused and the view changes, upon returning to the main view, the image alpha and label alpha doesn't appear to animate... although the debugger says the alpha is changing appropriately, nothing appears on screen.
How can I get it to animate appropriately when play is tapped after returning from the WebView?
Here's the play button:
touch += 1
//define animated layers
imgLayer = firstImg.layer
labelLayer = firstLbl.layer
if touch == 1 {
print("touch = \(touch)")
self.firstImg.alpha = 0
self.firstLbl.text = ""
animateStart()
player.play()
self.playOut.setBackgroundImage(UIImage(named: "pause.png"), for: UIControlState.normal)
} else {
touch = 2
}
//toggle pause
if touch == 2 {
print("touch = \(touch)")
pause = !pause
if pause {
animatePause()
player.pause()
} else {
animateResume()
player.play()
}
}
}
and the animate start block:
func animateStart() {
print("animation started")
UIView.animate(withDuration: 4, animations: {
self.firstImg.alpha = 1
print("firstImg alpha = \(self.firstImg.alpha)")
self.firstImg.image = UIImage(named:"peter.JPG")
self.view.layoutIfNeeded()
}, completion: { finished in
if finished {
self.animateSecond()
}
})
}
and here's how I dismiss the VC on segue
func dismissVC() {
touch = 0
self.playOut.setBackgroundImage(UIImage(named: "playbtn.png"), for: UIControlState.normal)
player.stop()
initAudio()
self.dismiss(animated: true, completion: nil)
}
Related
I have a view (grey background) and another over it with a UITextField.
When I tap on the grey background, I want the view with the UITextField to disappear with an animation (move to top)
When I tap on the return key (virtual keyboard) the animation is smooth, but when I tap on the grey background, it disappears without animation. When I comment out the vueRecherche.isHidden = true in the completion, it's ok, but I want to hide it!
here is my function
private func fermeRecherche() {
if self.constraintVueRecherche_Top.constant == -5 {
return
}
self.txtRercherche.endEditing(true)
UIView.animate(withDuration: 0.3, animations: {
self.constraintVueRecherche_Top.constant = -5
self.vueFondGris.alpha = 0.0
self.view.layoutIfNeeded()
}, completion: { termine in
self.vueRecherche.isHidden = true
self.vueFondGris.isHidden = true
})
}
with this call, the animation is smooth :
func textFieldDidEndEditing(_ textField: UITextField) {
fermeRecherche()
}
but not with this one :
let tap=UITapGestureRecognizer(target: self, action: #selector(self.tapFondGris(_:)))
vueFondGris.addGestureRecognizer(tap)
func tapFondGris(_ sender: UITapGestureRecognizer) {
fermeRecherche()
}
any ideas?
Thanks
During the navigation controller push/pop animation, keyboard is darker then it is in its final state. And on animation end, this black background view just disappears. Light (white) keyboard style does not have this effect.
How can I get rid of this black background?
I already tried setting the window color to white and setting the navigation controller background to white.
VIDEO:
https://www.dropbox.com/s/z1grj821fj306th/Untitled.mov?dl=0
SCREENSHOT:
Option 1
The easiest solution is to just to disable the keyboard transparency by setting its background color to black or white (depending on if you have a light or dark keyboard):
myTextField.becomeFirstResponder()
guard UIDevice().userInterfaceIdiom == .phone else {
return;
}
//keyboard window should be there now, look for it:
var keyboardWindow: UIWindow?
for window in UIApplication.shared.windows.reversed() {
if String(describing: type(of: window)) == "UIRemoteKeyboardWindow" {
keyboardWindow = window
break
}
}
keyboardWindow?.rootViewController?.view.subviews.first?.backgroundColor = .black
Option 2
If you don't like this I have a hack that seems to work pretty well, at least in iOS 14. It results in the keyboard slide up animation occurring during the slide in push animation, instead of after. It relies on showing the keyboard right before pushing by adding a temporary text field.
Run this code whenever you want to push the VC:
guard UIDevice().userInterfaceIdiom == .phone else {
// fix doesn't apply to iPad, push or perform segue:
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
return;
}
//create text field with matching settings so keyboard will look the same as its destination
let tempTextField = UITextField.init()
tempTextField.keyboardType = .default
tempTextField.keyboardAppearance = .light
tempTextField.autocorrectionType = .default
view.addSubview(tempTextField)
//show keyboard, then right after push VC, then discard the text field
//make sure destination text field calls becomeFirstResponder() in its VC's viewDidLoad()
tempTextField.becomeFirstResponder()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0/60.0) {
// push or perform segue:
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
tempTextField.removeFromSuperview()
}
}
Option 3
I improved on option 2 by copying the push slide animation to the keyboard, essentially resulting in exactly what you asked. There is also the option to keep the sliding up animation too during the slide in, just give it a try.
Run this code on the pushing VC whenever you want to push:
guard UIDevice().userInterfaceIdiom == .phone else {
// fix doesn't apply to iPad, push or perform segue:
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
return;
}
//create text field with matching settings so keyboard will look the same as its destination
let tempTextField = UITextField.init()
tempTextField.keyboardType = .default
tempTextField.keyboardAppearance = .light
tempTextField.autocorrectionType = .default
view.addSubview(tempTextField)
//show keyboard, then push VC, then remove the text field (see bottom)
//make sure destination text field calls becomeFirstResponder() in its VC's viewDidLoad()
UIView.setAnimationsEnabled(false) //set to false to disable slide up animation or true to keep it
tempTextField.becomeFirstResponder()
UIView.setAnimationsEnabled(true)
//find keyboard window
var keyboardWindow: UIWindow?
for window in UIApplication.shared.windows.reversed() {
if String(describing: type(of: window)) == "UIRemoteKeyboardWindow" {
keyboardWindow = window
break
}
}
keyboardWindow?.rootViewController?.view.isHidden = true //this prevents glitches
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0/60.0) {
// push or perform segue
self.performSegue(withIdentifier: "mySegue", sender: self)
// navigationController?.pushViewController(destinationVC, animated: true)
keyboardWindow?.rootViewController?.view.isHidden = false
//this spring animation is identical to default push slide animation
let spring = CASpringAnimation(keyPath: "position")
spring.damping = 500
spring.mass = 3
spring.initialVelocity = 0
spring.stiffness = 1000
spring.fromValue = CGPoint.init(x: self.view.frame.width, y:0) //you can enter e.g y:1000 to delay slide up animation
spring.toValue = CGPoint.init(x: 0, y:0)
spring.duration = 0.5
spring.isAdditive = true
keyboardWindow?.layer.add(spring, forKey: nil)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
tempTextField.removeFromSuperview()
}
You can keep the sliding up animation by not disabling the animations (above becomeFirstResponder()). If you choose this it is possible to delay the slide up animation by replacing y:0 with y:1000 for example. You can play with this value.
I'm building a Video player. I added a controlsContainer UIView (that contains custom playback controls) on top of a UIView I used for my AVPlayerLayer. How do I make this controlsContainer view to always hide after few seconds of appearing and only reappear when an area of the AVPlayerLayer is tapped just like the YouTube iOS App's video player?
Add touch events to AVplayerLayer and when the user touches the player unhide it for say 5 secs and after 5 secs hide it.If you want you can add the animation for hiding and unhiding.
or
If you are using avplayerviewcontroller directly you can simply say
showPlayBackControls=true;
Assuming your container view is on top of your video layer add a tap gesture recognizer to your view and keep track of whether are not your controls are showing. When the state changes animate the change. Here I just set all the subviews alpha to do a fade in/out but you can just as easily set the transform to have a slide in out instead. When the control appears on screen you set a count down timer and have the timer turn the controls off. You could also keep a reference to this timer and then cancel it and reschedule it each time the user interacts with the ui, which is probably better than what I have here, but you get the idea.
class ViewController: UIViewController {
weak var controlView: UIView!
var isHidingControls = false
override func viewDidLoad() {
super.viewDidLoad()
controlView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapControls)))
}
func tapControls() {
isHidingControls = !isHidingControls
animateControls()
}
func animateControls() {
if isHidingControls {
UIView.animate(withDuration: 0.25, animations: {
self.controlView.subviews.forEach {$0.alpha = 0}
}, completion: { _ in
self.controlView.subviews.forEach {$0.isHidden = true}
})
} else {
self.controlView.subviews.forEach {$0.isHidden = true}
UIView.animate(withDuration: 0.25, animations: {
self.controlView.subviews.forEach {$0.alpha = 1}
}, completion: { _ in
Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { [weak self] _ in
guard self?.isHidingControls == false else {
return
}
self?.isHidingControls = true
self?.animateControls()
}
})
}
}
}
Referring to my last question:
Sprite moves two places after being paused and then unpaused
Hi, I have a tap gesture which moves a sprite in my game forward 1 space and when I press the pause button it continues to register the tap gesture and then when I resume the gameplay It moves two spaces.
so I managed to define a bool variable that detects (using if statements) if I have paused the tap gesture
var tapIsPaused: Bool = false
func tapUp(){
if(tapIsPaused == true) {
//do nothing
} else if (tapIsPaused == false) {
let amountToMove:CGFloat = levelUnitHeight
let move:SKAction = SKAction.moveByX(0, y: amountToMove, duration: 0.1)
menubutton.hidden = true
settingsButton.hidden = true
highscoreLabel.hidden = true
pauseButton.hidden = false
thePlayer.runAction(move)
clearNodes()
}
}
But the problem I have now is that when I press the resume button to resume the gameplay it still moves the sprite, but this time it's only moving one space up, which is because when I press the resume button it turns the tap on which then registers the tap of the resume button to move the player up.
How can I fix this?
Here is my pause button:
else if (node == pauseButton) {
tapIsPaused = true
pauseButton.removeFromParent()
addChild(resumeButton)
addChild(restartButton)
self.runAction (SKAction.runBlock(self.pauseGame))
}
Here is my resume button:
else if (node == resumeButton) {
resumeButton.removeFromParent()
restartButton.removeFromParent()
addChild(pauseButton)
self.runAction (SKAction.runBlock(self.resumeGame))
tapIsPaused = false
}
Here is my tap gesture handler code:
let TapUpRec = UITapGestureRecognizer()
TapUpRec.addTarget(self, action: "tapUp")
self.view!.addGestureRecognizer(TapUpRec)
You can remove Gesture on Pause click using following:
self.view.removeGestureRecognizer(YOUR_GESTURE_RECOGNISER)
and add it again if resume game
Modify your resume function as:
else if (node == resumeButton) {
resumeButton.removeFromParent()
restartButton.removeFromParent()
addChild(pauseButton)
tapIsPaused = false
self.runAction (SKAction.runBlock(self.resumeGame))
}
Very simple and easiest way.No need to add or remove Gesture.
You can do it with enable or disable your gesture.
For swift 2.3
TapUpRec.enabled = false //pause click
TapUpRec.enabled = true //resume click
For swift 3.0
TapUpRec.isEnabled = false //pause click
TapUpRec.isEnabled = true //resume click
I have created an interactive transition. My func animateTransition(transitionContext: UIViewControllerContextTransitioning) is quite normal, I get the container UIView, I add the two UIViewControllers and then I do the animation changes in a UIView.animateWithDuration(duration, animations, completion).
I add a UIScreenEdgePanGestureRecognizer to my from UIViewController. It works well except when I do a very quick pan.
In that last scenario, the app is not responsive, still on the same UIViewController (the transition seems not to have worked) but the background tasks run. When I run the Debug View Hierarchy, I see the new UIViewController instead of the previous one, and the previous one (at least its UIView) stands where it is supposed to stand at the end of the transition.
I did some print out and check points and from that I can say that when the problem occurs, the animation's completion (the one in my animateTransition method) is not reached, so I cannot call the transitionContext.completeTransition method to complete or not the transition.
I could see as well that the pan goes sometimes from UIGestureRecognizerState.Began straight to UIGestureRecognizerState.Ended without going through UIGestureRecognizerState.Changed.
When it goes through UIGestureRecognizerState.Changed, both the translation and the velocity stay the same for every UIGestureRecognizerState.Changed states.
EDIT :
Here is the code:
animateTransition method
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
let containerView = transitionContext.containerView()
let screens: (from: UIViewController, to: UIViewController) = (transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!, transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!)
let parentViewController = presenting ? screens.from : screens.to
let childViewController = presenting ? screens.to : screens.from
let parentView = parentViewController.view
let childView = childViewController.view
// positionning the "to" viewController's view for the animation
if presenting {
offStageChildViewController(childView)
}
containerView.addSubview(parentView)
containerView.addSubview(childView)
let duration = transitionDuration(transitionContext)
UIView.animateWithDuration(duration, animations: {
if self.presenting {
self.onStageViewController(childView)
self.offStageParentViewController(parentView)
} else {
self.onStageViewController(parentView)
self.offStageChildViewController(childView)
}}, completion: { finished in
if transitionContext.transitionWasCancelled() {
transitionContext.completeTransition(false)
} else {
transitionContext.completeTransition(true)
}
})
}
Gesture and gesture handler:
weak var fromViewController: UIViewController! {
didSet {
let screenEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: "presentingViewController:")
screenEdgePanRecognizer.edges = edge
fromViewController.view.addGestureRecognizer(screenEdgePanRecognizer)
}
}
func presentingViewController(pan: UIPanGestureRecognizer) {
let percentage = getPercentage(pan)
switch pan.state {
case UIGestureRecognizerState.Began:
interactive = true
presentViewController(pan)
case UIGestureRecognizerState.Changed:
updateInteractiveTransition(percentage)
case UIGestureRecognizerState.Ended:
interactive = false
if finishPresenting(pan, percentage: percentage) {
finishInteractiveTransition()
} else {
cancelInteractiveTransition()
}
default:
break
}
}
Any idea what might happen?
EDIT 2:
Here are the undisclosed methods:
override func getPercentage(pan: UIPanGestureRecognizer) -> CGFloat {
let translation = pan.translationInView(pan.view!)
return abs(translation.x / pan.view!.bounds.width)
}
override func onStageViewController(view: UIView) {
view.transform = CGAffineTransformIdentity
}
override func offStageParentViewController(view: UIView) {
view.transform = CGAffineTransformMakeTranslation(-view.bounds.width / 2, 0)
}
override func offStageChildViewController(view: UIView) {
view.transform = CGAffineTransformMakeTranslation(view.bounds.width, 0)
}
override func presentViewController(pan: UIPanGestureRecognizer) {
let location = pan.locationInView((fromViewController as! MainViewController).tableView)
let indexPath = (fromViewController as! MainViewController).tableView.indexPathForRowAtPoint(location)
if indexPath == nil {
pan.state = .Failed
return
}
fromViewController.performSegueWithIdentifier("chartSegue", sender: pan)
}
I remove the "over" adding lines => didn't fix it
I added updateInteractiveTransition in .Began, in .Ended, in both => didn't fix it
I turned on shouldRasterize on the layer of the view of my toViewController and let it on all the time => didn't fix it
But the question is why, when doing a fast interactive gesture, is it not responding quickly enough
It actually works with a fast interactive gesture as long as I leave my finger long enough. For example, if I pan very fast on more than (let say) 1cm, it's ok. It's not ok if I pan very fast on a small surface (let say again) less than 1cm
Possible candidates include the views being animated are too complicated (or have complicated effects like shading)
I thought about a complicated view as well but I don't think my view is really complicated. There are a bunch of buttons and labels, a custom UIControl acting as a segmented segment, a chart (that is loaded once the controller appeared) and a xib is loaded inside the viewController.
Ok I just created a project with the MINIMUM classes and objects in order to trigger the problem. So to trigger it, you just do a fast and brief swipe from the right to the left.
What I noticed is that it works pretty easily the first time but if you drag the view controller normally the first time, then it get much harder to trigger it (even impossible?). While in my full project, it doesn't really matter.
When I was diagnosing this problem, I noticed that the gesture's change and ended state events were taking place before animateTransition even ran. So the animation was canceled/finished before it even started!
I tried using GCD animation synchronization queue to ensure that the updating of the UIPercentDrivenInterativeTransition doesn't happen until after `animate:
private let animationSynchronizationQueue = dispatch_queue_create("com.domain.app.animationsynchronization", DISPATCH_QUEUE_SERIAL)
I then had a utility method to use this queue:
func dispatchToMainFromSynchronizationQueue(block: dispatch_block_t) {
dispatch_async(animationSynchronizationQueue) {
dispatch_sync(dispatch_get_main_queue(), block)
}
}
And then my gesture handler made sure that changes and ended states were routed through that queue:
func handlePan(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .Began:
dispatch_suspend(animationSynchronizationQueue)
fromViewController.performSegueWithIdentifier("segueID", sender: gesture)
case .Changed:
dispatchToMainFromSynchronizationQueue() {
self.updateInteractiveTransition(percentage)
}
case .Ended:
dispatchToMainFromSynchronizationQueue() {
if isOkToFinish {
self.finishInteractiveTransition()
} else {
self.cancelInteractiveTransition()
}
}
default:
break
}
}
So, I have the gesture recognizer's .Began state suspend that queue, and I have the animation controller resume that queue in animationTransition (ensuring that the queue starts again only after that method runs before the gesture proceeds to try to update the UIPercentDrivenInteractiveTransition object.
Have the same issue, tried to use serialQueue.suspend()/resume(), does not work.
This issue is because when pan gesture is too fast, end state is earlier than animateTransition starts, then context.completeTransition can not get run, the whole animation is messed up.
My solution is forcing to run context.completeTransition when this situation happened.
For example, I have two classes:
class SwipeInteractor: UIPercentDrivenInteractiveTransition {
var interactionInProgress = false
...
}
class AnimationController: UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
if !swipeInteractor.interactionInProgress {
DispatchQueue.main.asyncAfter(deadline: .now()+transitionDuration) {
if context.transitionWasCancelled {
toView?.removeFromSuperview()
} else {
fromView?.removeFromSuperview()
}
context.completeTransition(!context.transitionWasCancelled)
}
}
...
}
...
}
interactionInProgress is set to true when gesture began, set to false when gesture ends.
I had a similar problem, but with programmatic animation triggers not triggering the animation completion block. My solution was like Sam's, except instead of dispatching after a small delay, manually call finish on the UIPercentDrivenInteractiveTransition instance.
class SwipeInteractor: UIPercentDrivenInteractiveTransition {
var interactionInProgress = false
...
}
class AnimationController: UIViewControllerAnimatedTransitioning {
private var swipeInteractor: SwipeInteractor
..
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
...
if !swipeInteractor.interactionInProgress {
swipeInteractor.finish()
}
...
UIView.animateWithDuration(...)
}
}