How to create a closure inside a closure? [duplicate] - ios

This question already has answers here:
Calling Swift Closure Inside Closure
(4 answers)
Closed 4 years ago.
I am trying to create a function in Swift so I can make a repeating animation (I know you can use .repeat, but I do not want to use that). In my completion closure, I am getting an error. Here is my code so far:
import UIKit
var withDurationVar:TimeInterval = 0
var optionsVar:UIViewAnimationOptions?
var iterateVar = 0
var animationsVar:(() -> ()?)?
var completionVar:(() -> ()?)?
var numberOfIterations = 0
func repeatingAnimation(withDuration:TimeInterval, options:UIViewAnimationOptions, iterate:Int, animations:#escaping () -> (), completion:#escaping () -> ()) {
withDurationVar = withDuration
optionsVar = options
iterateVar = iterate
animationsVar = animations
completionVar = completion
}
func animationRepeat() {
UIView.animate(withDuration: withDurationVar, delay: 0, options: optionsVar!, animations: animationsVar as! () -> Void, completion: { (Finished) in
// Increase number of iterations
numberOfIterations += 1
// If it has not yet done every iteration needed
if numberOfIterations != iterateVar {
// Repeat animation
animationRepeat()
}
else {
completionVar // Where I get an error. 'Expression resolves to an unused I-value'
}
})
}
I can do this however:
func animationRepeat() {
UIView.animate(withDuration: withDurationVar, delay: 0, options: optionsVar!, animations: animationsVar as! () -> Void, completion: completionVar)
}
So how can I have the completion from my repeatingAnimation function into the completion of animationRepeat with the rest of the code as well? Thanks!

I have updated my code if anyone want to use it as a repeating animation function :)
import UIKit
// Make some variables to be accessed across the file
var withDurationVar:TimeInterval = 0
var optionsVar:UIViewAnimationOptions?
var iterateVar = 0
var animationsVar:(() -> ()?)?
var completionVar:(() -> ()?)?
var numberOfIterations = 0
// The function to call
func repeatingAnimation(withDuration:TimeInterval, options:UIViewAnimationOptions, iterate:Int, animations:#escaping () -> (), completion:#escaping () -> ()) {
// Set the global variables from the parameters
withDurationVar = withDuration
optionsVar = options
iterateVar = iterate
animationsVar = animations
completionVar = completion
// Has not started repeating yet
numberOfIterations = 0
// Start repeat
animationRepeatForFunction()
}
// The function which is ONLY called by the repeatingAnimation or itself, not the user
func animationRepeatForFunction() {
UIView.animate(withDuration: withDurationVar, delay: 0, options: optionsVar!, animations: {animationsVar?()}, completion: { (Finished) in
// Increase number of iterations
numberOfIterations += 1
// If it has not yet done every iteration needed
if numberOfIterations < iterateVar {
// Repeat animation
animationRepeatForFunction()
}
else {
// Perform what the user wanted to do after the repeating animation
completionVar?()
}
})
}
Call the function using repeatingAnimation(...)
The .repeat function can only really be used for infinite loops, which do not need to be stopped. In my case, I wanted to repeat a finite amount of times (terminates), so I made this custom function which can have its own .swift file.

Related

is it possible to combine two animations in Lottie framwork

swift version is 5 and lottie version is 3.1.1
I want to show two animations Json file with Lottie, that way fade in first animation and after it done it fades out and the another one fades in and I have to take a loop and do it on an infinity loop.
boardAnimationViewForSecondSlide = AnimationView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.width * (690 / 750)))
boardAnimationViewForSecondSlide?.animation = Animation.named("Slidetwop1")
slide.addSubview(boardAnimationViewForSecondSlide)
and I define a closure for handle completion play's method
private var animationState: Int = 0 // 0 first slid, 1 second slide
private var changeStateInSlide2: (Bool) -> Void = { finish in
if animationState == 0 {
boardAnimationViewForSecondSlide.animation = Animation.named("Slidetwop2.json")
playSecondPage = true
} else {
boardAnimationViewForSecondSlide.animation = Animation.named("Slidetwop1.json")
playSecondPage = true
}
}
fileprivate var playSecondPage: Bool {
get {
return false
}
set {
if newValue {
boardAnimationViewForSecondSlide.play(completion:changeStateInSlide2)
}
}
}
I think the moste simple is to create a func to launch the animation.
example :
/// Start animation with Lottie
func startAnimation(viewName: AnimationView, jsonName: String) {
viewName.isHidden = false
viewName.animation = Animation.named(jsonName)
viewName.play { (_) in
viewName.isHidden = true
}
After this you can simply call the method one after one :
startAnimation(viewName: checkAnimation, jsonName: "Slidetwop1")
startAnimation(viewName: checkAnimation, jsonName: "Slidetwop2")
Or use the completion handler to call the second.
EDIT: For the loop you can use this solution:
/// Start animation with Lottie
func startAnimation() {
animationLottieView.animation = Animation.named("Slidetwop1")
animationLottieView.play { (finished) in
// completion handler
self.animationLottieView.animation = Animation.named("Slidetwop2")
self.animationLottieView.play { (finishedAnimation) in
self.startAnimation()}
}
}

iOS (Swift): Changing UILabel text with a for loop

I have a for loop as follows:
#objc private func resetValue() {
for i in stride(from: value, to: origValue, by: (value > origValue) ? -1 : 1) {
value = i
}
value = origValue
}
And when value is set it updates a label:
private var value = 1 {
didSet {
updateLabelText()
}
}
private func updateLabelText() {
guard let text = label.text else { return }
if let oldValue = Int(text) { // is of type int?
let options: UIViewAnimationOptions = (value > oldValue) ? .transitionFlipFromTop : .transitionFlipFromBottom
UIView.transition(with: label, duration: 0.5, options: options, animations: { self.label.text = "\(value)" }, completion: nil)
} else {
label.text = "\(value)"
}
}
I was hoping that if value=5 and origValue=2, then the label would flip through the numbers 5,4,3,2. However, this is not happening - any suggestions why, please?
I've tried using a delay function:
func delay(_ delay:Double, closure: #escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}
and then placing the following within the stride code:
delay(2.0) { self.value = i }
However, this doesn't seem to work either.
Thanks for any help offered.
UIKit won't be able to update the label until your code is finished with the main thread, after the loop completes. Even if UIKit could update the label after each iteration of the loop, the loop is going to complete in a fraction of a second.
The result is that you only see the final value.
When you attempted to introduce the delay, you dispatched the update to the label asynchronously after 0.5 second; Because it is asynchronous, the loop doesn't wait for the 0.5 second before it continues with the next iteration. This means that all of the delayed updates will execute after 0.5 seconds but immediately one after the other, not 0.5 seconds apart. Again, the result is you only see the final value as the other values are set too briefly to be visible.
You can achieve what you want using a Timer:
func count(fromValue: Int, toValue: Int) {
let stride = fromValue > toValue ? -1 : 1
self.value = fromValue
let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats:true) { [weak self] (timer) in
guard let strongSelf = self else {
return
}
strongSelf.value += stride
if strongSelf.value == toValue {
timer.invalidate()
}
}
}
I would also update the didSet to send the oldValue to your updateLabelText rather than having to try and parse the current text.
private var value = 1 {
didSet {
updateLabelText(oldValue: oldValue, value: value)
}
}
private func updateLabelText(oldValue: Int, value: Int) {
guard oldValue != value else {
self.label.text = "\(value)"
return
}
let options: UIViewAnimationOptions = (value > oldValue) ? .transitionFlipFromTop : .transitionFlipFromBottom
UIView.transition(with: label, duration: 0.5, options: options, animations: { self.label.text = "\(value)" }, completion: nil)
}

For loop in swift seems to randomly choose increment

I have a for loop that executes animations and then removes them on completion, and I am trying to call another method upon completion. Here is my code:
func animateMatch(completion: () -> ()) {
var movingShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
let destinationShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
for everyShape in 1..<pointsContainingShapeArray.count {
movingShape = level.shapeAtColumn(pointsContainingShapeArray[everyShape].Column, row: pointsContainingShapeArray[everyShape].Row)
let Duration: NSTimeInterval = 1
let moveShapes = SKAction.moveTo((destinationShape?.sprite!.position)!,duration: Duration)
moveShapes.timingMode = .EaseOut
movingShape?.sprite?.runAction(moveShapes, completion: {
movingShape?.sprite?.removeFromParent()
print("Removed shape \(everyShape)")
if everyShape == pointsContainingShapeArray.count {
completion()
}
})
}
}
Basically the idea is that every shape in the array after the first position moves to the position of the first one and then removes it from the scene. This works fine, but my completion was getting called at random times every time. So finally I added the print statement in there. Here was the output:
Removed shape 3
Removed shape 1
Removed shape 4
Removed shape 2
At this point I got so frustrated I decided it was time for stack overflow. I can't possibly figure out what's going on here. Thanks for any help!
Here is the code that calls this method:
func handleSwipe(move: Move) {
view.userInteractionEnabled = false
level.performMove(move)
scene.animateMove(move) {
self.view.userInteractionEnabled = true
if hasMatch {
self.view.userInteractionEnabled = false
self.scene.animateMatch() {
self.scene.addNewSpritesForShapes(pointsContainingShapeArray)
hasMatch = false
self.view!.userInteractionEnabled = true
}
}
}
}
The issue isn't the for loop, it's the asynchronous nature of the run action.
When you call moveShapes action, it runs asynchronously before calling the completion back on the original thread. This can happen in any order. You can see this yourself by calling your print synchronously:
for everyShape in 1..<pointsContainingShapeArray.count {
print("Synchronous loop: \(everyShape)")
}
I think you'd be better off using a dispatch_group_t for your final completion:
func animateMatch(completion: () -> ()) {
var movingShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
let destinationShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
// HERE
let group = dispatch_group_create()
dispatch_group_notify(group, dispatch_get_main_queue(), completion)
//
for everyShape in 1..<pointsContainingShapeArray.count {
// HERE
dispatch_group_enter(group)
//
movingShape = level.shapeAtColumn(pointsContainingShapeArray[everyShape].Column, row: pointsContainingShapeArray[everyShape].Row)
let Duration: NSTimeInterval = 1
let moveShapes = SKAction.moveTo((destinationShape?.sprite!.position)!,duration: Duration)
moveShapes.timingMode = .EaseOut
movingShape?.sprite?.runAction(moveShapes, completion: {
movingShape?.sprite?.removeFromParent()
print("Removed shape \(everyShape)")
// HERE
dispatch_group_leave(group)
//
})
}
}

Swift 2.2: Segmentation fault: 11

I am getting a segfault: 11 after updating to the latest version of Xcode (7.3). The code that is triggering this is:
private func makeScreenshots(points points_I: [[CGPoint]], frames frames_I: [[CGRect]], scale:CGFloat, completion: (screenshots: [[UIImage]]) -> Void) {
var counter: Int = 0
// use didSet as a responder to the completion handler,instead of a loop, ensuring nice sequential execution
var images: [[UIImage]] = [] {
didSet {
if counter < points_I.count {
internalScreenshotRow()
} else {
completion(screenshots: images)
}
}
}
// same code is used twice -> nested function
func internalScreenshotRow() {
makeScreenshotRow(points: points_I[counter], frames: frames_I[counter], scale: scale) { (screenshot) -> Void in
counter += 1
images.append(screenshot)
}
}
internalScreenshotRow() // start first run
}
private func makeScreenshotRow(points points_I: [CGPoint], frames frames_I: [CGRect], scale:CGFloat, completion: (screenshots: [UIImage]) -> Void) {
var counter: Int = 0
// use didSet as a responder to the completion handler,instead of a loop, ensuring nice sequential execution
var images: [UIImage] = [] {
didSet {
if counter < points_I.count {
internalTakeScreenshotAtPoint()
} else {
completion(screenshots: images)
}
}
}
// same code is used twice -> nested function
func internalTakeScreenshotAtPoint() {
takeScreenshotAtPoint(point: points_I[counter], scale: scale) { (screenshot) -> Void in
counter += 1
images.append(screenshot)
}
}
internalTakeScreenshotAtPoint() // start first run
}
The error being thrown by compiler is:
1. While emitting SIL for 'makeScreenshots' at /ScrollViewImager.swift:115:13
2. While emitting SIL for 'internalScreenshotRow' at /ScrollViewImager.swift:131:9
3. While silgen closureexpr SIL function #_TFFFE8BeamItUpPS_16ScrollViewImagerP33_22F49B169CD6FD663C230349D2C79AE615makeScreenshotsFT6pointsGSaGSaVSC7CGPoint__6framesGSaGSaVSC6CGRect__5scaleV12CoreGraphics7CGFloat10completionFT11screenshotsGSaGSaCSo7UIImage___T__T_L_21internalScreenshotRowFT_T_U_FGSaS5__T_ for expression at [/ScrollViewImager.swift:132:99 - line:135:13] RangeText="{ (screenshot) -> Void in
counter += 1
images.append(screenshot)
}"
The ScrollViewImager is an open source library for taking screenshots of scrollview Reference

Single SKAction Animation on several nodes using a loop with completion handler

Objective: Apply a single SKAction Animation on several nodes using a loop, while waiting for completion handler.
func animateShuffle(cards: [Card], completionHandler:(success: Bool) -> Void) {
var rotationInRadians:CGFloat = CGFloat(M_PI)
var rotateFirstDeck = SKAction.rotateByAngle(rotationInRadians, duration: 1.0)
var moveFirstDeck = SKAction.moveToX(-150, duration: 1.0)
var returnToOrigPosition = SKAction.moveToX(-125, duration: 1.0)
var firstDeckSequence = SKAction.sequence([moveFirstDeck, rotateFirstDeck, returnToOrigPosition])
var rotateSecondDeck = SKAction.rotateByAngle(-rotationInRadians, duration: 1.0)
var moveSecondDeck = SKAction.moveToX(-100, duration: 1.0)
var secondDeckSequence = SKAction.sequence([moveSecondDeck, rotateSecondDeck, returnToOrigPosition])
for (index, Card) in enumerate(cards){
if index % 2 == 0{
Card.image.runAction(firstDeckSequence)
}
else{
Card.image.runAction(secondDeckSequence)
}
}
//hack
cards[0].image.runAction(firstDeckSequence, completion: { () -> Void in
completionHandler(success: true)
})
}
Here I call the function which kicks off another Animation:
animateShuffle(newDeck.deck, completionHandler: { (success) -> Void in
print("entered")
var drawnCards = [Card]()
if let areThereCards = newDeck.drawCards(4){
drawnCards = areThereCards
}
self.dealCards(drawnCards)
})
I used a hack to get to 99% of where I want to be but my gut tells me there is better way to do this. How can I tell when the last node has finished animating?
Both sequences run for 3 seconds so when the first completion block runs you can safely assume they are all done. To be 100% certain set a bool ivar in the completion handler and then run your post-sequence code in the didEvaluateActions method of the scene.
Alternatively count how many sequences you run, then increase a counter in completion block, and run your code when both counters match.

Resources