CABasicAnimation creates empty default value copy of CALayer - swift

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)
}

Related

UIView to UIImage code is not working. It causing an runtime error

I have to convert UIView to UIImage because I want to make a custom marker.
Currently, I'm making an app using Naver Map SDK.
Here is My Code.
let marker = NMFMarker()
marker.position = NMGLatLng(lat: lat, lng: lng)
let windowView = CustomInfoWindowView()
windowView.nameLabel.text = name
windowView.jobLabel.text = job
windowView.scoreLabel.text = "\(score)"
marker.iconImage = NMFOverlayImage(image: windowView.asImage())
marker.mapView = mapView
NMFOverlayImage only supported UIImage type.
Here is My full code.
class MapViewController: UIViewController {
let viewModel = MapViewModel()
let locations = BehaviorRelay<Void>(value: ())
let disposedBag = DisposeBag()
private let imageCell = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
let mapView = NMFMapView(frame: view.frame)
view.addSubview(mapView)
bind(mapView: mapView)
}
private func bind(mapView: NMFMapView) {
let input = MapViewModel.Input(getLocations: locations.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
output.getLocations.asObservable().subscribe(onNext: { [unowned self] res in
if !res.isEmpty {
for i in 0..<res.count {
addMarker(
lat: res[i].address[1],
lng: res[i].address[0],
mapView: mapView,
name: res[i].name,
job: res[i].field,
score: res[i].level
)
}
}
}).disposed(by: disposedBag)
}
private func addMarker(lat: Double, lng: Double, mapView: NMFMapView, name: String, job: String, score: Double) {
let marker = NMFMarker()
marker.position = NMGLatLng(lat: lat, lng: lng)
let windowView = CustomInfoWindowView()
windowView.nameLabel.text = name
windowView.jobLabel.text = job
windowView.scoreLabel.text = "\(score)"
marker.iconImage = NMFOverlayImage(image: windowView.asImage())
marker.mapView = mapView
}
}
and this is my custom view code.
import UIKit
import SnapKit
import Then
class CustomInfoWindowView: UIView {
let customView = UIView().then {
$0.backgroundColor = .clear
}
let windowView = UIView().then {
$0.backgroundColor = R.color.mainColor()
$0.layer.cornerRadius = 10
}
let marker = UIImageView().then {
$0.image = R.image.marker()
}
let nameLabel = UILabel().then {
$0.font = .boldSystemFont(ofSize: 16)
$0.textColor = .white
}
let jobLabel = UILabel().then {
$0.font = .systemFont(ofSize: 12, weight: .medium)
$0.textColor = R.color.underLine()!
}
let starLogo = UIImageView().then {
$0.image = UIImage(systemName: "star.fill")
$0.tintColor = .systemYellow
}
let scoreLabel = UILabel().then {
$0.font = .boldSystemFont(ofSize: 14)
$0.textColor = .white
}
override init(frame: CGRect) {
super.init(frame: frame)
setUpSubViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func setUpSubViews() {
self.addSubview(customView)
[windowView, marker].forEach({customView.addSubview($0)})
[nameLabel, jobLabel, starLogo, scoreLabel].forEach({self.addSubview($0)})
customView.snp.makeConstraints {
$0.width.equalTo(108)
$0.height.equalTo(100)
}
windowView.snp.makeConstraints {
$0.width.equalToSuperview()
$0.height.equalTo(64)
$0.top.equalTo(0)
}
marker.snp.makeConstraints {
$0.width.equalTo(20)
$0.height.equalTo(27)
$0.top.equalTo(windowView.snp.bottom).offset(10)
$0.centerX.equalTo(windowView.snp.centerX)
}
nameLabel.snp.makeConstraints {
$0.top.leading.equalTo(10)
$0.width.equalTo(10)
}
jobLabel.snp.makeConstraints {
$0.centerY.equalTo(nameLabel.snp.centerY)
$0.leading.equalTo(nameLabel.snp.trailing).offset(5)
}
starLogo.snp.makeConstraints {
$0.top.equalTo(nameLabel.snp.bottom).offset(5)
$0.leading.equalTo(nameLabel.snp.leading)
$0.width.height.equalTo(15)
}
scoreLabel.snp.makeConstraints {
$0.centerY.equalTo(starLogo.snp.centerY)
$0.leading.equalTo(starLogo.snp.trailing).offset(3)
}
}
}
and this is my UIView Extension code.
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { context in
layer.render(in: context.cgContext)
}
}
}
I Use Snapkit, Then, RxSwift, RxCocoa,NMapsMap.
please Help me.
When I run the code .asImage(), It had runtime error.
enter image description here

Processing touches on moving/ animating UiViews

I currently have the problem that touches are not always identified correctly,
My goal is to have 3 gestures,The 3 gestures are
A user can tap on a view and the tap gets recognised,
A user can double tap on a view and the double tap is recognised,
A user can move their finger on the screen and if a view is below it
a tab is recognised.
However I have multiple views all animating constantly and they may overlap,
Currently I sort views by size and have the smallest views on top of larger views.
And I typically get an issue that UIViews are not recognised when tapping on them. In particular double taps, swiping seems to work fine most of the time however the whole experience is very inconsistent.
The current code I'm using to solve the problem is:
class FinderrBoxView: UIView {
private var lastBox: String?
private var throttleDelay = 0.01
private var processQueue = DispatchQueue(label: "com.finderr.FinderrBoxView")
public var predictedObjects: [FinderrItem] = [] {
didSet {
predictedObjects.forEach { self.checkIfBoxIntersectCentre(prediction: $0) }
drawBoxs(with: FinderrBoxView.sortBoxByeSize(predictedObjects))
setNeedsDisplay()
}
}
func drawBoxs(with predictions: [FinderrItem]) {
var newBoxes = Set(predictions)
var views = subviews.compactMap { $0 as? BoxView }
views = views.filter { view in
guard let closest = newBoxes.sorted(by: { x, y in
let xd = FinderrBoxView.distanceBetweenBoxes(view.frame, x.box)
let yd = FinderrBoxView.distanceBetweenBoxes(view.frame, y.box)
return xd < yd
}).first else { return false }
if FinderrBoxView.updateOrCreateNewBox(view.frame, closest.box)
{
newBoxes.remove(closest)
UIView.animate(withDuration: self.throttleDelay, delay: 0, options: .curveLinear, animations: {
view.frame = closest.box
}, completion: nil)
return false
} else {
return true
}
}
views.forEach { $0.removeFromSuperview() }
newBoxes.forEach { self.createLabelAndBox(prediction: $0) }
accessibilityElements = subviews
}
func update(with predictions: [FinderrItem]) {
var newBoxes = Set(predictions)
var viewsToRemove = [UIView]()
for view in subviews {
var shouldRemoveView = true
for box in predictions {
if FinderrBoxView.updateOrCreateNewBox(view.frame, box.box)
{
UIView.animate(withDuration: throttleDelay, delay: 0, options: .curveLinear, animations: {
view.frame = box.box
}, completion: nil)
shouldRemoveView = false
newBoxes.remove(box)
}
}
if shouldRemoveView {
viewsToRemove.append(view)
}
}
viewsToRemove.forEach { $0.removeFromSuperview() }
for prediction in newBoxes {
createLabelAndBox(prediction: prediction)
}
accessibilityElements = subviews
}
func checkIfBoxIntersectCentre(prediction: FinderrItem) {
let centreX = center.x
let centreY = center.y
let maxX = prediction.box.maxX
let minX = prediction.box.midX
let maxY = prediction.box.maxY
let minY = prediction.box.minY
if centreX >= minX, centreX <= maxX, centreY >= minY, centreY <= maxY {
// NotificationCenter.default.post(name: .centreIntersectsWithBox, object: prediction.name)
}
}
func removeAllSubviews() {
UIView.animate(withDuration: throttleDelay, delay: 0, options: .curveLinear) {
for i in self.subviews {
i.frame = CGRect(x: i.frame.midX, y: i.frame.midY, width: 0, height: 0)
}
} completion: { _ in
self.subviews.forEach { $0.removeFromSuperview() }
}
}
static func getDistanceFromCloseBbox(touchAt p1: CGPoint, items: [FinderrItem]) -> Float {
var boxCenters = [Float]()
for i in items {
let distance = Float(sqrt(pow(i.box.midX - p1.x, 2) + pow(i.box.midY - p1.y, 2)))
boxCenters.append(distance)
}
boxCenters = boxCenters.sorted { $0 < $1 }
return boxCenters.first ?? 0.0
}
static func sortBoxByeSize(_ items: [FinderrItem]) -> [FinderrItem] {
return items.sorted { i, j -> Bool in
let iC = sqrt(pow(i.box.height, 2) + pow(i.box.width, 2))
let jC = sqrt(pow(j.box.height, 2) + pow(j.box.width, 2))
return iC > jC
}
}
static func updateOrCreateNewBox(_ box1: CGRect, _ box2: CGRect) -> Bool {
let distance = sqrt(pow(box1.midX - box2.midX, 2) + pow(box1.midY - box2.midY, 2))
print(distance)
return distance < 50
}
static func distanceBetweenBoxes(_ box1: CGRect, _ box2: CGRect) -> Float {
return Float(sqrt(pow(box1.midX - box2.midX, 2) + pow(box1.midY - box2.midY, 2)))
}
func createLabelAndBox(prediction: FinderrItem) {
let bgRect = prediction.box
let boxView = BoxView(frame: bgRect ,itemName: "box")
addSubview(boxView)
}
#objc func handleTap(_ sender: UITapGestureRecognizer) {
// handling code
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
processTouches(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
processTouches(touches, with: event)
}
func processTouches(_ touches: Set<UITouch>, with event: UIEvent?) {
if UIAccessibility.isVoiceOverRunning { return }
if predictedObjects.count == 0 { return }
if let touch = touches.first {
let hitView = hitTest(touch.location(in: self), with: event)
if hitView?.accessibilityLabel == lastBox { return }
lastBox = hitView?.accessibilityLabel
guard let boxView = hitView as? BoxView else {
return
}
UIView.animate(withDuration: 0.1, delay: 0, options: .curveLinear) {
boxView.backgroundColor = UIColor.yellow.withAlphaComponent(0.5)
} completion: { _ in
UIView.animate(withDuration: 0.1, delay: 0, options: .curveLinear, animations: {
boxView.backgroundColor = UIColor.clear
}, completion: nil)
}
}
}
}
class BoxView: UIView {
let id = UUID()
var itemName: String
init(frame: CGRect, itemName: String) {
self.itemName = itemName
super.init(frame: frame)
if !UIAccessibility.isVoiceOverRunning {
let singleDoubleTapRecognizer = SingleDoubleTapGestureRecognizer(
target: self,
singleAction: #selector(handleDoubleTapGesture),
doubleAction: #selector(handleDoubleTapGesture)
)
addGestureRecognizer(singleDoubleTapRecognizer)
}
}
#objc func navigateAction() -> Bool {
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
return true
}
required init?(coder aDecoder: NSCoder) {
itemName = "error aDecoder"
super.init(coder: aDecoder)
}
#objc func handleDoubleTapGesture(_: UITapGestureRecognizer) {
// handling code
// NotificationCenter.default.post(name: .didDoubleTapOnObject, object: itemName)
}
}
public class SingleDoubleTapGestureRecognizer: UITapGestureRecognizer {
var targetDelegate: SingleDoubleTapGestureRecognizerDelegate
public var timeout: TimeInterval = 0.5 {
didSet {
targetDelegate.timeout = timeout
}
}
public init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
targetDelegate = SingleDoubleTapGestureRecognizerDelegate(target: target, singleAction: singleAction, doubleAction: doubleAction)
super.init(target: targetDelegate, action: #selector(targetDelegate.recognizerAction(recognizer:)))
}
}
class SingleDoubleTapGestureRecognizerDelegate: NSObject {
weak var target: AnyObject?
var singleAction: Selector
var doubleAction: Selector
var timeout: TimeInterval = 0.5
var tapCount = 0
var workItem: DispatchWorkItem?
init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
self.target = target
self.singleAction = singleAction
self.doubleAction = doubleAction
}
#objc func recognizerAction(recognizer: UITapGestureRecognizer) {
tapCount += 1
if tapCount == 1 {
workItem = DispatchWorkItem { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.singleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
DispatchQueue.main.asyncAfter(
deadline: .now() + timeout,
execute: workItem!
)
} else {
workItem?.cancel()
DispatchQueue.main.async { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.doubleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
}
}
}
class FinderrItem: Equatable, Hashable {
var box: CGRect
init(
box: CGRect)
{
self.box = box
}
func hash(into hasher: inout Hasher) {
hasher.combine(Float(box.origin.x))
hasher.combine(Float(box.origin.y))
hasher.combine(Float(box.width))
hasher.combine(Float(box.height))
hasher.combine(Float(box.minX))
hasher.combine(Float(box.maxY))
}
static func == (lhs: FinderrItem, rhs: FinderrItem) -> Bool {
return lhs.box == rhs.box
}
}
By default view objects block user interaction while an animation is "in flight". You need to use one of the "long form" animation methods, and pass in the option .allowUserInteraction. Something like this:
UIView.animate(withDuration: 0.5,
delay: 0.0,
options: .allowUserInteraction,
animations: {
myView.alpha = 0.5
})

Persisting CABasicAnimation

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

How to fix MTKView random fill color when rendering a new image

I'm working on a photo editor and I just switched from a UIImageView to an MTKView for performance reasons. It works great except for some reason on some images there is a red box. The image size changes and the red box gets bigger when displaying a new image.
I am using Mac Catalyst on this
This is my MTKView code
public let colorSpace = CGColorSpaceCreateDeviceRGB()
public lazy var commandQueue: MTLCommandQueue = {
[unowned self] in
if self.device == nil {
self.device = MTLCreateSystemDefaultDevice()
}
return self.device!.makeCommandQueue()!
}()
public lazy var ciContext: CIContext = {
[unowned self] in
if self.device == nil {
self.device = MTLCreateSystemDefaultDevice()
}
let context = CIContext(mtlDevice: self.device!, options: [CIContextOption.highQualityDownsample: true, CIContextOption.priorityRequestLow: false])
return context
}()
public override init(frame frameRect: CGRect, device: MTLDevice? = MTLCreateSystemDefaultDevice()) {
super.init(frame: frameRect,
device: device ?? MTLCreateSystemDefaultDevice())
if super.device == nil
{
fatalError("Device doesn't support Metal")
}
self.framebufferOnly = false
}
required public init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.framebufferOnly = false
self.device = MTLCreateSystemDefaultDevice()
}
/// The image to display
public var image: CIImage?
{
didSet
{
renderImage()
}
}
public var orientation: CGImagePropertyOrientation? {
didSet {
renderImage()
}
}
func renderImage()
{
guard var
image = self.image,
let targetTexture = self.currentDrawable?.texture else
{
print("No texture/image")
return
}
self.drawableSize = image.extent.size
if let orientation = orientation {
image = image.oriented(orientation)
}
let commandBuffer = self.commandQueue.makeCommandBuffer()
let bounds = CGRect(origin: CGPoint.zero, size: self.drawableSize)
let originX = image.extent.origin.x
let originY = image.extent.origin.y
let scaleX = self.drawableSize.width / image.extent.width
let scaleY = self.drawableSize.height / image.extent.height
let scale = min(scaleX, scaleY)
let scaledImage = image
.transformed(by: CGAffineTransform(translationX: -originX, y: -originY))
.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
self.ciContext.render(scaledImage,
to: targetTexture,
commandBuffer: commandBuffer,
bounds: bounds,
colorSpace: self.colorSpace)
commandBuffer?.present(self.currentDrawable!)
commandBuffer?.commit()
self.draw()
self.releaseDrawables()
}

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()
}
}
}