Persisting CABasicAnimation - swift

I'm using an extension I found online to persist a CABasicAnimation that I'm using for my app; the code for that is below. It works and the animation does persist in the sense that the animation layer is not totally removed from the screen, but the issue I'm having is that if the timer is running and the countdown animation has begun and the user leaves the app at 10 seconds lets say and enters back at 15 seconds, the animation continues from 10 seconds but the actual count is ahead.
public class LayerPersistentHelper {
private var persistentAnimations: [String: CAAnimation] = [:]
private var persistentSpeed: Float = 0.0
private weak var layer: CALayer?
public init(with layer: CALayer) {
self.layer = layer
addNotificationObservers()
}
deinit {
removeNotificationObservers()
}}
private extension LayerPersistentHelper {
func addNotificationObservers() {
let center = NotificationCenter.default
let enterForeground = UIApplication.willEnterForegroundNotification
let enterBackground = UIApplication.didEnterBackgroundNotification
center.addObserver(self, selector: #selector(didBecomeActive), name: enterForeground, object: nil)
center.addObserver(self, selector: #selector(willResignActive), name: enterBackground, object: nil)
}
func removeNotificationObservers() {
NotificationCenter.default.removeObserver(self)
}
func persistAnimations(with keys: [String]?) {
guard let layer = self.layer else { return }
keys?.forEach { (key) in
if let animation = layer.animation(forKey: key) {
persistentAnimations[key] = animation
}
}
}
func restoreAnimations(with keys: [String]?) {
guard let layer = self.layer else { return }
keys?.forEach { (key) in
if let animation = persistentAnimations[key] {
layer.add(animation, forKey: key)
}
}
}}
#objc extension LayerPersistentHelper {
func didBecomeActive() {
guard let layer = self.layer else { return }
restoreAnimations(with: Array(persistentAnimations.keys))
persistentAnimations.removeAll()
if persistentSpeed == 1.0 { // if layer was playing before background, resume it
layer.resumeAnimations()
}
}
func willResignActive() {
guard let layer = self.layer else { return }
persistentSpeed = layer.speed
layer.speed = 1.0 // in case layer was paused from outside, set speed to 1.0 to get all animations
persistAnimations(with: layer.animationKeys())
layer.speed = persistentSpeed // restore original speed
layer.pauseAnimations()
}}
public extension CALayer {
var isAnimationsPaused: Bool {
return speed == 0.0
}
static var timeElapsed: Double = 0
func pauseAnimations() {
if !isAnimationsPaused {
let currentTime = CACurrentMediaTime()
let pausedTime = convertTime(currentTime, from: nil)
speed = 0.0
timeOffset = pausedTime
}
}
func resumeAnimations() {
let pausedTime = timeOffset
speed = 1.0
timeOffset = 0.0
beginTime = 0.0
let currentTime = CACurrentMediaTime()
let timeSincePause = convertTime(currentTime, from: nil) - pausedTime
beginTime = timeSincePause
}}
extension CALayer. {
static private var persistentHelperKey = "progressAnim"
func makeAnimationsPersistent() {
var object = objc_getAssociatedObject(self, &CALayer.persistentHelperKey)
if object == nil {
object = LayerPersistentHelper(with: self)
let nonatomic = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(self, &CALayer.persistentHelperKey, object, nonatomic)
}
}}
Any Ideas?

If anyone has this issue I found that setting the begin time equal to
beginTime = convertTime(timeOffset, from: nil)
does the trick

Related

How to prevent periodicTimeObserver updating the progressBar while user drags the slider manually?

I have a periodicTimeObserver and it updates the elapsed and remaining timeLabels in the way I want, but the slider is jumping. How to prevent periodicTimeObserver updating the UISlider while user drags the slider manually?
This is my UISlider
private lazy var progressBar: UISlider = {
let v = UISlider()
v.translatesAutoresizingMaskIntoConstraints = false
//v.minimumTrackTintColor = UIColor(named: "PlayerColors")
v.isContinuous = false
return v
}()
Periodic time observer which updates the UISlider and the elapsed and remaining time labels.
player = AVPlayer(playerItem: playerItem)
player!.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if self.player!.currentItem?.status == .readyToPlay {
let currentTime : Float64 = CMTimeGetSeconds(self.player!.currentTime());
let totalTime : Float64 = CMTimeGetSeconds(self.player!.currentItem!.duration);
self.progressBar.value = Float(currentTime)
self.progressBar.minimumValue = 0
self.progressBar.maximumValue = Float(totalTime)
self.elapsedTimeLabel.text = self.stringFromTimeInterval(interval: currentTime)
self.remainingTimeLabel.text = self.stringFromTimeIntervalRemaining(interval: totalTime - currentTime)
The function that should seek to a point of the audio and update the time labels.
#objc func progressScrubbed(_ :UISlider) {
let seconds : Int64 = Int64(self.progressBar.value)
let targetTime:CMTime = CMTimeMake(value: seconds, timescale: 1)
player!.seek(to: targetTime)
if player!.rate == 0
{
play()
}
}
You need to know if user is interacting with a slider in order to ignore PeriodicTimeObserver. Moreover you need to reset PeriodicTimeObserver on each seek. So let's create a custom UISlider and override a one method:
class MySlider: UISlider {
var onTouchesBegan: (() -> ())?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
onTouchesBegan?()
}
}
Now you can create a parameter which will track if slider is touched or not and set it in closures of MySlider:
private var isTouchingSlider: Bool = false
private lazy var progressBar: MySlider = {
let v = MySlider()
v.translatesAutoresizingMaskIntoConstraints = false
v.isContinuous = false
v.onTouchesBegan = { [weak self] in
self?.isTouchingSlider = true
}
return v
}()
And your periodic observer methods would look like this:
var periodicObserverToken: Any?
func addPeriodicTimeObserver() {
let interval = CMTime(
seconds: 1,
preferredTimescale: CMTimeScale(NSEC_PER_SEC)
)
periodicObserverToken = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ in
guard let `self` = self, let player = self.player, player.currentItem?.status == .readyToPlay, let currentItem = self.player.currentItem, !self.isTouchingSlider else { return }
let currentTime : Float64 = CMTimeGetSeconds(player.currentTime());
let totalTime : Float64 = CMTimeGetSeconds(currentItem.duration);
self.progressBar.value = Float(currentTime)
self.progressBar.minimumValue = 0
self.progressBar.maximumValue = Float(totalTime)
}
}
private func removePeriodicTimeObserver() {
guard let periodicObserverToken = periodicObserverToken else { return }
player?.removeTimeObserver(periodicObserverToken)
self.playerPeriodicTimeObserver = nil
}
You need to make all the necessary updates when slider is updated:
#objc func progressScrubbed(_ :UISlider) {
let seconds : Int64 = Int64(self.progressBar.value)
let targetTime:CMTime = CMTimeMake(value: seconds, timescale: 1)
removePeriodicTimeObserver()
isTouchingSlider = false
player!.seek(to: targetTime)
addPeriodicTimeObserver()
if player!.rate == 0 {
play()
}
}

SwiftUI UIViewRepresentable AVPlayer crashing due to "periodTimeObserver"

I have a SwiftUI application which has a carousel of videos. I'm using an AVPlayer with UIViewRepresentable and I'm creating the carousel with a ForEach loop of my custom UIViewRepresentable view. I want to have a "periodicTimeObserver" on the active AVPlayer, but it crashes and says
"An instance of AVPlayer cannot remove a time observer that was added
by a different instance of AVPlayer SwiftUI"
My question is how can I remove the periodicTimeObserver of an AVPlayer inside of a UIViewRepresentable inside of a UIView, without causing the app to crash?
Here is my code:
ForEach(videosArray.indices, id: \.self) { i in
let videoURL = videosArray[i]
ZStack {
VStack {
VideoView.init(viewModel: viewModel, videoURL: URL(string: videoURL)!, videoIndex: i)
}
}
}
struct VideoView: UIViewRepresentable {
#ObservedObject var viewModel = viewModel.init()
var videoURL:URL
var previewLength:Double?
var videoIndex: Int
func makeUIView(context: Context) -> UIView {
return PlayerView.init(frame: .zero, url: videoURL, previewLength: previewLength ?? 6)
}
func updateUIView(_ uiView: UIView, context: Context) {
if videoIndex == viewModel.currentIndexSelected {
if let playerView = uiView as? PlayerView {
if !viewModel.isPlaying {
playerView.pause()
} else {
playerView.play(customStartTime: viewModel.newStartTime, customEndTime: viewModel.newEndTime)
}
}
} else {
if let playerView = uiView as? PlayerView {
playerView.pause()
}
}
}
}
public class ViewModel: ObservableObject {
#Published public var currentIndexSelected: Int = 0
#Published public var isPlaying: Bool = true
#Published public var newStartTime = 0.0
#Published public var newEndTime = 30.0
}
class PlayerView: UIView {
private let playerLayer = AVPlayerLayer()
private var previewTimer:Timer?
var previewLength:Double
var player: AVPlayer?
var timeObserver: Any? = nil
init(frame: CGRect, url: URL, previewLength:Double) {
self.previewLength = previewLength
super.init(frame: frame)
player = AVPlayer(url: url)
player!.volume = 0
player!.play()
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
playerLayer.backgroundColor = UIColor.black.cgColor
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
self.previewLength = 15
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
func pause() {
if let timeObserver = timeObserver {
self.player?.removeTimeObserver(timeObserver)
self.timeObserver = nil
}
player?.pause()
}
#objc func replayFinishedItem(noti: NSNotification) {
print("REPLAY FINISHED NOTIIIII: \(noti)")
if let timeDict = noti.object as? [String: Any], let startTime = timeDict["startTime"] as? Double, let endTime = timeDict["endTime"] as? Double/*, let player = timeDict["player"] as? AVPlayer, let observer = timeDict["timeObserver"]*/ {
self.removeTheTimeObserver()
self.play(customStartTime: startTime, customEndTime: endTime)
}
}
#objc func removeTheTimeObserver() {
print("ATTEMPT TO REMOVE IT!")
if let timeObserver = timeObserver {
self.player?.removeTimeObserver(timeObserver)
self.timeObserver = nil
}
}
func play(at playPosition: Double = 0.0, customStartTime: Double = 0.0, customEndTime: Double = 15.0) {
var startTime = customStartTime
var endTime = customEndTime
if customStartTime > customEndTime {
startTime = customEndTime
endTime = customStartTime
}
if playPosition != 0.0 {
player?.seek(to: CMTime(seconds: playPosition, preferredTimescale: CMTimeScale(1)))
} else {
player?.seek(to: CMTime(seconds: startTime, preferredTimescale: CMTimeScale(1)))
}
player?.play()
var timeDict: [String: Any] = ["startTime": startTime, "endTime": endTime]
NotificationCenter.default.addObserver(self, selector: #selector(self.replayFinishedItem(noti:)), name: .customAVPlayerShouldReplayNotification, object: nil)
self.timeObserver = self.player?.addPeriodicTimeObserver(forInterval: CMTime.init(value: 1, timescale: 100), queue: DispatchQueue.main, using: { [weak self] time in
guard let strongSelf = self else {
return
}
let currentTime = CMTimeGetSeconds(strongSelf.player!.currentTime())
let currentTimeStr = String(currentTime)
if let currentTimeDouble = Double(currentTimeStr) {
let userDefaults = UserDefaults.standard
userDefaults.set(currentTimeDouble, forKey: "currentTimeDouble")
NotificationCenter.default.post(name: .currentTimeDouble, object: currentTimeDouble)
if currentTimeDouble >= endTime {
if let timeObserver = strongSelf.timeObserver {
strongSelf.player?.removeTimeObserver(timeObserver)
strongSelf.timeObserver = nil
}
strongSelf.player?.pause()
NotificationCenter.default.post(name: .customAVPlayerShouldReplayNotification, object: timeDict)
} else if let currentItem = strongSelf.player?.currentItem {
let seconds = currentItem.duration.seconds
if currentTimeDouble >= seconds {
if let timeObserver = strongSelf.timeObserver {
strongSelf.player?.removeTimeObserver(timeObserver)
strongSelf.timeObserver = nil
}
NotificationCenter.default.post(name: .customAVPlayerShouldReplayNotification, object: timeDict)
}
}
}
})
}
}

Swift: Notification permissions pop up affecting class functionality

This is a hard one to explain as I'm not entirely sure what's going wrong here. In my AppDelegate I add an array of services to the app:
override var services: [ApplicationService] {
if UIApplication.isRunningUnitTests() {
return []
}
return [
SettingsService(),
TrackingApplicationService(),
NotificationApplicationService(),
AudioApplicationService(),
ConnectionService()
]
}
The connection service is the one I am interested in here. It looks like this:
import PluggableAppDelegate
import Connectivity
final class ConnectionService: NSObject, ApplicationService {
// MARK: - Helpers
private struct Constants {
static let containerHeight: CGFloat = 20
static let containerPadding: CGFloat = 20
static let statusCenterAdjustment: CGFloat = -1
static let statusFont: UIFont = .systemFont(ofSize: 15, weight: .semibold)
static let labelLines = 1
static let radiusDenominator: CGFloat = 2.0
static let animationDuration = 0.2
static let defaultAnchorConstant: CGFloat = 0
}
internal enum ConnectionStatus {
case connected
case disconnected
}
// MARK: - Properties
private let connectivity = Connectivity()
private let queue = DispatchQueue.global(qos: .utility)
internal var currentStatus = ConnectionStatus.connected
private var statusTopAnchor: NSLayoutConstraint?
private lazy var statusContainer: UIView = {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
container.backgroundColor = UIColor.red
container.layer.cornerRadius = Constants.containerHeight / Constants.radiusDenominator
container.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner]
container.isHidden = true
container.addSubview(statusLabel)
NSLayoutConstraint.activate([
statusLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor, constant: Constants.statusCenterAdjustment),
statusLabel.leftAnchor.constraint(equalTo: container.leftAnchor, constant: Constants.containerPadding),
statusLabel.rightAnchor.constraint(equalTo: container.rightAnchor, constant: -Constants.containerPadding)
])
return container
}()
private lazy var statusLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = Constants.labelLines
label.font = Constants.statusFont
label.textAlignment = .center
label.textColor = .white
label.text = Strings.common.user.noInternetConnection.localized
return label
}()
// MARK: - Lifecycle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
DispatchQueue.main.async { [weak self] in
self?.prepareUI()
}
prepareConnectivityListener()
updateConnectionStatus(connectivity.status)
return true
}
// MARK: - Connectivity
func updateConnectionStatus(_ status: Connectivity.Status) {
switch status {
case .connected, .connectedViaWiFi, .connectedViaCellular:
updateLabelWith(connectionStatus: .connected)
case .notConnected, .connectedViaWiFiWithoutInternet, .connectedViaCellularWithoutInternet:
updateLabelWith(connectionStatus: .disconnected)
}
NotificationCenter.default.post(name: .connectivityStatusChanged, object: self.currentStatus)
}
// MARK: - Configuration
private func prepareUI() {
guard let window = UIApplication.shared.keyWindow else {
assertionFailure("We require a Window")
return // Don't setup in release if we hit this, no point
}
window.addSubview(statusContainer)
statusTopAnchor = statusContainer.topAnchor.constraint(equalTo: window.topAnchor, constant: -Constants.containerHeight)
guard let statusTopAnchor = statusTopAnchor else {
assertionFailure("Top anchor could not be build")
return
}
NSLayoutConstraint.activate([
statusTopAnchor,
statusContainer.centerXAnchor.constraint(equalTo: window.centerXAnchor),
statusContainer.heightAnchor.constraint(equalToConstant: Constants.containerHeight)
])
}
private func prepareConnectivityListener() {
let connectivityChanged: (Connectivity) -> Void = { [weak self] connectivity in
self?.updateConnectionStatus(connectivity.status)
}
connectivity.whenConnected = connectivityChanged
connectivity.whenDisconnected = connectivityChanged
connectivity.startNotifier(queue: queue)
}
private func updateLabelWith(connectionStatus: ConnectionStatus) {
if currentStatus == connectionStatus {
return // No need to update if we are the same state
} else {
currentStatus = connectionStatus
}
let topAnchorConstant: CGFloat = (connectionStatus == .connected) ? -Constants.containerHeight : Constants.defaultAnchorConstant
DispatchQueue.main.async { [weak self] in
DispatchQueue.main.async { [weak self] in
if connectionStatus != .connected {
self?.statusContainer.isHidden = false
}
self?.statusTopAnchor?.constant = topAnchorConstant
UIView.animate(withDuration: Constants.animationDuration,
animations: { [weak self] in
self?.statusContainer.superview?.layoutIfNeeded()
},
completion: { _ in
if connectionStatus == .connected {
self?.statusContainer.isHidden = true
}
})
}
}
}
}
So I'm using this service to check the connection status of the device and on the login view controller I display different warnings depending on the connection status.
However, my problem is is that the connection service is not functioning properly when the permissions pop up appears. As you can see from the ConnectionService class, an alert should be generated when the app is offline. Now because I am adding this connection service before the permissions are requested, the window briefly shows the alert, but as soon as the permissions alert appears, it disappears.
Furthermore, you can see in ConnectionService.swift I have a listener set up:
NotificationCenter.default.post(name: .connectivityStatusChanged, object: self.currentStatus)
I'm listening for this in the subsequent viewController which triggers this method:
#objc private func connectivityChanged(notification: Notification) {
if notification.object as? ConnectionService.ConnectionStatus == .connected {
deviceConnected = true
} else {
deviceConnected = false
}
}
This in turn triggers a UI change:
private var deviceConnected: Bool = true {
willSet {
DispatchQueue.main.async {
if newValue == true {
self.tabBarItem.image = #imageLiteral(resourceName: "retrieve_completed")
self.errorLabel?.isHidden = true
} else {
self.tabBarItem.image = #imageLiteral(resourceName: "retrieve_offline")
self.errorLabel?.isHidden = false
self.errorLabel?.text = Strings.eHandshake.warning.deviceOffline.title.localized
}
}
}
}
When permissions have already been granted and therefore the app launches without requesting permission, this works fine, but when the permissions alert appears, this seems to break this part of the logic.
To be clear the services var is initially declared here:
public protocol ApplicationService: UIApplicationDelegate {}
extension ApplicationService {
public var window: UIWindow? {
return UIApplication.shared.delegate?.window ?? nil
}
}
open class PluggableApplicationDelegate: UIResponder,
UIApplicationDelegate {
public var window: UIWindow?
open var services: [ApplicationService] { return [] }
internal lazy var _services: [ApplicationService] = {
return self.services
}()
#discardableResult
internal func apply<T, S>(_ work: (ApplicationService, #escaping (T) -> Void) -> S?, completionHandler: #escaping ([T]) -> Swift.Void) -> [S] {
let dispatchGroup = DispatchGroup()
var results: [T] = []
var returns: [S] = []
for service in _services {
dispatchGroup.enter()
let returned = work(service, { result in
results.append(result)
dispatchGroup.leave()
})
if let returned = returned {
returns.append(returned)
} else { // delegate doesn't implement method
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
completionHandler(results)
}
return returns
}
}
Any help with this would be much appreciated.

Swift animated circular progress bar

I have created a circular progress bar in Swift that animated over 1.5 seconds to value 1 when user hold on view. But I want to add a new viewcontroller when animation is done and restart my circular progressbar if user ended to early. Can someone help me?
Circulars progress bar is working with animation when user hold on view and stop at release.
class CounterView: UIView {
var bgPath: UIBezierPath!
var shapeLayer: CAShapeLayer!
var progressLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
bgPath = UIBezierPath()
self.simpleShape()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
bgPath = UIBezierPath()
self.simpleShape()
}
func simpleShape()
{
createCirclePath()
shapeLayer = CAShapeLayer()
shapeLayer.path = bgPath.cgPath
shapeLayer.lineWidth = 5
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.clear.cgColor
progressLayer = CAShapeLayer()
progressLayer.path = bgPath.cgPath
progressLayer.lineCap = kCALineCapRound
progressLayer.lineWidth = 5
progressLayer.fillColor = nil
progressLayer.strokeColor = UIColor.yellow.cgColor
progressLayer.strokeEnd = 0.0
self.layer.addSublayer(shapeLayer)
self.layer.addSublayer(progressLayer)
}
private func createCirclePath()
{
let x = self.frame.width/2
let y = self.frame.height/2
let center = CGPoint(x: x, y: y)
print(x,y,center)
bgPath.addArc(withCenter: center, radius: x/CGFloat(2), startAngle: CGFloat(0), endAngle: CGFloat(6.28), clockwise: true)
bgPath.close()
}
var animationCompletedCallback: ((_ isAnimationCompleted: Bool) -> Void)?
func setProgressWithAnimation(duration: TimeInterval, value: Float) {
CATransaction.setCompletionBlock {
if let callBack = self.animationCompletedCallback { callBack(true) }
}
CATransaction.begin()
let animation = CABasicAnimation (keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = value
animation.repeatCount = 1
animation.timingFunction = CAMediaTimingFunction (name: kCAMediaTimingFunctionLinear)
progressLayer.strokeEnd = CGFloat(value)
progressLayer.add(animation, forKey: "animateprogress")
CATransaction.commit()
}
func removeLayers() {
shapeLayer.removeAllAnimations()
shapeLayer.removeFromSuperlayer()
progressLayer.removeAllAnimations()
progressLayer.removeFromSuperlayer()
}
}
class ViewController: UIViewController {
#IBOutlet weak var counterView: CounterView!
#IBOutlet weak var holdView: UIView!
var isAnimationCompleted = false
override func viewDidLoad() {
super.viewDidLoad()
addLongPressGesture()
addCounterViewCallback()
}
#objc func longPress(gesture: UILongPressGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.began {
// self.counterView.simpleShape()
self.counterView.setProgressWithAnimation(duration: 1.5, value: 1.0)
}
if gesture.state == UIGestureRecognizerState.ended {
if !isAnimationCompleted {
self.counterView.removeLayers()
}
}
}
func addLongPressGesture(){
let lpgr = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gesture:)))
lpgr.minimumPressDuration = 0
self.holdView.addGestureRecognizer(lpgr)
}
private func addCounterViewCallback() {
counterView.animationCompletedCallback = { [weak self] (isCompleted) in
guard let weakSelf = self else {return}
weakSelf.isAnimationCompleted = isCompleted
weakSelf.addFlashView()
}
}
func addFlashView(){
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let resultViewController = storyBoard.instantiateViewController(withIdentifier: "ResultView") as! Flash
self.present(resultViewController, animated:true, completion:nil)
}
Add new viewcontroller when animation is done and restart animation if user release view and hold on it again.
Add a callback to know when animation is ended. And use CATransaction to know when animation is completed.
var animationCompletedCallback: (() -> Void)?
func setProgressWithAnimation(duration: TimeInterval, value: Float) {
CATransaction.setCompletionBlock {
if let callBack = animationCompletedCallback {
callBack()
}
}
CATransaction.begin()
let animation = CABasicAnimation (keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = value
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction (name: kCAMediaTimingFunctionLinear)
progressLayer.strokeEnd = CGFloat(value)
progressLayer.add(animation, forKey: "animateprogress")
CATransaction.commit()
}
And add this function after addLongPressGesture() in viewDidLoad() :
private func addCounterViewCallback() {
counterView.animationCompletedCallback = { [weak self] in
guard let weakSelf = self else {return}
weakSelf.addFlashView()
}
}
To remove layer use this:
func removeLayers() {
shapeLayer.removeAllAnimations()
shapeLayer.removeFromSuperlayer()
progressLayer.removeAllAnimations()
progressLayer.removeFromSuperlayer()
}
Update 1:
To remove animation if user stops pressing, you need can add on variable in callback like this :
var animationCompletedCallback: ((isAnimationCompleted: Bool) -> Void)?
So now callback in CounterView will be :
if let callBack = animationCompletedCallback { callBack(true) }
In your controller add one variable:
var isAnimationCompleted = false
Change addCounterViewCallback() :
private func addCounterViewCallback() {
counterView.animationCompletedCallback = { [weak self] (isCompleted) in
guard let weakSelf = self else {return}
weakSelf.isAnimationCompleted = isCompleted
weakSelf.addFlashView()
}
}
Now you can add one condition in your longPress():
if gesture.state == UIGestureRecognizerState.ended {
if !isAnimationCompleted {
//Call remove layers code
}
}
Update 2:
Add a variable in CounterView:
var isAnimationCompleted = true
Change callback like this :
CATransaction.setCompletionBlock {
if let callBack = self.animationCompletedCallback { callBack(isAnimationCompleted) }
}
In controller longPress() :
if gesture.state == UIGestureRecognizerState.ended {
if !isAnimationCompleted {
self.counterView.isAnimationCompleted = false
self.counterView.removeLayers()
}
}
Modify addCounterViewCallback() to this:
private func addCounterViewCallback() {
counterView.animationCompletedCallback = { [weak self] (isCompleted) in
guard let weakSelf = self else {return}
weakSelf.isAnimationCompleted = isCompleted
if isCompleted {
weakSelf.addFlashView()
}
}
}

CABasicAnimation creates empty default value copy of CALayer

I have a custom CALayer that draws radial gradients. It works great except during animation. It seems that each iteration of CABasicAnimation creates a new copy of the CALayer subclass with empty, default values for the properties:
In the screenshot above, you see that CABasicAnimation has created a new copy of the layer and is updating gradientOrigin but none of the other properties have come along for the ride.
This has the result of not rendering anything during the animation. Here's a GIF:
Here's what is should look like:
Here's the animation code:
let animation = CABasicAnimation(keyPath: "gradientOrigin")
animation.duration = 2
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let newOrigin: CGPoint = CGPoint(x: 0, y: triangle.bounds.height/2)
animation.fromValue = NSValue(CGPoint: triangle.gradientLayer.gradientOrigin)
animation.toValue = NSValue(CGPoint: newOrigin)
triangle.gradientLayer.gradientOrigin = newOrigin
triangle.gradientLayer.addAnimation(animation, forKey: nil)
Here's the custom CALayer code:
enum RadialGradientLayerProperties: String {
case gradientOrigin
case gradientRadius
case colors
case locations
}
class RadialGradientLayer: CALayer {
var gradientOrigin = CGPoint() {
didSet { setNeedsDisplay() }
}
var gradientRadius = CGFloat() {
didSet { setNeedsDisplay() }
}
var colors = [CGColor]() {
didSet { setNeedsDisplay() }
}
var locations = [CGFloat]() {
didSet { setNeedsDisplay() }
}
override init(){
super.init()
needsDisplayOnBoundsChange = true
}
required init(coder aDecoder: NSCoder) {
super.init()
}
override init(layer: AnyObject) {
super.init(layer: layer)
}
override class func needsDisplayForKey(key: String) -> Bool {
if key == RadialGradientLayerProperties.gradientOrigin.rawValue || key == RadialGradientLayerProperties.gradientRadius.rawValue || key == RadialGradientLayerProperties.colors.rawValue || key == RadialGradientLayerProperties.locations.rawValue {
print("called \(key)")
return true
}
return super.needsDisplayForKey(key)
}
override func actionForKey(event: String) -> CAAction? {
if event == RadialGradientLayerProperties.gradientOrigin.rawValue || event == RadialGradientLayerProperties.gradientRadius.rawValue || event == RadialGradientLayerProperties.colors.rawValue || event == RadialGradientLayerProperties.locations.rawValue {
let animation = CABasicAnimation(keyPath: event)
animation.fromValue = self.presentationLayer()?.valueForKey(event)
return animation
}
return super.actionForKey(event)
}
override func drawInContext(ctx: CGContext) {
guard let colorRef = self.colors.first else { return }
let numberOfComponents = CGColorGetNumberOfComponents(colorRef)
let colorSpace = CGColorGetColorSpace(colorRef)
let deepGradientComponents: [[CGFloat]] = (self.colors.map {
let colorComponents = CGColorGetComponents($0)
let buffer = UnsafeBufferPointer(start: colorComponents, count: numberOfComponents)
return Array(buffer) as [CGFloat]
})
let flattenedGradientComponents = deepGradientComponents.flatMap({ $0 })
let gradient = CGGradientCreateWithColorComponents(colorSpace, flattenedGradientComponents, self.locations, self.locations.count)
CGContextDrawRadialGradient(ctx, gradient, self.gradientOrigin, 0, self.gradientOrigin, self.gradientRadius, .DrawsAfterEndLocation)
}
}
Figured out the answer!
In init(layer:) you have to copy the property values to your class manually. Here's how that looks in action:
override init(layer: AnyObject) {
if let layer = layer as? RadialGradientLayer {
gradientOrigin = layer.gradientOrigin
gradientRadius = layer.gradientRadius
colors = layer.colors
locations = layer.locations
}
super.init(layer: layer)
}