iOS blink animation on UIView with finite number of times - ios

I am trying to create a blinking effect on the UIView. Currently I am using a code which blinks the UIView with infinite number of times. the Code looks like this
WhatI have done so far:
func startBlink() {
UIView.animate(withDuration: 0.8,//Time duration
delay:0.0,
options:[.allowUserInteraction, .curveEaseInOut, .autoreverse, .repeat],
animations: { self.alpha = 0 },
completion: nil)
}
But this code blinks the ui view for infinite number of time. I used another code but that was blinking for one time only.
What I want:
So I am pretty close but I really want to blink the UIView for finite
number of times i.e 30 times, and it must stop after 30th blink.
Please help me in this, I think I have clear in my question. Please help me out.

Use this function to animate View. I hope it can help
extension UIView {
func flash(numberOfFlashes: Float) {
let flash = CABasicAnimation(keyPath: "opacity")
flash.duration = 0.2
flash.fromValue = 1
flash.toValue = 0.1
flash.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
flash.autoreverses = true
flash.repeatCount = numberOfFlashes
layer.add(flash, forKey: nil)
}
}

There is a builtin in class function for the count and call it in the block.
class func setAnimationRepeatCount(_ repeatCount: Float)
func startBlink() {
UIView.animate(withDuration: 0.8,//Time duration
delay:0.0,
options:[.allowUserInteraction, .curveEaseInOut, .autoreverse, .repeat],
animations: {
UIView.setAnimationRepeatCount(30) // repeat 30 times.
self.alpha = 0
},
completion: nil)
}

Related

Sequence 2 Animations That Loop With Delay In-Between

Goal: Create a looping animation with 2 overlapping uilabels to show one and hide the other by setting alphas respectively to 1 and 0.
Any tips would be appreciated.
What I've Tried:
Autoreverse/repeat animation - While this works, there is no delay in-between when the animation reverses and repeats itself.
DispatchQueue.main.async {
UIView.animate(withDuration: 1.0, delay: 3.0, options: [.autoreverse, .repeat]) {
self.labelOne.alpha = 0
self.labelTwo.alpha = 1
}
}
Recursive animation - This works, but when I re-enter the foreground from the background, the animation spazzes out showing/hiding each uilabel. My issue here is being unable to stop/reset the recursive animation.
DispatchQueue.main.async {
UIView.animate(withDuration: 1.0, delay: 3.0, options: .curveLinear) {
self.labelOne.alpha = 0
self.labelTwo.alpha = 1
} completion: { isComplete in
UIView.animate(withDuration: 1.0, delay: 3.0, options: [.curveLinear]) {
self.labelOne.alpha = 1
self.labelTwo.alpha = 0
} completion: { isComplete in
self.runThisAnimation()
}
}
}

How to move UITableViewCell back and forth to show it can be swiped?

I see in some apps when you come to a screen with a tableview there's a short animation of the cell starting to be swiped, showing the red "swipe to delete" button (UIContextualAction button) and then it returns to normal. It is giving the user the hint: "These rows can be swiped."
Is there a way to achieve this effect? Maybe a way to programmatically start a row swipe then cancel it?
Swift Solution
Well, about 1.5 years later I finally came up with a solution.
Step 1 - Cell UI Setup
I set up my custom table view cell like this:
A and B represent the swipe action colors.
C is the UIView that I will animate side-to-side.
Step 2 - Add Animation to Cell
func animateSwipeHint() {
slideInFromRight()
}
private func slideInFromRight() {
UIView.animate(withDuration: 0.5, delay: 0.3, options: [.curveEaseOut], animations: {
self.cellBackgroundView.transform = CGAffineTransform(translationX: -self.swipeHintDistance, y: 0)
self.cellBackgroundView.layer.cornerRadius = 10
}) { (success) in
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveLinear], animations: {
self.cellBackgroundView.transform = .identity
}, completion: { (success) in
// Slide from left if you have leading swipe actions
self.slideInFromLeft()
})
}
}
private func slideInFromLeft() {
UIView.animate(withDuration: 0.5, delay: 0, options: [.curveEaseOut], animations: {
self.cellBackgroundView.transform = CGAffineTransform(translationX: self.swipeHintDistance, y: 0)
}) { (success) in
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveLinear], animations: {
self.cellBackgroundView.transform = .identity
})
}
}
Step 3 - Trigger the Animation
In the viewDidLoad of the view controller that has the table view, I have this code:
if self.tableView.visibleCells.count > 0 {
let cell = self.tableView.visibleCells[0] as! TableViewCell
cell.animateSwipeHint()
}
Example:
Video Solution
I created a video if you'd like a more in-depth walkthrough of this solution:
https://youtu.be/oAGoFd_GrxE
I have a piece of code that I saw long time ago to animate a view. Since our UITableViewCell is also a view, we can use it :) You just need to get your visible cell to animate, like so:
if let visibleCell = self.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? CustomCell {
print("Started animation...")
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 0.6
animation.values = [-20.0, 20.0, -20.0, 20.0, -10.0, 10.0, -5.0, 5.0, 0.0 ]
visibleCell.layer.add(animation, forKey: "shake")
}
Let me know if this helps. Tested it.
EDIT:
Animating your UITableView to let the user see that they can swipe on a cell is pretty easy, try it like so:
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
self.tableView.setEditing(true, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
self.tableView.setEditing(false, animated: true)
}
}
HOWEVER, if you want to swipe programmatically your cell to show your custom row actions, (I've been researching this for an hour), you can only achieve this, as far as I know, by using method swizzling. See this SO answer: http://codejaxy.com/q/186524/ios-swift-uitableview-how-to-present-uitableviewrowactions-from-pressing-a-button

Perfect Swift3 Boing

To animate a bar opening...
#IBOutlet var barHeight: NSLayoutConstraint!
barHeight.constant = barShut?30:100
self.view.layoutIfNeeded()
t = !barShut?30:100
UIView.animate(withDuration: 0.15,
delay: 0,
options: UIViewAnimationOptions.curveEaseOut,
animations: { () -> Void in
self.barHeight.constant = t
self.view.layoutIfNeeded()
},
completion: {_ in
Screen.barShut = !Screen.barShut
}
)
That's great ...
But how would you make it boing like this?
(The only way I'd know to do this is, use CADisplayLink, with a few lines of code for a spring decaying.) Is this available in UIKit?
You can use the spring animation method that is built in to UIView:
func toggleBar() -> Void {
self.view.layoutIfNeeded()
let newHeight:CGFloat = !barShut ? 30:100
barShut = !barShut
barHeightConstraint.constant = newHeight
UIView.animate(withDuration: 1.5, delay: 0, usingSpringWithDamping: 0.2, initialSpringVelocity: 3, options: [], animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
You will want a longer animation duration than 0.15 of a second in order for the bounce to seem realistic; I think the values I have look pretty good, but you can play with them to get the exact effect you are after.
Since the animation duration is longer, I found that I could tap the button the triggered the open/shut while the previous animation was still running. Setting barShut in the completion block meant that the bar didn't react to all taps. I moved the toggle outside of the animation to address this.

Best way to execute multiple animations synchronously

This will be a 'best way to do' kind of a question.
I know there are multiple ways to do sequence animations, but which one is the better way to do it?
let's say I have 3 animations that I'm trying to execute, one after the other, so the next one only starts when the previous has finished.
I can do it with the UIView.animate(withCompletionBlock:) so in the completion section I call the second one and on the completionBlock of the second one I call the third one.
This one in particular makes me think: Is that a 'beautiful' way to do it? Is there any known issues using completionBlocks within another?
Just a thought. Calling animations within the previous animation completion block, may bring the code somehow like the pyramid of doom, if there's something like 10 synchronous animations, don't you think?
Another way to do is with the dispatch_after, the I call the next animation based on the NSTimeInterval of the previous one. In this case it may cause a app crash suspending the Main thread for the next one to execute, then suspend it again to execute the third one.
The last known way that I know is that I can use a NSTimeInterval to call the next animation, but this one is somewhat like the dispatch_after option.
In your opinion which would be the best way to do it?
Executing 3 or more visual animations, all synchronously (waiting for the previous one to finish to start the next one).
I might use this a lot in my app and I want to start it with the best way.
EXAMPLES
Animations within completionBlocks
DispatchQueue.main.async {
UIView.animate(withDuration: 2.0, animations: {
view1.alpha = 1
}, completion: { (_) in
UIView.animate(withDuration: 5.0, animations: {
view2.alpha = 1
}, completion: { (_) in
UIView.animate(withDuration: 2.0, animations: {
view3.alpha = 0
}, completion: { (_) in
UIView.animate(withDuration: 2.0, animations: {
view4.alpha = 0
}, completion: { (_) in
print("all completed")
})
})
})
})
}
With dispatch_after:
let anim1_duration = 2.0
let anim2_duration = 3.0
let anim3_duraiton = 5.0
UIView.animate(withDuration: anim1_duration, animations: {
view1.alpha = 1
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.init(uptimeNanoseconds: UInt64(anim1_duration * Double(NSEC_PER_SEC)))) {
UIView.animate(withDuration: anim2_duration, animations: {
view2.alpha = 1
}
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.init(uptimeNanoseconds: UInt64((anim1_duration + anim2_duration) * Double(NSEC_PER_SEC)))) {
UIView.animate(withDuration: anim3_duration, animations: {
view3.alpha = 1
}
}
Check out UIView's animate​Keyframes(with​Duration:​delay:​options:​animations:​completion:​):
class func animateKeyframes(withDuration duration: TimeInterval,
delay: TimeInterval,
options: UIViewKeyframeAnimationOptions = [],
animations: #escaping () -> Void,
completion: ((Bool) -> Void)? = nil)
For your specific case, it would look like this:
UIView.animateKeyframes(withDuration: 20,
delay: 0,
options: [],
animations: {
UIView.addKeyframe(withRelativeStartTime: 0,
relativeDuration: 0.2,
animations: {
view1.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 0.2,
relativeDuration: 0.3,
animations: {
view2.alpha = 1
})
UIView.addKeyframe(withRelativeStartTime: 0.5,
relativeDuration: 0.1,
animations: {
view3.alpha = 0
})
UIView.addKeyframe(withRelativeStartTime: 0.6,
relativeDuration: 0.4,
animations: {
view4.alpha = 0
})
},
completion: nil)
In terms of 'better reading code' consider these 2 ways of performing animations:
building stacks of animations: more
create keyframe animations using CAKeyframeAnimation: more
These options are avoiding completion blocks, which I like.

animateKeyframes with repeat and delay does not work as expected

I'm creating a simple left to right animation for a label using key frames but when the animation repeats, the delay is ignored.
The first time it executes, the delay of 3 seconds has an effect, but when the animation repeats, the delay is ignored. This causes the animation to re-start immediately after it ends.
UIView.animateKeyframes(withDuration: 10, delay: 3, options: [.calculationModePaced, .repeat], animations: {
let xDist = self.Label_ArtistAlbum2.frame.origin.x
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = self.Label_ArtistAlbum2.frame.origin.x - (xDist * 0.1)
})
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
}, completion: nil)
I've tried adding an extra keyframe at the end however this has no effect even with the altered times:
UIView.animateKeyframes(withDuration: 10, delay: 3, options: [.calculationModePaced, .repeat], animations: {
let xDist = self.Label_ArtistAlbum2.frame.origin.x
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = self.Label_ArtistAlbum2.frame.origin.x - (xDist * 0.1)
})
UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.7, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
//attempted pause - does not appear to work perhaps since the position is unchanged?
UIView.addKeyframe(withRelativeStartTime: 0.8, relativeDuration: 0.2, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
}, completion: nil)
If the delay will not be repeated along with the rest of the animation, how can I create a pause before the entire animation repeats?
I had a similar problem for animating a loading view. I solved it this way:
I created an enum for the steps in the animation
private enum TriangleToAnimate {
case one
case two
case three
case pause
}
I have my variables
private var triangleViewToFireCount = TriangleToAnimate.one
var triangle1View : TriangleView
var triangle2View : TriangleView
var triangle3View : TriangleView
I start a timer to run each animation
override init(frame: CGRect) {
Timer.scheduledTimer(timeInterval: 0.33, target: self, selector: #selector(LoadingView.timerFire), userInfo: nil, repeats: true)
}
For the selector I have a fire method. In the method I have a switch for each of the enum cases
func timerFire(){
let anim = createAnimation()
switch triangleViewToFireCount {
case .one:
triangle1View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .two
case .two:
triangle2View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .three
case .three:
triangle3View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .pause
default:
triangleViewToFireCount = .one
}
}
This is the code how I created the animation as a keyframe
func createAnimation() -> CAKeyframeAnimation{
let tr = CATransform3DIdentity
let orignalScale = CATransform3DScale(tr, 1, 1, 1)
let doubleScale = CATransform3DScale(tr, 2, 2, 1)
let keyAn = CAKeyframeAnimation(keyPath: "transform")
keyAn.keyTimes = [0, 0.1, 0.6]
keyAn.duration = 1
keyAn.values = [orignalScale,doubleScale,orignalScale]
keyAn.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
return keyAn
}
I've done a lot of testing on this issue and have found what I believe is the problem.
I think that the animation option for the Curve is my issue.
In my case choosing
calculationModePaced
has the effect of recalculating my keyframe parameters and not guaranteeing to hit any of them except the beginning and end. All intermediate keyframes become 'suggestions'.
You can't create a pause at the end because the keyframe is 'consumed' when recalculating and does not stand on it's own.
I changed to calculationModeLinear and got the keyframes I expected, and the pause too.
However, it is as smooth as I would like so I'll have to keep tinkering...
Apple's docs are here - they're descriptive but could really use some graphics and/or examples:
https://developer.apple.com/reference/uikit/uiview?language=swift
A good graph of the curves can be found here: https://www.shinobicontrols.com/blog/ios7-day-by-day-day-11-uiview-key-frame-animations

Resources