Swift Animate duration not working in CGAffineTransform - swift

When I am translating one view with an animation of 1 second it is not working, but when I am executing the transform.identity it works fine.
Here is my code:
func hideCarousel() {
UIView.animate(withDuration: 1, animations: {
self.carouselER.transform = CGAffineTransform(translationX: 0, y: 200)
})
}
func showCarousel() {
UIView.animate(withDuration: 1, animations: {
self.carouselER.transform = .identity
})
}

To solve this issue I forced the animation to run in the main thread. Every time that you have problems with the performance of your UI elements like your animations or updating your label texts, try forcing to run the UI change in the main thread.
DispatchQueue.main.async {
UIView.animate(withDuration: 1, animations: {
self.carouselER.transform = CGAffineTransform(translationX: 0, y: 200)
})
}
I have also faced that problem with one timer that updated a label, but in this issue I thought it was some kind of problem of the CGAffineTransform.

Related

Asynchronous Function with DispatchGroup

I'm trying to set an animation inside of Playgrounds and make a simple message pop up in the debugger after completion.
With this, the .notify is appearing at the beginning. Wouldn't it appear after the animate functions went through inside of DispatchQueue.main.async(group: animationGroup)?
Here is the code provided:
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
extension UIView {
static func animate(withDuration duration: TimeInterval, animations: #escaping () -> Void, group: DispatchGroup, completion: ((Bool) -> Void)?) {
}
}
let animationGroup = DispatchGroup()
// A red square
let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = UIColor.red
// A yellow box
let box = UIView(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
box.backgroundColor = UIColor.yellow
view.addSubview(box)
PlaygroundPage.current.liveView = view
DispatchQueue.main.async(group: animationGroup) {
UIView.animate(withDuration: 1, animations: {
// Move box to lower right corner
box.center = CGPoint(x: 150, y: 150)
}, completion: {
_ in
UIView.animate(withDuration: 2, animations: {
// Rotate box 45 degrees
box.transform = CGAffineTransform(rotationAngle: .pi/4)
}, completion: .none)
})
UIView.animate(withDuration: 4, animations: { () -> Void in
// Change background color to blue
view.backgroundColor = UIColor.blue
})
}
animationGroup.notify(queue: DispatchQueue.main) {
print("Animations Completed!")
}
The call to async(group:os:flags:execute:) API that you’re using is used when performing tasks that are, themselves, synchronous. E.g. let's say you were going to process a bunch of images in some slow, synchronous API, but wanted to run this in parallel, on background threads to avoid blocking the main thread. Then you’d use async(group:execute:) API:
let group = DispatchGroup()
for image in images {
DispatchQueue.global().async(group: group) {
self.process(image)
}
}
group.notify(queue: .main) {
// all done processing the images
}
In this case, though, the individual, block-based UIView animation API calls run asynchronously. So you can’t use this async(group:execute:) pattern. You have to manually enter before starting each asynchronous process and then leave inside the respective completion closures, matching them up one-to-one:
animationGroup.enter()
UIView.animate(withDuration: 1, animations: {
box.center = CGPoint(x: 150, y: 150) // Move box to lower right corner
}, completion: { _ in
UIView.animate(withDuration: 2, animations: {
box.transform = CGAffineTransform(rotationAngle: .pi / 4) // Rotate box 45 degrees
}, completion: { _ in
animationGroup.leave()
})
})
animationGroup.enter()
UIView.animate(withDuration: 4, animations: {
view.backgroundColor = .blue // Change background color to blue
}, completion: { _ in
animationGroup.leave()
})
animationGroup.notify(queue: .main) {
print("Animations Completed!")
}
First of all, to quote from the documentation:
notify(queue:work:)
Schedules a work item to be submitted to a queue when a group of previously submitted block objects have completed.
Basically, there are no "previously" submitted blocks, so your notify block is called right away. This is happening because you added work items to the group in DispatchQueue.main.async. You can check this by adding a print inside DispatchQueue.main.async(group: animationGroup) and another print right before you call the notify method. The second one will print first.
On top of that, calling an asynchronous function inside DispatchGroup.main.async is problematic because the work item will enter immediately, then you'll dispatch your async work to another thread, and the work item will leave right after, even though your async work isn't done.
To fix this, you have to call animationGroup.enter() when you start your asynchronous operation, and then call animationGroup.leave() when you're done. Additionally, you have to take your code out of DispatchGroup.main.async. If you make these changes, the notify block will execute when the enter/leave calls are balanced.

Why is my button being removed from its superview before it completes its animation?

I am trying to move an my button to a certain part of the screen [as a custom button class GameBlock] and then remove it from its superview. However, it is acting weird in the sense that it removes itself from the superview before it completes the animation and I have no clue why. May somebody please help me out? I appreciate it.
UIView.animate(withDuration: movementTime,
delay: 0.0,
options: [],
animations: {
movedBlock.center = gridCoord[hitBlock.x][hitBlock.y]
},
completion: nil)
if movedBlock.center == gridCoord[hitBlock.x][hitBlock.y] {
movedBlock.removeFromSuperview()
}
If I understand your problem, this should works. Your view will be removed after the animation. The completion closure will be called at the end of the animation.
UIView.animate(withDuration: movementTime,
delay: 0.0,
options: [],
animations: {
movedBlock.center = gridCoord[hitBlock.x][hitBlock.y]
},
completion: { _ in
if movedBlock.center == gridCoord[hitBlock.x][hitBlock.y] {
movedBlock.removeFromSuperview()
}
})

Swift contentOffset scroll

I am developing a small application for tvOS where I need to show a UITextView.
Unfortunately sometimes this text can't fit inside the screen, that's why I need a way to scroll to the bottom of it programmatically.
I've tried using the following code:
self.setContentOffset(offset, animated: true)
It's actually working as intended, except for the fact that I need to handle the animation speed, so that the user can actually read; at the moment it's too fast.
This is what I tried to do, but with little success:
let offset = CGPoint(x: 0, y: contentSize.height - bounds.size.height)
UIView.animate(withDuration: 4, animations: {
self.setContentOffset(offset, animated: false)
})
I need a way to keep the text in place and scroll it to show the missing lines.
I am using a UITextView inside an UIScrollView.
Thanks.
If anyone is looking for a solution, this is what I ended up using:
var textTopDistance = 100
//calculate height of the text not being displayed
textNotVisibleHeight = descriptionLabel.contentSize.height - scrollView.bounds.size.height
func scrollText() {
//start new thread
DispatchQueue.main.async() {
//animation
UIView.animate(withDuration: 6, delay: 2, options: UIViewAnimationOptions.curveLinear, animations: {
//set scrollView offset to move the text
self.scrollView.contentOffset.y = CGFloat(self.textNotVisibleHeight - self.textTopDistance)
}, completion: nil)
}
}
It's not perfect I know, but at least it's working as intended.

Overlapping UIView animation canceling the first completion block

I have two buttons to move views. When I press the first button it will move a view and the other will move it back.
The first animation has a completion block, but I want to prevent this block from executing when the user moves the view back before the completion is done.
I have tried using solutions given to other questions similar to this but it doesn't seem to stop the execution of the first completion block.
#IBOutlet func moveView(sender: AnyObject) {
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .AllowUserInteraction, animations: { /* animation */ }) { _ in /* completion */ }
}
#IBOutlet func moveBack(sender: AnyObject) {
UIView.animateWithDuration(0.0, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .BeginFromCurrentState, animations: { /* animation */ }) { _ in /* completion */ }
}
I also tried removing all the animations and the completion parameter will be false but the animation will just jump to the end and animate from there.
Any ideas is very much appreciated.
You may use boolean flag to achieve the desired functionality like as under:
var moveViewFlag = false
var moveBackFlag = false
#IBOutlet func moveView(sender: AnyObject) {
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .AllowUserInteraction, animations: { /* animation */ }) { _ in /* completion */
//inside completion block make the moveBackFlag to true
moveBackFlag = true
}
}
#IBOutlet func moveBack(sender: AnyObject) {
UIView.animateWithDuration(0.0, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .BeginFromCurrentState, animations: { /* animation */ }) { _ in /* completion */
//inside completion block make the moveBackFlag to true
moveViewFlag = true
if moveBackFlag{
//do whatever you want to do!
}
}
}
Hope this helps.
Than you should create a custom class and handle that inside the class to avoid cluttered flags.
After searching for a while I think I've found a solution. To get the completion block to return false, the animation has to be removed. But by doing so, the animation will jump to the "end state" so to prevent this I used:
object.layer.frame = object.layer.presentationLayer()!.frame
object.layer.removeAllAnimations()
From here I can start my new animation (moving back the object) without worrying about the completion block of the first animation. Information
I'm not sure if this is how it should be done or if there's a better way of doing this.

animateWithDuration Animates Once But Not Twice

I have this piece of animation in a switch statement that runs and does what it's supposed to. The animation is called successfully every twenty seconds inside a timer. However, the animation does not happen any time but the first time. In my code below you will see println statements that I used to try and highlight the problem. Each println statement prints but no animation happens. What is it that I'm missing?
func popAnimation(indexNumber: Int) {
let image = self.overlayImageView[indexNumber]
image.hidden = false
image.alpha = 0.0
UIView.animateWithDuration(0.75, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1.0, options: nil, animations: { () -> Void in
println("animation ran")
println(image.alpha)
image.image = UIImage(named: "lowCountOverlay")
image.alpha = 1.0
}, completion: { (Bool) -> Void in
UIView.animateWithDuration(0.75, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1.0, options: nil, animations: { () -> Void in
println("animation 2 ran")
println(image.alpha)
image.alpha = 0.0
}, completion: { (Bool) -> Void in
println("animation 3 ran")
image.hidden = true
})
})
}
UPDATE: If I remove image.hidden from the second completion block it works fine and animates repeatedly. Which is interesting because if it's not hidden it should be covering other interactive content in the view, but it's not. The other content is perfectly accessible in simulator. The UIImageView image is definitely the top layer in Storyboard.
I could be off, but it seems like you're running from an NSTimer which may or may not be in the main thread.
UI updates need to take place in the main thread, try wrapping the completion handler in a GCD dispatch directed to the main thread like the following and see if that helps? Maybe even force the whole thing to the main thread.
UIView.animateWithDuration(0.75, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1.0, options: nil, animations: { () -> Void in
// First animation
}, , completion: { (Bool) -> Void in
dispatch_async(dispatch_get_main_queue(),{
// second animation
})
})
I think you need to look for your bug elsewhere. You don't seem to be missing anything in the code—as I'm able to run the animation repeatedly without any issues. I've copied your code as-is to a separate project and the animation works every time.
Try replacing:
let image = self.overlayImageView[indexNumber]
image.hidden = false
image.alpha = 0.0
with
let image = self.overlayImageView[indexNumber]
image.superview.bringSubviewToFront(image)
image.hidden = false
image.alpha = 0.0