Odd error when capturing function parameter in Swift call to animateWithDuration:Animations: - swift

I'm writing some simple animation code to make a button get taller and then shorter using UIView animations. The code is a little long, but fairly simple:
func animateButton(aButton: UIButton, step: Int)
{
let localStep = step - 1
let localButton = aButton
let halfHeight = aButton.bounds.height / 2
var transform: CGAffineTransform
switch step
{
case 2:
//Make the center of the grow animation be the bottom center of the button
transform = CGAffineTransformMakeTranslation(0, -halfHeight)
//Animate the button to 120% of it's normal height.
transform = CGAffineTransformScale( transform, 1.0, 1.2)
transform = CGAffineTransformTranslate( transform, 0, halfHeight)
UIView.animateWithDuration(0.5, animations:
{
aButton.transform = transform
},
completion:
{
(finshed) in
//------------------------------------
//--- This line throws the error ---
animateButton(aButton, step: 1)
//------------------------------------
})
case 1:
//In the second step, shrink the height down to .25 of normal
transform = CGAffineTransformMakeTranslation(0, -halfHeight)
//Animate the button to 120% of it's normal height.
transform = CGAffineTransformScale( transform, 1.0, 0.25)
transform = CGAffineTransformTranslate( transform, 0, halfHeight)
UIView.animateWithDuration(0.5, animations:
{
aButton.transform = transform
},
completion:
{
(finshed) in
animateButton(aButton, step: 0)
})
case 0:
//in the final step, animate the button back to full height.
UIView.animateWithDuration(0.5)
{
aButton.transform = CGAffineTransformIdentity
}
default:
break
}
}
The completion blocks for the animation methods are closures. I'm getting an error "Call to method animateButton in closure requires explicit self. to make capture semantics explicit.
The thing is, the parameter aButton is a parameter to the enclosing function. There is no reference to an instance variable.
It looks to me like this is compiler bug. Am I missing something here?

Calling methods in the same class are called with an implied self. In this case because of the closure you have to make it explicit:
self.animateButton(aButton, step: 1)

Related

Use stride to rotate object

I want to use the stride syntax to rotate a object in stride. The box should go through 3 iterations and rotate 270 degrees. I don’t believe everything some of my code below matches the Syntax by word. Just a FYI.
Var box = UIView()
Override func viewdidload(){
Super.viewdidload()
For rotate in Stride(from : 0, to: 3, by +1 ) {
box.transform = box.transform.rotatedby( .pi/2)
}}
Swift, Stirde, loop, viewdidload
you cannot use a loop to do that, because the UI will only be updated after the loop finish
you can use UIView.animate to do animation and do recursive to achieve n-times animation
func rotateBox(count: Int) {
if count > 0 {
UIView.animate(withDuration: 1, animations: {
self.box.transform = self.box.transform.rotated(by: .pi/2)
}, completion: { _ in
self.rotateBox(count: count - 1)
})
}
}

Swift Dynamic animation followed by property animator

I have an animation where I use a push animation, then a snap animation using UIDynamicBehavior, and then I finish with a property behavior:
for card in selectedCards {
removeCard(card: card)
}
private func removeCard(card: Card) {
guard let subView = cardsContainer.subviews.first(where: { ($0 as? PlayingCardView)?.card == card }) else {
return
}
if let card = subView as? PlayingCardView { card.selected = false }
let matchedCardsFrame = matchedCards.convert(matchedCards.frame, to: view)
view.addSubview(subView)
cardBehavior.addItem(subView) // here I add the push behavior
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.cardBehavior.removeItem(subView) // here I remove the push behavior
UIViewPropertyAnimator.runningPropertyAnimator(
withDuration: 0.3,
delay: 0,
options: [],
animations: {
self.cardBehavior.addSnapBehavior(subView, frame: matchedCardsFrame) // here I add the snap behavior
}, completion: { finished in
self.animator.removeAllBehaviors()
subView.frame.size = CGSize(width: matchedCardsFrame.height, height: matchedCardsFrame.width)
subView.transform = CGAffineTransform.identity.rotated(by: CGFloat.pi / 2)
subView.setNeedsDisplay()
})
}
}
Essentially the above code does the following:
Add push behavior
Remove push behavior
Add snap behavior
Remove all behaviors
Add property transform
What I want is for the push action to execute, then after a second or so, have the snap behavior execute, and after the snap execution is finished, to perform a transform. However, if I removeAllBehaviors() before I execute the property transform then the snap behavior doesn't finish. But if I leave the snap behavior and try to execute the property transform then it has no effect since it appears that the snap behavior acts on the object indefinitely, putting it at odds with the property transform.
How can I programmatically say finish the snap behavior and then perform the transform?

UIView.animateWithDuration not animating after initial animation

I have two methods:
private func showPicker() {
UIView.animate(withDuration: 0.3,
animations: {
var pickerFrame = self.vPickerWrapper.frame
pickerFrame.origin.y = self.view.frame.size.height - 300.0
self.vPickerWrapper.frame = pickerFrame
self.bIsShowingPicker = true
self.view.layoutIfNeeded()
self.view.updateConstraintsIfNeeded()
})
}
private func closeThePicker() {
UIView.animate(withDuration: 0.3,
animations: {
var pickerFrame = self.vPickerWrapper.frame
pickerFrame.origin.y = self.view.frame.size.height + 1
self.vPickerWrapper.frame = pickerFrame
self.bIsShowingPicker = false
self.view.layoutIfNeeded()
self.view.updateConstraintsIfNeeded()
})
....
the first method is called when the user touches a text field that requires them to populate from the pickerview. The second method is called when either the close or select button is touched within the wrapper.
The wrapper is created in storyboard and has constraints (trailing, leading bottom) to position it off screen. I initially made the bottom constraint a IBOutlet and wanted to change its constant to handle the animation. (Which I do with other objects in this VC). I then tried to work with CGAffineTransform.translate and .identity and finally dropped down to working with the frame. In all three cases the wrapper will display but not close (move back off screen).
Here's the value of vPickerWrapper after I set its frame to pickerFrame:
pickerFrame CGRect (origin = (x = 0, y = 668), size = (width = 375, height = 300))
which is what I am expecting, just not any animation in the sim. The view never moves. Any idea why the display is not showing it in the new coordinates?

Swift: Cant change CALayer background color in array

I am simply trying to change the background color of the last element of a CALayer array. Here is my entire View Class, however its only 2-3 lines that I actually try to access the last element of the CALayer.
Here is my progressViewClass and I put comments to where exactly my problem is:
class ProgressBarView: UIView {
//Variables for progress bar
var holdGesture = UILongPressGestureRecognizer()
let animation = CABasicAnimation(keyPath: "bounds.size.width")
var layerHolder = [CALayer]()
var widthIndex = CGPoint(x: 0, y: 0)
var nextXOffset = CGFloat(0.0)
var checkIfFull = CGFloat()
var newLayer : CALayer?
var progressBarAnimationDuration : CFTimeInterval = (MainController.sharedInstance.totalMiliSeconsToRecord / 10)
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
}
func startProgressBar(){
if(RecordingViewController().currentCameraMode == .recordingMode || RecordingViewController().currentCameraMode == .record1stClipMode) {
newLayer = CALayer()
newLayer?.frame = CGRect(x: nextXOffset, y: 0, width: 0, height: self.bounds.height)
newLayer?.backgroundColor = UIColor(red:0.82, green:0.01, blue:0.11, alpha:1.0).cgColor
//print("before \(nextXOffset)")
newLayer?.anchorPoint = widthIndex
animation.fromValue = 0
animation.toValue = self.bounds.width - nextXOffset
animation.duration = progressBarAnimationDuration - ((MainController.sharedInstance.miliSecondsPassed) / 10)
self.layer.addSublayer(newLayer!)
//print("Long Press Began")
newLayer?.add(animation, forKey: "bounds.size.width")
}
else{
stopProgressBar()
}
}
func stopProgressBar(){
if(RecordingViewController().currentCameraMode != .recordingMode){
pauseLayer(layer: newLayer!)
newLayer?.frame = (newLayer?.presentation()!.frame)!
nextXOffset = (newLayer?.frame.maxX)!
layerHolder.append(newLayer!)
print("Layerholder has elements : \(layerHolder.count)")
}
}
// HERE IS MY PROBLEM
func highlightLastLayer(){
print("in highlight last layer Layerholder has elements : \(layerHolder.count)")
// I CAN HIDE THE CALAYER SO I BELIEVE IM ACCESSING THE CORRECT LAYER
// layerHolder.last?.isHidden = true
// This is suppose to change the last element background color to blue but doesnt
layerHolder.last?.backgroundColor = UIColor.blue.cgColor
}
// ALSO MY PROBLEM
func unhighlightLastLayer(){
print("inside unhighlight last layer")
// I CAN HIDE THE CALAYER SO I BELIEVE IM ACCESSING THE CORRECT LAYER
//layerHolder.last?.isHidden = false
// Changes CALayer back to red
layerHolder.last?.backgroundColor = UIColor(red:0.82, green:0.01, blue:0.11, alpha:1.0).cgColor
}
//Function to pause the Progress Bar
func pauseLayer(layer : CALayer){
let pausedTime : CFTimeInterval = layer.convertTime(CACurrentMediaTime(), from: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
}
}
Simply put, I create a progressView object in my viewController and then call those functions based on certain button input. This view is essentially a progress bar that you'd see in many video recording applications to show how much you have recorded. In the highlightLastLayer, I am trying to grab the last element of the "layerHolder" array and change its color to blue. Simple right? Doesn't work. Any ideas?
Where are you calling highlight and unhighlight. I am pretty sure you are doing this when you are "stopped" or "paused" because thats the only time you add anything to the layerHolder Array. You can only do this when you are not animating because when you are animating the presentation layer is shown instead of the "real" layer. Instead of setting the speed to zero, setup your layer to look like the current animation state and call layer. removeAllAnimations to kill the presentation layer and show the actual layer for your view instead. Now you can make all the changes that you want and they will actually show up.

How to increase height of UIButton image without moving button

I have added a UIButton to storyboard and then added my image to the button. What I am looking to do is when you tap on the button the height increases slightly and then the button decreases in height by about a quarter.
#IBAction func StopsClick(sender: UIView) {
//Get the y coordinates of Image
let origin = sender.bounds.origin.y
//This decreases the height of Image but the image moved. I want the image to remain stationary and only the top of the image to increase in height.
UIView.animateWithDuration(0.5) {
sender.bounds = CGRectMake(0, origin, sender.bounds.width, sender.bounds.height * 1.2)
}
UIView.animateWithDuration(1.0) {
sender.bounds = CGRectMake(0, orgin, sender.bounds.width, sender.bounds.height * 0.4)
}
}
You should be able to get the effect you're after using transforms.
I would do the following:
Start with the identity transform.
Shift the origin of the transform down to the bottom of the image.
Increase the transform's scale.y as desired.
Shift the transform's origin back to the center of the image
Apply that transform to the button in your animation. Then animate it back to the identity transform.
If you don't shift the transform and only scale it it will grow in all directions from the center. (or only up and down if you only increase the scale.y)
Edit:
The code might look like this:
let halfHeight = button.bounds.height / 2
//Make the center of the grow animation be the bottom center of the button
var transform = CGAffineTransformMakeTranslation(0, -halfHeight)
//Animate the button to 120% of it's normal height.
tranform = CGAffineTransformScale( transform, 1.0, 1.2)
tranform = CGAffineTransformTranslate( transform, 0, halfHeight)
UIView.animateWithDuration(0.5)
{
button.transform = transform
}
The above code animates the button to 120% height, and leaves it there.
You could use one of the longer variants of animateWithDuration that takes options to make the animation auto-reverse. I leave that as an exercise for you.
Edit #2:
I banged out some code to do a 3-step animation:
func animateButton(step: Int)
{
let localStep = step - 1
let halfHeight = aButton.bounds.height / 2
var transform: CGAffineTransform
switch step
{
case 2:
//Make the center of the grow animation be the bottom center of the button
transform = CGAffineTransformMakeTranslation(0, -halfHeight)
//Animate the button to 120% of it's normal height.
transform = CGAffineTransformScale( transform, 1.0, 1.2)
transform = CGAffineTransformTranslate( transform, 0, halfHeight)
UIView.animateWithDuration(0.5, animations:
{
aButton.transform = transform
},
completion:
{
(finshed) in
animateButton(step)
})
case 1:
//In the second step, shrink the height down to .25 of normal
transform = CGAffineTransformMakeTranslation(0, -halfHeight)
//Animate the button to 120% of it's normal height.
transform = CGAffineTransformScale( transform, 1.0, 0.25)
transform = CGAffineTransformTranslate( transform, 0, halfHeight)
UIView.animateWithDuration(0.5, animations:
{
aButton.transform = transform
},
completion:
{
(finshed) in
animateButton(step)
})
case 0:
//in the final step, animate the button back to full height.
UIView.animateWithDuration(0.5)
{
aButton.transform = CGAffineTransformIdentity
}
default:
break
}
}
You'd invoke it with
animateButton(3)
It would be cleaner to use an enum for the step number, and it could use some range checking to make sure the input value is 3, 2, or 1, but you get the idea...