CABasicAnimation reaches its endpoint faster then the time it's set to - ios

If anyone wants to try this code you can just c+p the file and it will run
I actually have 2 problems in the code below.
1- I have a Timer and a CABasicAnimation that both run when a longPressGesture is triggered. The timer is 15 secs and I decided to use it to just time the animation once I noticed the issue. What's happening is the animation finishes before the timer does. The animation will close/reach its endpoint around 1 sec before the timer finishes AND before CATransaction.setCompletionBlock() and animationDidStop(_:finished) are called. Basically the animation finishes too early.
2- If I take my finger off of the button, the longPressGesture's .cancelled/.ended are called and I pause the timer in invalidateTimer via pauseShapeLayerAnimation(). That was the only way I found to actually stop the animation. When I long press the button again, I restart the timer and animation from the beginning. The issue is because pauseShapeLayerAnimation() is also called when the timer stops (goes to 15secs) CATransaction.setCompletionBlock() are never animationDidStop(_:finished) called. They are only called once I put my finger back on the button.
UPDATE I fixed the second issue by just checking if seconds are != 0 in the invalidateTimer function
import UIKit
class ViewController: UIViewController {
//MARK:- UIElements
fileprivate lazy var roundButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = UIColor.blue
return button
}()
fileprivate lazy var timerLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.monospacedDigitSystemFont(ofSize: 22, weight: .medium)
label.textColor = UIColor.black
label.text = initialStrForTimerLabel
label.textAlignment = .center
return label
}()
fileprivate lazy var box: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .brown
return view
}()
//MARK:- Properties
fileprivate let shapeLayer = CAShapeLayer()
fileprivate let bgShapeLayer = CAShapeLayer()
fileprivate var basicAnimation: CABasicAnimation!
fileprivate var maxTimeInSecs = 15
fileprivate lazy var seconds = maxTimeInSecs
fileprivate var milliseconds = 0
fileprivate lazy var timerStr = initialStrForTimerLabel
fileprivate lazy var initialStrForTimerLabel = "\(maxTimeInSecs).0"
fileprivate weak var timer: Timer?
//MARK:- View Controller Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setAnchors()
setGestures()
}
fileprivate var wereCAShapeLayersAdded = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !wereCAShapeLayersAdded {
wereCAShapeLayersAdded = true
roundButton.layer.cornerRadius = roundButton.frame.width / 2
addBothCAShapeLayersToRoundButton()
}
}
//MARK:- Animation Methods
fileprivate func addBothCAShapeLayersToRoundButton() {
bgShapeLayer.frame = box.bounds
bgShapeLayer.path = UIBezierPath(rect: box.bounds).cgPath
bgShapeLayer.strokeColor = UIColor.lightGray.cgColor
bgShapeLayer.fillColor = UIColor.clear.cgColor
bgShapeLayer.lineWidth = 6
box.layer.addSublayer(bgShapeLayer)
box.layer.insertSublayer(bgShapeLayer, at: 0)
shapeLayer.frame = box.bounds
shapeLayer.path = UIBezierPath(rect: box.bounds).cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 6
shapeLayer.lineCap = .round
shapeLayer.strokeEnd = 0
box.layer.addSublayer(shapeLayer)
}
fileprivate var isBasicAnimationAnimating = false
fileprivate func addProgressAnimation() {
CATransaction.begin()
basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
removeAnimation()
if shapeLayer.timeOffset > 0.0 {
shapeLayer.speed = 1.0
shapeLayer.timeOffset = 0.0
}
basicAnimation.delegate = self
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
basicAnimation.fromValue = 0
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(seconds)
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
basicAnimation.isRemovedOnCompletion = false
CATransaction.setCompletionBlock {
print("CATransaction completion called\n")
}
shapeLayer.add(basicAnimation, forKey: "myAnimation")
CATransaction.commit()
}
fileprivate func removeAnimation() {
shapeLayer.removeAnimation(forKey: "myAnimation")
}
fileprivate func pauseShapeLayerAnimation() {
let pausedTime = shapeLayer.convertTime(CACurrentMediaTime(), from: nil)
shapeLayer.speed = 0.0
shapeLayer.timeOffset = pausedTime
print("animation has paused/stopped\n")
}
//MARK:- Anchors
fileprivate func setAnchors() {
view.addSubview(box)
view.addSubview(roundButton)
view.addSubview(timerLabel)
box.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 3).isActive = true
box.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 3).isActive = true
box.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -3).isActive = true
box.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -3).isActive = true
roundButton.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true
roundButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
roundButton.widthAnchor.constraint(equalToConstant: 75).isActive = true
roundButton.heightAnchor.constraint(equalToConstant: 75).isActive = true
timerLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10).isActive = true
timerLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
timerLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
}
}
//MARK:- CAAnimationDelegate
extension ViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print("***** animation done *****\n")
}
}
//MARK:- Timer Methods
extension ViewController {
fileprivate func startTimer() {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] _ in
self?.timerIsRunning()
})
}
#objc fileprivate func timerIsRunning() {
updateTimerLabel()
if !isBasicAnimationAnimating {
isBasicAnimationAnimating = true
addProgressAnimation()
}
milliseconds -= 1
if milliseconds < 0 {
milliseconds = 9
if seconds != 0 {
seconds -= 1
} else {
invalidateTimer()
print("timer done\n")
}
}
if milliseconds == 0 {
milliseconds = 0
}
}
fileprivate func updateTimerLabel() {
let millisecStr = "\(milliseconds)"
let secondsStr = seconds > 9 ? "\(seconds)" : "0\(seconds)"
timerLabel.text = "\(secondsStr).\(millisecStr)"
}
fileprivate func resetTimerSecsAndLabel() {
milliseconds = 0
seconds = maxTimeInSecs
timerLabel.text = initialStrForTimerLabel
}
fileprivate func invalidateTimer() {
if isBasicAnimationAnimating {
isBasicAnimationAnimating = false
if seconds != 0 {
pauseShapeLayerAnimation()
}
}
timer?.invalidate()
}
}
//MARK:- Gestures
extension ViewController {
fileprivate func setGestures() {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGesture))
roundButton.addGestureRecognizer(tapRecognizer)
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressGesture))
roundButton.addGestureRecognizer(longPressRecognizer)
}
#objc private func tapGesture(recognizer: UITapGestureRecognizer) {
print("tap\n")
}
#objc private func longPressGesture(recognizer: UILongPressGestureRecognizer) {
switch recognizer.state {
case .began:
resetTimerSecsAndLabel()
startTimer()
print("long gesture began\n")
case .ended, .cancelled:
invalidateTimer()
print("long gesture ended or cancelled\n")
case .failed:
print("long gesture failed\n")
default:
break
}
}
}

I think the animation finishing early is an illusion caused by three factors:
You are using CAMediaTimingFunctionName.easeInEaseOut which means the drawing starts slow and ends slow making it hard to judge the real end of drawing.
The drawing finishes by drawing over the start of the line which also makes it hard to see exactly when drawing stops.
Your timer should be subtracting 0.1 from the time before updating the label, because 0.1 has already passed when the timer first updates.
When I changed the timing function to CAMediaTimingFunctionName.linear and fixed the timer, it seemed to always hit 0 when the drawing finished.

Related

Swift: Longpress Button Animation

I have a Button that acts as an SOS Button. I would like to only accept longpresses on that button (something like two seconds long press) and animate the button while pressing to let the user know he has to longpress.
The Button is just a round Button:
let SOSButton = UIButton()
SOSButton.backgroundColor = Colors.errorRed
SOSButton.setImage(UIImage(systemName: "exclamationmark.triangle.fill"), for: .normal)
SOSButton.translatesAutoresizingMaskIntoConstraints = false
SOSButton.tintColor = Colors.justWhite
SOSButton.clipsToBounds = true
SOSButton.layer.cornerRadius = 25
SOSButton.addTarget(self, action: #selector(tappedSOSButton(sender:)), for: .touchUpInside)
SOSButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(SOSButton)
which looks something like this:
Now, when the button is getting long-pressed, I'd like to animate a stroke like a circular progress view. It would start from 0* and fill the whole circle to finally look like this:
I know it looks the same because the background is white, but there is a white stroke around it.
If the user lets go of the button before the circle fills up, it should animate back to zero in the same speed. If the user holds on long enough, only then should the action get executed.
How would I go about designing such a button? I have not found anything I can work off right now. I know I can animate stuff but animating while long-pressing seems like I'd need to implement something very custom.
Interested in hearing ideas.
You can create a custom class of UIView and add layer to it.
class CircularProgressBar: UIView {
private var circularPath: UIBezierPath = UIBezierPath()
var progressLayer: CAShapeLayer!
var progress: Double = 0 {
willSet(newValue) {
progressLayer?.strokeEnd = CGFloat(newValue)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
removeAllSubviewAndSublayers()
setupCircle()
}
private func removeAllSubviewAndSublayers() {
layer.sublayers?.forEach { $0.removeFromSuperlayer() }
subviews.forEach { $0.removeFromSuperview() }
}
func setupCircle() {
let x = self.frame.width / 2
let y = self.frame.height / 2
let center = CGPoint(x: x, y: y)
circularPath = UIBezierPath(arcCenter: center, radius: x, startAngle: -0.5 * CGFloat.pi, endAngle: 1.5 * CGFloat.pi, clockwise: true)
progressLayer = CAShapeLayer()
progressLayer.path = circularPath.cgPath
progressLayer.strokeColor = UIColor.white.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineWidth = x/10
progressLayer.lineCap = .round
progressLayer.strokeEnd = 0
layer.addSublayer(progressLayer)
}
func addStroke(duration: Double = 2.0) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = duration
animation.beginTime = CACurrentMediaTime()
progressLayer.add(animation, forKey: "strokeEnd")
}
func removeStroke(duration: Double = 0.0) {
let revAnimation = CABasicAnimation(keyPath: "strokeEnd")
revAnimation.duration = duration
revAnimation.fromValue = progressLayer.presentation()?.strokeEnd
revAnimation.toValue = 0.0
progressLayer.removeAllAnimations()
progressLayer.add(revAnimation, forKey: "strokeEnd")
}
}
In UIViewController create a UIImageView and CircularProgressBar. Set isUserInteractionEnabled to true of imageView and add progressView to it.
In viewDidLayoutSubviews() method set the frame of progressView equal to bounds of imageView. You also need to set Timer to execute action. Here is the full code.
class ViewController: UIViewController {
let imageView = UIImageView(image: UIImage(named: "icon_sos")!)
let progressView = CircularProgressBar()
var startTime: Date?
var endTime: Date?
var longPress: UILongPressGestureRecognizer?
var timer: Timer?
let longPressDuration: Double = 2.0
override func viewDidLoad() {
super.viewDidLoad()
longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(_:)))
longPress?.minimumPressDuration = 0.01
imageView.frame = CGRect(x: 100, y: 100, width: 30, height: 30)
imageView.isUserInteractionEnabled = true
imageView.addGestureRecognizer(longPress!)
imageView.addSubview(progressView)
self.view.addSubview(imageView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
progressView.frame = imageView.bounds
}
private func setupTimer() {
timer = Timer.scheduledTimer(timeInterval: self.longPressDuration, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: false)
}
#objc private func fireTimer() {
longPress?.isEnabled = false
longPress?.isEnabled = true
progressView.removeStroke()
if let timer = timer {
timer.invalidate()
self.timer = nil
}
// execute button action here
print("Do something")
}
#objc private func longPressAction(_ sender: UILongPressGestureRecognizer) {
if sender.state == .began {
print("Long Press Began: ", Date())
startTime = Date()
self.progressView.addStroke(duration: self.longPressDuration)
setupTimer()
}
if sender.state == .changed {
print("Long Press Changed: ", Date())
}
if sender.state == .cancelled {
print("Long Press Cancelled: ", Date())
endTime = Date()
if let startTime = startTime, let endTime = endTime {
let interval = DateInterval(start: startTime, end: endTime)
progressView.removeStroke(duration: interval.duration.magnitude)
if interval.duration.magnitude < self.longPressDuration {
timer?.invalidate()
timer = nil
}
}
}
if sender.state == .ended {
print("Long Press Ended: ", Date())
endTime = Date()
if let startTime = startTime, let endTime = endTime {
let interval = DateInterval(start: startTime, end: endTime)
progressView.removeStroke(duration: interval.duration.magnitude)
if interval.duration.magnitude < self.longPressDuration {
timer?.invalidate()
timer = nil
}
}
}
}
}

How to make loading animation in iOS Swift?

Hi I want to make animation with 3 UIView. The main problem is I can't start animation once it's stopped by removing animation from layer.
Here is the code:
class HTAnimatedTypingView: UIView {
#IBOutlet weak var view1: UIView!
#IBOutlet weak var view2: UIView!
#IBOutlet weak var view3: UIView!
func startAnimation(){
UIView.animate(withDuration: 0.5, delay: 0, options: .repeat, animations: {
self.view1.frame.origin.y = 0
}, completion: nil)
UIView.animate(withDuration: 0.3, delay: 0.5, options: .repeat, animations: {
self.view2.frame.origin.y = 0
}, completion: nil)
UIView.animate(withDuration: 0.2, delay: 1.0, options: .repeat, animations: {
self.view3.frame.origin.y = 0
}, completion: nil)
}
func stopAnimations(){
self.view1.layer.removeAllAnimations()
self.view2.layer.removeAllAnimations()
self.view3.layer.removeAllAnimations()
}
}
Output of Above Code:
Expected Animation:
How can make it work with start animation & stop animation functionality? Thanks in advance...
Since you need to add some pause in between each sequence of animations, I would personally do it using key frames as it gives you some flexibility:
class AnimationViewController: UIViewController {
private let stackView: UIStackView = {
$0.distribution = .fill
$0.axis = .horizontal
$0.alignment = .center
$0.spacing = 10
return $0
}(UIStackView())
private let circleA = UIView()
private let circleB = UIView()
private let circleC = UIView()
private lazy var circles = [circleA, circleB, circleC]
func animate() {
let jumpDuration: Double = 0.30
let delayDuration: Double = 1.25
let totalDuration: Double = delayDuration + jumpDuration*2
let jumpRelativeDuration: Double = jumpDuration / totalDuration
let jumpRelativeTime: Double = delayDuration / totalDuration
let fallRelativeTime: Double = (delayDuration + jumpDuration) / totalDuration
for (index, circle) in circles.enumerated() {
let delay = jumpDuration*2 * TimeInterval(index) / TimeInterval(circles.count)
UIView.animateKeyframes(withDuration: totalDuration, delay: delay, options: [.repeat], animations: {
UIView.addKeyframe(withRelativeStartTime: jumpRelativeTime, relativeDuration: jumpRelativeDuration) {
circle.frame.origin.y -= 30
}
UIView.addKeyframe(withRelativeStartTime: fallRelativeTime, relativeDuration: jumpRelativeDuration) {
circle.frame.origin.y += 30
}
})
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
circles.forEach {
$0.layer.cornerRadius = 20/2
$0.layer.masksToBounds = true
$0.backgroundColor = .systemBlue
stackView.addArrangedSubview($0)
$0.widthAnchor.constraint(equalToConstant: 20).isActive = true
$0.heightAnchor.constraint(equalTo: $0.widthAnchor).isActive = true
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animate()
}
}
It should be pretty straightforward, but feel free to let me know if you have any questions!
And this is how the result looks like:
One way could be to use a Timer. Keep an instance of Timer in your class. When startAnimation is called, schedule it. When stopAnimation is called, invalidate it. (This means that the currently ongoing animation will be completed before the animation actually stops, which IMO makes it a nice non-abrupt stop).
On each tick of the timer, animate the dots once. Note that the animation you apply on each dot should have the same duration, as in the expected output, they all bounce at the same rate, just at different instants in time.
Some illustrative code:
// startAnimation
timer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true) { _ in
self.animateDotsOnce()
}
// stopAnimation
timer.invalidate()
// animateDotsOnce
UIView.animate(withDuration: animationDuration, delay: 0, animations: {
self.view1.frame.origin.y = animateHeight
}, completion: {
_ in
UIView.animate(withDuration: animationDuration) {
self.view1.frame.origin.y = 0
}
})
// plus the other two views, with different delays...
I'll leave it to you to find a suitable animateHeight, timerInterval, animationDuration and delays for each view.
I'd recommend using a CAKeyframeAnimation instead of handling completion blocks and that sorcery. Here's a quick example:
for i in 0 ..< 3 {
let bubble = UIView(frame: CGRect(x: 20 + i * 20, y: 200, width: 10, height: 10))
bubble.backgroundColor = .red
bubble.layer.cornerRadius = 5
self.view.addSubview(bubble)
let animation = CAKeyframeAnimation()
animation.keyPath = "position.y"
animation.values = [0, 10, 0]
animation.keyTimes = [0, 0.5, 1]
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.duration = 1
animation.isAdditive = true
animation.repeatCount = HUGE
animation.timeOffset = CACurrentMediaTime() + 0.2 * Double(i)
bubble.layer.add(animation, forKey: "anim")
}
When you wanna remove the animation you just use bubble.layer.removeAnimation(forKey: "anim"). You might have to play around with the timing function or values and keyTimes to get the exact movement you want. But keyframes is the way to go to make a specific animation.
Side note: this example won't work in viewDidLoad cause the view doesn't have a superview yet so the animation won't work. If you test it in viewDidAppear it will work.
UIView animation is different to CALayer animation,best not to mix them.
Write locally and tested.
import UIKit
import SnapKit
class HTAnimatedTypingView: UIView {
private let view0 = UIView()
private let view1 = UIView()
private let view2 = UIView()
init() {
super.init(frame: CGRect.zero)
makeUI()
}
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
makeUI()
}
private func makeUI() {
backgroundColor = UIColor.white
view0.backgroundColor = UIColor.red
view1.backgroundColor = UIColor.blue
view2.backgroundColor = UIColor.yellow
addSubview(view0)
addSubview(view1)
addSubview(view2)
view0.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.left.equalTo(self.snp.left)
}
view1.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.centerX.equalTo(self.snp.centerX)
}
view2.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.right.equalTo(self.snp.right)
}
}
public func startAnimation() {
let duration:CFTimeInterval = 0.5
let animation_delay:CFTimeInterval = 0.1
assert(duration >= animation_delay * 5, "animation_delay should be way smaller than duration in order to make animation natural")
let translateAnimation = CABasicAnimation(keyPath: "position.y")
translateAnimation.duration = duration
translateAnimation.repeatCount = Float.infinity
translateAnimation.toValue = 0
translateAnimation.fillMode = CAMediaTimingFillMode.both
translateAnimation.isRemovedOnCompletion = false
translateAnimation.autoreverses = true
view0.layer.add(translateAnimation, forKey: "translation")
DispatchQueue.main.asyncAfter(deadline: .now() + animation_delay) { [unowned self ] in
self.view1.layer.add(translateAnimation, forKey: "translation")
}
DispatchQueue.main.asyncAfter(deadline: .now() + animation_delay * 2) { [unowned self ] in
self.view2.layer.add(translateAnimation, forKey: "translation")
}
}
public func stopAnimation() {
self.view0.layer.removeAllAnimations()
self.view1.layer.removeAllAnimations()
self.view2.layer.removeAllAnimations()
}
}

Wrong reference to custom AVPlayer

I have a UICollectionView with multiple cells (FeedCell). Those cells can also contain a UICollectionView which can also have multiple cells, up to five (MediaSliderCell). Basically, the set up is the same as Instagram: you have a post, and one post can have multiple images or videos.
The problem I am now facing is, sometimes the wrong video is shown in the wrong cell. I use an imageView to show a placeholder when the video is not played yet, this placeholder is hidden when the video starts playing (play button is tapped).
I figured everything goes well when the videos are not playing, but the problem arises when I do play videos. The cells get switched up, meaning the video of MediaSliderCell indexPath.item 5 is shown in MediaSLiderCell indexPath.item 2, for example.
At first, I thought the problem was in the cells, which don't get reused well, but this would also mean the photos could get switched up, which never happens. So I feel the problem is in my AVPlayer, which then uses the wrong reference or wrong URL. Let me demonstrate my code:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "mediaSliderCell", for: indexPath) as! MediaSliderCell
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
if(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia!.count > 0) {
if(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaType == .photo) {
let mediaURL = URL(string: "https://myfileserver.com/media/\(HomeControllerID.userFeed.userFeed[feedCellID!].feedID!)/\(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaURL)")
cell.photoView.kf.setImage(with: mediaURL, options: [.targetCache(mediaCache)])
cell.photoView.isHidden = false
cell.videoView.isHidden = true
} else if(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaType == .video) {
let mediaThumbURL = URL(string: "https://myfileserver.com/media/\(HomeControllerID.userFeed.userFeed[feedCellID!].feedID!)/\(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaThumbURL!)")
let mediaURL = URL(string: "https://myfileserver.com/media/\(HomeControllerID.userFeed.userFeed[feedCellID!].feedID!)/\(HomeControllerID.userFeed.userFeed[feedCellID!].userMedia![indexPath.item].mediaURL)")!
cell.videoView.placeholderView.kf.setImage(with: mediaThumbURL, options: [.targetCache(mediaCache)])
cell.videoView.mediaURL = mediaURL
cell.photoView.isHidden = true
cell.videoView.isHidden = false
}
}
return cell
}
MediaSLiderCell is some pretty basic UICollectionViewCell stuff:
class MediaSliderCell: UICollectionViewCell {
override func prepareForReuse() {
super.prepareForReuse()
photoView.isHidden = false
videoView.isHidden = true
videoView.mediaURL = nil
videoView.placeholderView.kf.cancelDownloadTask()
videoView.placeholderView.image = UIImage()
photoView.image = UIImage()
}
var photoView: UIImageView = {
let photoView = UIImageView()
photoView.translatesAutoresizingMaskIntoConstraints = false
photoView.backgroundColor = .black
photoView.isHidden = true
return photoView
}()
var videoView: VideoView = {
let videoView = VideoView()
videoView.translatesAutoresizingMaskIntoConstraints = false
videoView.backgroundColor = .black
return videoView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
addSubview(photoView)
addSubview(videoView)
photoView.topAnchor.constraint(equalTo: topAnchor).isActive = true
photoView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
photoView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
photoView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
videoView.topAnchor.constraint(equalTo: topAnchor).isActive = true
videoView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
videoView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
videoView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I think all of the above is fine. But then, for the VideoView, I use a custom UIView in which I set up a placeholder (UIImageView). It's a lot of code below, but most if it is controls and user interface stuff. What's important to know is, I use two libraries: Cache and CachingPlayerItem. This is to avoid downloading the same video over and over again, so when the video is not found in cache, I download the item, and when the video is downloaded, I save it in cache to reuse later. All makes sense I guess. I feel the problem lies somewhere in there, or in the AVPlayer itself. Take a look at the code:
class VideoView: UIView, CachingPlayerItemDelegate {
var playerItem: CachingPlayerItem?
func playerItem(_ playerItem: CachingPlayerItem, didFinishDownloadingData data: Data) {
// A track is downloaded. Saving it to the cache asynchronously.
print("Saving video to cache on device")
storage?.async.setObject(data, forKey: mediaURL.absoluteString, completion: { _ in} )
}
var playerLooper: NSObject?
var playerLayer: AVPlayerLayer!
var queuePlayer: AVQueuePlayer?
var mediaURL: URL!
var placeholderView: UIImageView = {
let placeholderView = UIImageView()
placeholderView.translatesAutoresizingMaskIntoConstraints = false
placeholderView.backgroundColor = .black
return placeholderView
}()
var playerView: UIView = {
let playerView = UIView()
playerView.translatesAutoresizingMaskIntoConstraints = false
playerView.backgroundColor = .clear
playerView.isHidden = true
return playerView
}()
var playButton: UIImageView = {
let playButton = UIImageView()
playButton.image = UIImage(named: "playButton")
playButton.translatesAutoresizingMaskIntoConstraints = false
playButton.isUserInteractionEnabled = true
playButton.backgroundColor = .clear
return playButton
}()
var pauseButton: UIImageView = {
let pauseButton = UIImageView()
pauseButton.image = UIImage(named: "pauseButton")
pauseButton.translatesAutoresizingMaskIntoConstraints = false
pauseButton.backgroundColor = .clear
pauseButton.isUserInteractionEnabled = true
pauseButton.alpha = 0
return pauseButton
}()
var volumeOnButton: UIImageView = {
let volumeOnButton = UIImageView()
volumeOnButton.image = UIImage(named: "volumeOn")
volumeOnButton.translatesAutoresizingMaskIntoConstraints = false
volumeOnButton.backgroundColor = .clear
volumeOnButton.isUserInteractionEnabled = true
volumeOnButton.alpha = 0
return volumeOnButton
}()
var volumeOffButton: UIImageView = {
let volumeOffButton = UIImageView()
volumeOffButton.image = UIImage(named: "volumeOff")
volumeOffButton.translatesAutoresizingMaskIntoConstraints = false
volumeOffButton.backgroundColor = .clear
volumeOffButton.isUserInteractionEnabled = true
volumeOffButton.alpha = 0
return volumeOffButton
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.setupViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupViews()
}
func setupViews() {
addSubview(placeholderView)
placeholderView.topAnchor.constraint(equalTo: topAnchor).isActive = true
placeholderView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
placeholderView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
placeholderView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
addSubview(playerView)
playerView.topAnchor.constraint(equalTo: topAnchor).isActive = true
playerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
playerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
playerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
let tapView = UITapGestureRecognizer(target: self, action: #selector(showControls))
playerView.addGestureRecognizer(tapView)
addSubview(playButton)
playButton.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
playButton.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
playButton.heightAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
playButton.widthAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
let tapPlayButton = UITapGestureRecognizer(target: self, action: #selector(playVideo))
playButton.addGestureRecognizer(tapPlayButton)
addSubview(pauseButton)
pauseButton.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
pauseButton.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
pauseButton.heightAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
pauseButton.widthAnchor.constraint(equalToConstant: profilePicWidth * 2).isActive = true
let tapPauseButton = UITapGestureRecognizer(target: self, action: #selector(pauseVideo))
pauseButton.addGestureRecognizer(tapPauseButton)
addSubview(volumeOnButton)
volumeOnButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -(normalSpacing + 2)).isActive = true
volumeOnButton.topAnchor.constraint(equalTo: topAnchor, constant: normalSpacing + 2).isActive = true
volumeOnButton.heightAnchor.constraint(equalToConstant: 32).isActive = true
volumeOnButton.widthAnchor.constraint(equalToConstant: 32).isActive = true
let tapVolumeOnButton = UITapGestureRecognizer(target: self, action: #selector(volumeAction))
volumeOnButton.addGestureRecognizer(tapVolumeOnButton)
addSubview(volumeOffButton)
volumeOffButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -(normalSpacing + 2)).isActive = true
volumeOffButton.topAnchor.constraint(equalTo: topAnchor, constant: normalSpacing + 2).isActive = true
volumeOffButton.heightAnchor.constraint(equalToConstant: 32).isActive = true
volumeOffButton.widthAnchor.constraint(equalToConstant: 32).isActive = true
let tapVolumeOffButton = UITapGestureRecognizer(target: self, action: #selector(volumeAction))
volumeOffButton.addGestureRecognizer(tapVolumeOffButton)
}
#objc func volumeAction() {
buttonTimer?.invalidate()
volumeTimer?.invalidate()
if UserDefaults.exists(key: "volumeOn") {
if(UserDefaults.standard.bool(forKey: "volumeOn") == false) {
self.queuePlayer?.isMuted = false
UserDefaults.standard.set(true, forKey: "volumeOn")
self.volumeOnButton.alpha = 1
self.volumeOffButton.alpha = 0
} else {
self.queuePlayer?.isMuted = true
UserDefaults.standard.set(false, forKey: "volumeOn")
self.volumeOnButton.alpha = 0
self.volumeOffButton.alpha = 1
}
} else {
self.queuePlayer?.isMuted = false
UserDefaults.standard.set(true, forKey: "volumeOn")
self.volumeOnButton.alpha = 1
self.volumeOffButton.alpha = 0
}
volumeTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadeVolumeButton), userInfo: nil, repeats: false)
buttonTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadePauseButton), userInfo: nil, repeats: false)
}
#objc func checkVolume() {
if UserDefaults.exists(key: "volumeOn") {
if(UserDefaults.standard.bool(forKey: "volumeOn") == false) {
self.queuePlayer?.isMuted = true
} else {
self.queuePlayer?.isMuted = false
}
} else {
self.queuePlayer?.isMuted = true
}
}
#objc func showControls() {
buttonTimer?.invalidate()
volumeTimer?.invalidate()
if(self.volumeOnButton.alpha > 0 || self.volumeOffButton.alpha > 0) {
UIView.animate(withDuration: 0.2) {
self.volumeOnButton.alpha = 0
self.volumeOffButton.alpha = 0
}
} else {
if UserDefaults.exists(key: "volumeOn") {
if(UserDefaults.standard.bool(forKey: "volumeOn") == false) {
UIView.animate(withDuration: 0.2) {
self.volumeOffButton.alpha = 1
self.volumeOnButton.alpha = 0
}
} else {
UIView.animate(withDuration: 0.2) {
self.volumeOffButton.alpha = 0
self.volumeOnButton.alpha = 1
}
}
} else {
UIView.animate(withDuration: 0.2) {
self.volumeOffButton.alpha = 1
self.volumeOnButton.alpha = 0
}
}
volumeTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadeVolumeButton), userInfo: nil, repeats: false)
}
if(self.queuePlayer?.timeControlStatus == .playing) {
if(self.pauseButton.alpha > 0) {
UIView.animate(withDuration: 0.2) { self.pauseButton.alpha = 0 }
} else {
UIView.animate(withDuration: 0.2) { self.pauseButton.alpha = 1 }
buttonTimer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(fadePauseButton), userInfo: nil, repeats: false)
}
} else if(self.queuePlayer?.timeControlStatus == .paused) {
if(self.playButton.alpha > 0) {
UIView.animate(withDuration: 0.2) { self.pauseButton.alpha = 0 }
} else {
UIView.animate(withDuration: 0.2) { self.playButton.alpha = 1 }
}
}
}
lazy var storage: Cache.Storage? = {
return try? Storage(diskConfig: diskConfig, memoryConfig: memoryConfig, transformer: TransformerFactory.forData())
}()
func loadVideo() {
// Trying to retrieve a track from cache asynchronously.
storage?.async.entry(forKey: mediaURL.absoluteString, completion: { result in
switch result {
case .error:
// The track is not cached.
print("Downloading from network")
self.playerItem = CachingPlayerItem(url: self.mediaURL)
case .value(let entry):
// The track is cached.
print("Downloading from cached library on device")
self.playerItem = CachingPlayerItem(data: entry.object, mimeType: "video/mp4", fileExtension: "mp4")
}
self.playerItem?.delegate = self
DispatchQueue.main.async {
if let playerItem = self.playerItem {
self.queuePlayer = AVQueuePlayer(items: [playerItem])
self.queuePlayer?.automaticallyWaitsToMinimizeStalling = false
self.playerLayer = AVPlayerLayer(player: self.queuePlayer)
self.playerLooper = AVPlayerLooper(player: self.queuePlayer!, templateItem: playerItem)
self.playerView.layer.addSublayer(self.playerLayer!)
self.playerLayer?.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 1.25)
self.checkVolume()
do {
try AVAudioSession.sharedInstance().setCategory(.playback)
} catch(let error) {
print(error.localizedDescription)
}
self.queuePlayer?.play()
self.queuePlayer?.addObserver(self, forKeyPath: "timeControlStatus", options: .initial, context:nil)
}
}
})
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if(keyPath == "timeControlStatus") {
if(self.playerView.isHidden && self.queuePlayer?.timeControlStatus == .playing) {
self.playerView.isHidden = false
}
}
}
var buttonTimer: Timer?
var volumeTimer: Timer?
#objc func playVideo() {
buttonTimer?.invalidate()
volumeTimer?.invalidate()
if(self.queuePlayer?.currentItem == nil) {
self.loadVideo()
}
if(self.queuePlayer?.timeControlStatus == .paused) {
do {
try AVAudioSession.sharedInstance().setCategory(.playback)
} catch(let error) {
print(error.localizedDescription)
}
self.queuePlayer?.play()
}
self.playButton.alpha = 0
buttonTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(fadePauseButton), userInfo: nil, repeats: false)
volumeTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(fadeVolumeButton), userInfo: nil, repeats: false)
}
#objc func pauseVideo() {
buttonTimer?.invalidate()
self.queuePlayer?.pause()
self.playButton.alpha = 1
self.pauseButton.alpha = 0
self.volumeOnButton.alpha = 0
self.volumeOffButton.alpha = 0
}
#objc func fadePauseButton() {
UIView.animate(withDuration: 0.8) {
self.pauseButton.alpha = 0
}
}
#objc func fadeVolumeButton() {
UIView.animate(withDuration: 0.8) {
self.volumeOnButton.alpha = 0
self.volumeOffButton.alpha = 0
}
}
}
In my opinion, the problem should be somewhere above in loadVideo(). However, I provide all my code of the custom UIView VideoView to avoid any mistakes or other important parts. Anybody who can help me? Would help me out a big deal, because I've been looking into this for weeks now. I have tried every suggestion for dequeuing, I have tried everything I could for reusing cells, ... and it all didn't work. So I guess the solution should be found in the AVPlayer, the Cache library or the CachingPlayerItem library. Would appreciate any help or suggestions. Thank you in advance.
For the record, something I forgot but should be clear from the code: the AVPlayer is not shown and thus loadVideo() is not called initially. Only when the user taps the play button (playVideo()). Everything goes well if I don't play video too. Photos are shown correctly, the placeholders are shown correctly, but the videos get mixed up after I start playing one or more videos (tap the play button, which calls playVideo(), which calls loadVideo()).
EDIT: Ok so I figured something out. It seems like the video itself is not changed when the cell gets reused. What I mean is, in loadVideo(), the AVQueuePlayer() gets set, and this is what changes the video shown and played afterwards. I feel like I need to set this block:
// Trying to retrieve a track from cache asynchronously.
storage?.async.entry(forKey: mediaURL.absoluteString, completion: { result in
switch result {
case .error:
// The track is not cached.
print("Downloading from network")
DispatchQueue.main.async {
self.playerItem = CachingPlayerItem(url: self.mediaURL)
}
case .value(let entry):
// The track is cached.
print("Downloading from cached library on device")
DispatchQueue.main.async {
self.playerItem = CachingPlayerItem(data: entry.object, mimeType: "video/mp4", fileExtension: "mp4")
}
}
self.playerItem?.delegate = self
DispatchQueue.main.async {
if let playerItem = self.playerItem {
self.queuePlayer = AVQueuePlayer(items: [playerItem])
self.queuePlayer?.automaticallyWaitsToMinimizeStalling = false
self.playerLayer = AVPlayerLayer(player: self.queuePlayer)
self.playerLooper = AVPlayerLooper(player: self.queuePlayer!, templateItem: playerItem)
self.playerView.layer.addSublayer(self.playerLayer!)
self.playerLayer?.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 1.25)
self.checkVolume()
do {
try AVAudioSession.sharedInstance().setCategory(.playback)
} catch(let error) {
print(error.localizedDescription)
}
self.queuePlayer?.addObserver(self, forKeyPath: "timeControlStatus", options: .initial, context:nil)
}
}
})
somewhere else, before the video gets played at all. Because this only gets called when the play button is tapped, but then the wrong video is shown. Have tried calling loadVideo() directly in the cell, but doesn't seem to fix the problem. I feel like I am getting closer tho. Any thoughts?
Don't store AVQueuePlayer inside tableView cells. Remember that you have upper limit on the amount of AVPlayer instances you can keep.
Move players it-selfs, their configurations, starting, pausing, whatever else action you have into UIViewController what store your UITableView.
When User will press play on UITableViewCell, you will tell the UIViewController thru delegate to start playing and UIViewController will do something like that:
Cancel previous playing
Populate AVQueuePlayer with player items
Insert AVQueuePlayer into UITableViewCell
Start playing
Don't forget to remove AVQueuePlayer from UITableViewCell when you reuse cell

Button stops moving when creating new button who moves

I'm creating a game where a button who is being created moves from one side of the screen to the other when I click a button called start. The problem is that when I click start before the button who was moving reaches its end point, it stops instead of continuing (and the another created button start moving like expected).
Should I create a new CADisplayLink every time I click the start button? If so, how would I do that? Here's the code:
var button1 = UIButton()
var displayLink: CADisplayLink?
var startTime: CFAbsoluteTime?
let duration = 2.0
var leadingConstraint: NSLayoutConstraint!
var topConstraint: NSLayoutConstraint!
var l1 = false
#IBAction func start(sender: UIButton) {
n1()
}
func n1() {
l1 = false
startTime = CFAbsoluteTimeGetCurrent()
displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
func handleDisplayLink(displayLink: CADisplayLink) {
if l1 == false { // so it doesn't randomize leading constraint twice
button1 = createButton()
let randomNumber = Int(arc4random_uniform(180) + 30)
let elapsed = CFAbsoluteTimeGetCurrent() - startTime!
var percentComplete = CGFloat(elapsed / duration)
if percentComplete >= 1.0 {
percentComplete = 1.0
// self.button1.removeFromSuperview()
displayLink.invalidate()
button1.hidden = true
}
leadingConstraint.constant = CGFloat(randomNumber)
topConstraint.constant = 390 - 350 * percentComplete
NSLayoutConstraint.activateConstraints([
leadingConstraint,
topConstraint,
button1.widthAnchor.constraintEqualToConstant(75),
button1.heightAnchor.constraintEqualToConstant(75)
])
l1 = true
}
else{
let elapsed = CFAbsoluteTimeGetCurrent() - startTime!
var percentComplete = CGFloat(elapsed / duration)
if percentComplete >= 1.0 {
percentComplete = 1.0
displayLink.invalidate()
button1.hidden = true
}
topConstraint.constant = 390 - 350 * percentComplete
NSLayoutConstraint.activateConstraints([
leadingConstraint,
topConstraint,
button1.widthAnchor.constraintEqualToConstant(75),
button1.heightAnchor.constraintEqualToConstant(75)
])
}
}
func buttonPressed(sender: UIButton!) {
button1.hidden = true
displayLink?.invalidate()
}
func createButton() ->UIButton {
let button = UIButton()
button.setImage(UIImage(named: "BlueBall.png")!, forState: UIControlState.Normal)
button.addTarget(self, action: "buttonPressed:", forControlEvents: UIControlEvents.TouchUpInside)
self.view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
leadingConstraint = button.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor, constant: 0)
topConstraint = button.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: 0)
NSLayoutConstraint.activateConstraints([
leadingConstraint,
topConstraint,
button.widthAnchor.constraintEqualToConstant(75),
button.heightAnchor.constraintEqualToConstant(75)
])
return button
}
Please help. It would be really appreciated. Thanks in advance. Anton
Okay Anton O; as discussed I post an answer how to detect a touch on a moving UIView. This works for both, CAAnimation and UIView.animationWith..
First I created an extension of CGRect, just for convenience:
extension CGRect {
init(position: CGPoint, size: CGSize) {
self.origin = CGPoint(x: position.x - (size.width / 2.0), y: position.y - (size.height / 2.0))
self.size = size
}
}
Then I created two methods which create and move the view. You can adapt the code then to your needs. (I hold a global variable called nextView to keep reference to the view, can also be extended to an array of course)
Create View:
private func createView(index: Int) {
nextView?.removeFromSuperview()
nextView = UIView()
nextView?.bounds = CGRect(x: 0, y: 0, width: 60, height: 60)
nextView?.backgroundColor = UIColor.redColor()
nextView?.center = CGPoint(x: 30, y: CGRectGetMidY(self.view.bounds))
if let nextView = nextView {
view.addSubview(nextView)
}
}
Move View:
private func moveView() {
guard let nextView = nextView else {
return
}
UIView.animateWithDuration(5.0) { () -> Void in
nextView.center = CGPoint(x: CGRectGetMaxX(self.view.bounds) + 30, y: CGRectGetMidY(self.view.bounds))
}
}
Detect Touch:
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesEnded(touches, withEvent: event)
if let touch = touches.first, nextView = nextView {
let touchRect = CGRect(position: touch.locationInView(self.view), size: CGSize(width: 20, height: 20))
guard let viewPosition = nextView.layer.presentationLayer()?.position else {
return
}
let viewRect = CGRect(position: viewPosition, size: nextView.bounds.size)
if CGRectIntersectsRect(touchRect, viewRect) {
print("👍 💯")
} else {
print("👎")
}
}
}
You can extend the methods for your needs and also add some "performance" enhancing checks (like if a view is visible and move on or return right there in the touchesEnded method, etc.)

CABasicAnimation responsiveness

I have a custom UIActivityIndicatorView. It is a view, with a CAAnimation on its layer. The problem is the following:
I do some heavy work and create a lot of views. It takes approximately 0.5 seconds. In order for it to be smooth I decided to use activity indicator, while it "happens". It was all fine with default activity indicator, however with the one that I wrote I get unexpected results.
So, when the view loads I launch my activity indicator, which starts animating. When heavy duty work starts my view freezes for 0.5 seconds and when it's done I stop animating it and it disappears. This "freeze" looks very unpleasant to an eye. Because the idea was to keep animating while other views get initialized and added as subviews(although hidden).
I suspect that the problem is that my "activity indicator" is not asynchronous or simply was not coded right.
Here is the code for it:
class CustomActivityIndicatorView: UIView {
// MARK - Variables
var colors = [UIColor.greenColor(),UIColor.grayColor(),UIColor.blueColor(),UIColor.redColor()]
var colorIndex = 0
var animation: CABasicAnimation!
lazy var customView : UIView! = {
let frame : CGRect = CGRectMake(0.0, 0.0, 100, 100)
let view = UIView(frame: frame)
image.frame = frame
image.center = view.center
view.backgroundColor = UIColor.greenColor()
view.clipsToBounds = true
view.layer.cornerRadius = frame.width/2
return view
}()
var isAnimating : Bool = false
var hidesWhenStopped : Bool = true
var from: NSNumber = 1.0
var to: NSNumber = 0.0
var growing = false
override func animationDidStart(anim: CAAnimation!) {
}
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
growing = !growing
if growing {
colorIndex++
if colorIndex == colors.count {
colorIndex = 0
}
println(colorIndex)
customView.backgroundColor = colors[colorIndex]
from = 0.0
to = 1.0
} else {
from = 1.0
to = 0.0
}
if isAnimating {
addPulsing()
resume()
}
}
// MARK - Init
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addSubview(customView)
addPulsing()
pause()
self.hidden = true
}
// MARK - Func
func addPulsing() {
let pulsing : CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
pulsing.duration = 0.4
pulsing.removedOnCompletion = false
pulsing.fillMode = kCAFillModeForwards
pulsing.fromValue = from
pulsing.toValue = to
pulsing.delegate = self
let layer = customView.layer
layer.addAnimation(pulsing, forKey: "pulsing")
}
func pause() {
let layer = customView.layer
let pausedTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
isAnimating = false
}
func resume() {
let layer = customView.layer
let pausedTime : CFTimeInterval = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime
layer.beginTime = timeSincePause
isAnimating = true
}
func startAnimating () {
if isAnimating {
return
}
if hidesWhenStopped {
self.hidden = false
}
resume()
}
func stopAnimating () {
let layer = customView.layer
if hidesWhenStopped {
self.hidden = true
}
pause()
layer.removeAllAnimations()
}
deinit {
println("Spinner Deinitied")
}
}
Regarding animationDidStop method:
The idea is the following. The view pulsates, and after it has shrunk, it starts growing again and the background color is changed.
Any idea on what I'm doing wrong?
Solved it using CAKeyFrameAnimation to achieve the same effect. For everybody with the same problem, remember that animationDidStart and animationDidStop start running on the main thread, so that whatever you do with your animation there will be halted if the main thread is busy.

Resources