Asynchronous Function with DispatchGroup - swift

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.

Related

How to define functionality to be executed after a ui transformation

I have an element I need to animate and I followed this question to transform it as follows.
I defined a button that when I click it, the view would move horizontally. However, I need to define certain functions after the transformation (For example, I would need the print statement to be working after the transformation is completed in 5 seconds). But currently, it is printed simultaneously.
How can I define functionality to be done after the transformation.
#IBAction func mybutton(_ sender: Any) {
UIView.animate(withDuration: 5.0) {
self.hhview.transform = CGAffineTransform(translationX: -160, y: 0)
print("Voila!")
}
}
You can use a completion handler to execute code after the transformation:
UIView.animate(withDuration: 5.0) {
self.hhview.transform = CGAffineTransform(translationX: -160, y: 0)
} completion: { _ in
print("Completed")
}

View animating from the incorrect position

I am trying to animate a cover view in my code. Essentially the cover slides up (closes) from off-screen to on-screen, and after the user presses a button the cover slides down (opens up) all the way off-screen.
My code to slide the cover view up the screen and into view is working perfectly. However, the code to slide the cover view down the screen and out of view is not working! The starting position for the cover sliding down is what seems to be incorrect. The cover slides down as expected, but starts far too high, such that the animation ends where it should start.
I've got a screen recording of the error in action (obviously slowed the animation down so you can see what I mean by the error). The cover sliding down should be sliding down from its original position where it slid up to.
Link to recording.
Can anyone spot where I'm going wrong in my code?
func showScoresCover() {
verticalDistance = ScoresCoverView.frame.size.height
self.ScoresCoverView.frame.origin.y += verticalDistance
UIView.animate(
withDuration: 0.5,
delay: 0.25,
options: .curveEaseOut,
animations:
{ self.ScoresCoverView.frame.origin.y -= self.verticalDistance},
completion:
{ finished in
print("Score cover closed.")
})
}
func hideScoresCover() {
verticalDistance = ScoresCoverView.frame.size.height
UIView.animate(
withDuration: 0.5,
delay: 0.25,
options: .curveEaseIn,
animations:
{ self.ScoresCoverView.frame.origin.y += self.verticalDistance},
completion:
{ finished in
print("Score cover opened.")
})
}
EDIT
hey #Nickolans thanks for your help on this issue. So... I have a bizarre update for you on this query. I implemented your code, and it works if I have a pre-programmed button which fires the showScoreCover and hideScoresCover. Turns out even my original code works, but again, only if I have a pre-programmed button which fires the show/hide functions.
However, I need to fire the function using a button where I programmatically reconfigure what the button does. How I've implemented this is through .addTarget and a number of selectors, and those selectors will differ based on where in the game the users are.
Anyway, this is what my code looks like:
self.continueButton.addTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
self.continueButton.addTarget(self, action: #selector(self.startTeamB), for: UIControlEvents.touchUpInside)
Originally I only used one .addTarget and included the hideScoresCover() in the startTeamB function. This was resulting in the original issue where the cover doesn't hide correctly (starts too high on the screen). So I split it up into the above two .addTargets, and that also results in the same issue. HOWEVER, if I comment out the entire .addTarget line above related to the startTeamB selector and leave the selector related to hideScoresCover, then the cover is shown and is hidden absolutely as I want to to be, the animation is perfect. This means the underlying code in hideScoresCover() is working fine, but that the issue is arising somehow in how hideScoresCover is being run when the continue button is pressed.
Do you have any idea why this would be? somehow that continueButton target is firing incorrectly whenever there are two addTargets, or when there is only one target but hideScoresCover() is included in startTeamB().
I'm stumped as to why this would be the case. startTeamB() is not a complex function at all, so shouldn't be interfering with hideScoresCover(). So this leads me only to conclude it must be the way the continueButton fires hideScoresCover() when its either a separate target in addition to startTeamB being a target, or when hideScoresCover() is included in startTeamB itself. Unusual that it does work correctly when the only .addTarget is a selector calling hidesScoresCover?
#objc func startTeamB() {
self.UpdateTeam() //Just changes the team name
self.TeamBTimer() //Starts a timer to countdown teamB's remaining time
self.TeamAIndicator.isHidden = true //Just hides teamA image
self.TeamBIndicator.isHidden = false //Just shows teamB image
print("Hintify: ","-----Start Team B.")
}
I took it into Xcode and set it up myself. I believe the issue may be how you're changing your Y value, instead of changing the origin change the view layers y position.
The following code works so you can just take it! Make sure to change the frame to whatever you need it to be when you first start.
class ViewController: UIViewController {
//Bool to keep track if view is open
var isOpen = false
var verticalDistance: CGFloat = 0
var ScoresCoverView = UIView()
var button = UIButton()
override func viewDidLoad() {
//Setup Score view
ScoresCoverView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.width)
ScoresCoverView.backgroundColor = .blue
view.addSubview(ScoresCoverView)
//Set vertical distance
verticalDistance = ScoresCoverView.frame.height
//TEST button to test view animations
button.frame = CGRect(x: 200, y: 500, width: 100, height: 100)
button.backgroundColor = .purple
button.addTarget(self, action: #selector(hasBeenFired), for: .touchUpInside)
self.view.addSubview(button)
}
//When button pressed
#objc func hasBeenFired() {
if isOpen {
self.hideScoresCover()
self.isOpen = false
} else {
self.showScoresCover()
self.isOpen = true
}
}
//Show
func showScoresCover() {
self.ScoresCoverView.isHidden = false
UIView.animate(withDuration: 0.5, delay: 0.25, options: .curveEaseOut, animations: {
self.ScoresCoverView.layer.position.y -= self.verticalDistance
}) { (finished) in
print("Score cover closed.")
}
}
//Hide
func hideScoresCover() {
UIView.animate(withDuration: 0.5, delay: 0.25, options: .curveEaseIn, animations: {
self.ScoresCoverView.layer.position.y += self.verticalDistance
}) { (finished) in
self.ScoresCoverView.isHidden = true
print("Score cover Opened.")
}
}
}
EDIT
If you'd like to change the functionality of the button, make sure to remove the previous target before adding a new one.
For example, if you want to switch from show/hide to teamB remove show/hide and add teamB-- and vice versa.
When you first start, add your initial target:
self.continueButton.addTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
Switch to startTeamB:
self.continueButton.removeTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
self.continueButton.addTarget(self, action: #selector(self.startTeamB), for: UIControlEvents.touchUpInside)
Switch to hideScoreCover:
self.continueButton.removeTarget(self, action: #selector(self.startTeamB), for: UIControlEvents.touchUpInside)
self.continueButton.addTarget(self, action: #selector(self.hideScoresCover), for: UIControlEvents.touchUpInside)
I just want to say one thing, maybe the += is not necessary here.
func showScoresCover() {
verticalDistance = ScoresCoverView.frame.size.height
self.ScoresCoverView.frame.origin.y += verticalDistance
UIView.animate(
withDuration: 0.5,
delay: 0.25,
options: .curveEaseOut,
animations:
{ self.ScoresCoverView.frame.origin.y -= self.verticalDistance},
completion:
{ finished in
print("Score cover closed.")
})
}
should be
`self.ScoresCoverView.frame.origin.y = verticalDistance`

Swift Animate duration not working in CGAffineTransform

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.

Invoke repeatable task when viewDidApper

I might be heading totally wrong direction, but what I'm trying to achieve is to load some view first and invoke some methods that will be running afterwards infinitely, while at the same time giving the User the possibility to interact with this view.
My first guess was to use viewDidApper with UIView.animate, as per below;
class MainView: UIView {
...
func animateTheme() {
//want this to print out infinitely
print("Test")
}
}
class ViewController: UIViewController {
#IBOutlet weak var mainView: MainView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 1.0, delay: 0.0, options: .repeat, animations: {
self.mainView.animateTheme()
}, completion: nil)
}
}
The case is that I can only see a single "Test" printed out but this has to be invoked infinitely. Also, as I mentioned the User needs to have the possibility to interact with the view once everything is loaded.
Thanks a lot for any of your help.
ViewDidAppear will indeed be called latest when the view is loaded, but I don´t think the user can or will have the time to interact before it´s loaded.
If you want to load animations after everything is loaded you could create a timer and wait x seconds before you do that. Below example will be called 10 seconds after initiation.
var timer = Timer()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
timer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(animateSomething), userInfo: nil, repeats: false)
}
func animateSomething() {
// Animate here
}
Update
To make an infinite loop you can use the following snippet:
UIView.animate(withDuration: 5.0, delay: 0, options: [.repeat, .autoreverse], animations: {
self.view.frame = CGRect(x: 100, y: 200, width: 200, height: 200)
}, completion: nil)
I've done something very similar on one of my ViewControllers that loads a UIWebView. While the webpage is loading I show a UILabel with text "Loading" and every 1.5 seconds I add a period to the end of the text. I think the approach you describe above and mine shown below have the same effect and neither would be considered an infinite loop.
private func showLoadingMessage(){
let label = loadingLabel ?? UILabel(frame: CGRect.zero)
label.text = "Loading"
view.addSubview(label)
loadingLabel = label
addEllipsis()
}
private func addEllipsis() {
if webpageHasLoaded { return }
guard let labelText = loadingLabel?.text else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
self.loadingLabel?.text = labelText + "."
self.addEllipsis()
}
}

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