How do I leverage ReactiveX to execute async calls in sequence?
I.e., execute a second call after first one has finished.
More specifically, I'm working with RxSwift in iOS, and the asyncs I want to chain together are UIView animations (instead of calling the second animation inside the completion block of the first one).
I know I have other options like Easy Animation, but I'd like to leverage Rx, since I'm already using it for streams.
Also, one solution would be (for 3 chained animations):
_ = UIView.animate(duration: 0.2, animations: {
sender.transform = CGAffineTransformMakeScale(1.8, 1.8)
})
.flatMap({ _ in
return UIView.animate(duration: 0.2, animations: {
sender.transform = CGAffineTransformMakeScale(0.8, 0.8)
})
})
.flatMap({ _ in
return UIView.animate(duration: 0.2, animations: {
sender.transform = CGAffineTransformIdentity
})
})
.subscribeNext({ _ in })
But I'm looking for something more elegant, the right way of doing it with Rx.
I don't think using Rx makes it much cleaner, but here's how you could do it:
let animation1 = Observable<Void>.create { observer in
UIView.animateWithDuration(0.2,
animations: {
// do your animations
}, completion: { _ in
observer.onCompleted()
})
return NopDisposable.instance
}
let animation2 = Observable<Void>.create { observer in
UIView.animateWithDuration(0.2,
animations: {
// do your animations
}, completion: { _ in
observer.onCompleted()
})
return NopDisposable.instance
}
Observable.of(animation1, animation2)
.concat()
.subscribe()
.addDisposableTo(disposeBag)
It would also be cleaner if you create a function to construct the Observable<Void>s for you.
func animation(duration: NSTimeInterval, animations: () -> Void) -> Observable<Void> {
return Observable<Void>.create { observer in
UIView.animateWithDuration(duration,
animations: animations,
completion: { _ in
observer.onCompleted()
})
return NopDisposable.instance
}
I guess a plus side to using Rx instead of just animateWithDuration:animations: chained, is that you don't have to nest the animations in completion blocks. This way, you can just define them on their own and compose them as you want afterward.
As an alternative to RxSwift, check out PromiseKit. RxSwift is a bit overkill for your animation callback needs. This blog post in particular is relevant.
Maybe its too late for #Rodrigo Ruiz, but I'm sure it might help other developers that happen to come across this post (like I did).
RxAnimated is available for free on GitHub: https://github.com/RxSwiftCommunity/RxAnimated
You can get started using this blog post
A small discloser - I'm not connected to this project or post by any way - I just found it while searching for a reactive solution for my animations :)
Related
I'm a noob and have been learning from Apple's Playgrounds and random books doing tutorials. I'm working on a tutorial where it deals with a closure. I've seen this 'finish in' before in another tutorial but I don't know what it means precisely in layman terms.
What is it finishing, what is being finished, and inside of what? Or is there an idea of order of operation?
Here is the function where it was used:
func playSequence(index: Int, highlightTime: Double){
currentPlayer = .Computer
if index == inputs.count{
currentPlayer = .Human
return
}
var button: UIButton = buttonByColor(color: inputs[index])
var originalColor: UIColor? = button.backgroundColor
var highlightColor: UIColor = UIColor.white
UIView.animate(withDuration: highlightTime, delay: 0.0, options: [.curveLinear, .allowUserInteraction, .beginFromCurrentState], animations: {
button.backgroundColor = highlightColor
}, completion: {
finished in button.backgroundColor = originalColor
var newIndex: Int = index + 1
self.playSequence(index: newIndex, highlightTime: highlightTime)
})
}
finished is the parameter to the completion closure. The in is simply part of Swift's closure syntax.
The full signature of the UIView animate method is:
class func animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIViewAnimationOptions = [], animations: #escaping () -> Void, completion: ((Bool) -> Void)? = nil)
Note the Bool parameter to the completion closure. The finished in your code is the name given to that parameter.
An excerpt from the documentation about the completion parameter states:
This block has no return value and takes a single Boolean argument that indicates whether or not the animations actually finished before the completion handler was called.
A more typical way to write the code is as:
UIView.animate(withDuration: highlightTime, delay: 0.0, options: [.curveLinear, .allowUserInteraction, .beginFromCurrentState], animations: {
// animation code
}) { (finished) in
// completion code
}
This syntax makes it clearer than the syntax you are using. This is also using the "trailing closure" syntax.
Another way, closer to your usage, would be:
UIView.animate(withDuration: highlightTime, delay: 0.0, options: [.curveLinear, .allowUserInteraction, .beginFromCurrentState], animations: {
// animation code
}, completion: { (finished) in
// completion code
})
Your usage simply omits the parentheses around the parameter and it leaves out a line break. Adding those back in makes the code clearer.
I searched everywhere on the internet but couldn't really deal with the answers I found. So if someone could help me here, that'd be appreciated.
I wrote a function that looks like this:
func setImage(imageName: String, completion: ((String) -> Void)?) {
UIView.transitionWithView(self.myImageView, duration: 0.3, options: .CurveEaseOut, animations: {
self.lockImageView.image = UIImage(named: "\(imageName).png")
}, completion: { finished in
//execute the completionBlock that was passed
})
}
I call it like this:
setImage("lockCheck", completion: { finished in
print("done")
})
Now, how do I execute whatever was passed as completion?
In the function, in the transition's completion block, I tried something like
for x in completion {self.x}
but that didn't work.
Thanks in advance :)
You could for example execute the completion handler right away in the completionHandler of the animation block like so:
func setImage(imageName: String, completion: ((Bool) -> Void)?) {
UIView.transitionWithView(self.myImageView, duration: 0.3, options: .CurveEaseOut, animations: { () -> Void in
self.lockImageView.image = UIImage(named: "\(imageName).png")
}, completion: completion)
}
You can also run an completion handler with extra parameters like the following (I hope it is clear like this):
func setImage(imageName: String, completion: ((Bool, String) -> Void)?) {
UIView.transitionWithView(self.lockImageView, duration: 0.3, options: .CurveEaseOut, animations: { () -> Void in
self.lockImageView.image = UIImage(named: "\(imageName).png")
}) { (finished) -> Void in
// Do some things for example print
print("Hi, this is the animation completion handler")
// Notice the ? because the completion handler is an optional
completion?(finished, "some string")
}
}
I'm looking to animate a few things when a long process (parsing) is complete. My barebones code currently looks like such:
func run_on_background_thread(code: () -> Void)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), code)
}
func run_on_main_thread(code: () -> Void)
{
dispatch_async(dispatch_get_main_queue(), code)
}
func manage_response()
{
run_on_background_thread
{
long_process()
run_on_main_thread
{
UIView.animateWithDuration(animation_duration, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: UIViewAnimationOptions.CurveEaseInOut, animations:
{
some_view.transform = CGAffineTransformMakeScale(0, 0)
}, completion: nil)
}
}
}
Quite obviously, the animation does not occur. The UI just updates to the final stage. What is the proper way to thread this (I know animateWithDuration() dispatch's it's own thread, but I don't know how to hold this animation until the long process is complete.
Thanks.
I think U way can work. just change
some_view.transform = CGAffineTransformMakeScale(0, 0)
to :
some_view.transform = CGAffineTransformMakeScale(0.01, 0.01)
change the scale value close to zero; U can see the animation occur. if scale value == 0,there is no animation occur. U can try this.
I am trying to use animateWithDuration closure in Swift. I have declared the arguments in the closure as mentioned in the Apple Book for Swift. However, I am still getting an error.
Below is the code snippet:
if(!isRotating){
isRotating = true
var myImageTemp :UIImageView = self.myImage
UIView.animateWithDuration(0.5, delay: 1, options: UIViewAnimationCurve.EaseOut, animations:
{
() in myImageTemp.transform = CGAffineTransformMakeRotation(angle + M_PI_2)
},
completion:
{
(Bool finished) in self.pathAnimation() })
}
It gives me an error:
Could find an overload that accepts the supplied arguments.
And also it tells me:
Implicit use of self in closure.
Can anybody help me with this?
Just try:
UIView.animateWithDuration(0.2,
animations:
{
// your code.
},
completion:
{
(completed: Bool) in
// your code.
})
The (completed: Bool) in part indicates that the closure takes a Bool parameter labeled completed. If you are not interested in accessing the completed parameter, you can ignore it using an underscore.
UIView.animateWithDuration(0.2,
animations:
{
// your code.
},
completion:
{ _ in
// your code.
})
I'm having trouble making the blocks work on Swift. Here's an example that worked (without completion block):
UIView.animateWithDuration(0.07) {
self.someButton.alpha = 1
}
or alternatively without the trailing closure:
UIView.animateWithDuration(0.2, animations: {
self.someButton.alpha = 1
})
but once I try to add the completion block it just won't work:
UIView.animateWithDuration(0.2, animations: {
self.blurBg.alpha = 1
}, completion: {
self.blurBg.hidden = true
})
The autocomplete gives me completion: ((Bool) -> Void)? but not sure how to make it work. Also tried with trailing closure but got the same error:
! Could not find an overload for 'animateWithDuration that accepts the supplied arguments
Update for Swift 3 / 4:
// This is how I do regular animation blocks
UIView.animate(withDuration: 0.2) {
<#code#>
}
// Or with a completion block
UIView.animate(withDuration: 0.2, animations: {
<#code#>
}, completion: { _ in
<#code#>
})
I don't use the trailing closure for the completion block because I think it lacks clarity, but if you like it then you can see Trevor's answer below.
The completion parameter in animateWithDuration takes a block which takes one boolean parameter. In Swift, like in Obj-C blocks, you must specify the parameters that a closure takes:
UIView.animateWithDuration(0.2, animations: {
self.blurBg.alpha = 1
}, completion: {
(value: Bool) in
self.blurBg.hidden = true
})
The important part here is the (value: Bool) in. That tells the compiler that this closure takes a Bool labeled 'value' and returns Void.
For reference, if you wanted to write a closure that returned a Bool, the syntax would be
{(value: Bool) -> bool in
//your stuff
}
The completion is correct, the closure must accept a Bool parameter: (Bool) -> (). Try
UIView.animate(withDuration: 0.2, animations: {
self.blurBg.alpha = 1
}, completion: { finished in
self.blurBg.hidden = true
})
Underscore by itself alongside the in keyword will ignore the input
Swift 2
UIView.animateWithDuration(0.2, animations: {
self.blurBg.alpha = 1
}, completion: { _ in
self.blurBg.hidden = true
})
Swift 3, 4, 5
UIView.animate(withDuration: 0.2, animations: {
self.blurBg.alpha = 1
}, completion: { _ in
self.blurBg.isHidden = true
})
There is my solution above based on accepted answer above. It fades out a view and hiddes it once almost invisible.
Swift 2
func animateOut(view:UIView) {
UIView.animateWithDuration (0.25, delay: 0.0, options: UIViewAnimationOptions.CurveLinear ,animations: {
view.layer.opacity = 0.1
}, completion: { _ in
view.hidden = true
})
}
Swift 3, 4, 5
func animateOut(view: UIView) {
UIView.animate(withDuration: 0.25, delay: 0.0, options: UIView.AnimationOptions.curveLinear ,animations: {
view.layer.opacity = 0.1
}, completion: { _ in
view.isHidden = true
})
}
Here you go, this will compile
Swift 2
UIView.animateWithDuration(0.3, animations: {
self.blurBg.alpha = 1
}, completion: {(_) -> Void in
self.blurBg.hidden = true
})
Swift 3, 4, 5
UIView.animate(withDuration: 0.3, animations: {
self.blurBg.alpha = 1
}, completion: {(_) -> Void in
self.blurBg.isHidden = true
})
The reason I made the Bool area an underscore is because you not using that value, if you need it you can replace the (_) with (value : Bool)
Sometimes you want to throw this in a variable to animate in different ways depending on the situation. For that you need
let completionBlock : (Bool) -> () = { _ in
}
Or you could use the equally verbose:
let completionBlock = { (_:Bool) in
}
But in any case, you have have to indicate the Bool somewhere.
SWIFT 3.x + 4.x
I'd like to make an update and simplify the things.
Example below is implemented in any view it is hiding slowly and when it is completely transparent; removes it self from parent view
ok variable will always returns true with animation termination.
alpha = 1
UIView.animate(withDuration: 0.5, animations: {
self.alpha = 0
}) { (ok) in
print("Ended \(ok)")
self.removeFromSuperview()
}