Swift: Longpress Button Animation - ios

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

Related

How to attach multiple UIDynamicItems to each other

I am trying to implement circles attached to each other like in Apple's Music App via UIDynamicAnimator. I need to attach circles to each other and to view center. I was trying to implement this via UIAttachmentBehavior, but seems to it's not supporting multiple attachments. In result, circles overlaps on each other :)
let attachment = UIAttachmentBehavior(item: circle, attachedToAnchor: CGPoint(x: view.center.x, y: view.center.y))
attachment.length = 10
animator?.addBehavior(attachment)
let push = UIPushBehavior(items: [circle], mode: .continuous)
collision.addItem(circle)
animator?.addBehavior(push)
What I am doing wrong?
I don't think the apple music genre picker thing uses UIAttachmentBehavior which is closer to attaching two views with a pole or a rope. But, it seems like the problem you're experiencing might be that all of the views are added at the same location which has the effect of placing them on top of each other and with the collision behavior causes them to be essentially be stuck together. One thing to do is to turn on UIDynamicAnimator debugging by calling animator.setValue(true, forKey: "debugEnabled").
For recreating the above circle picker design, I would look into using UIFieldBehavior.springField().
For example:
class ViewController: UIViewController {
lazy var animator: UIDynamicAnimator = {
let animator = UIDynamicAnimator(referenceView: view)
return animator
}()
lazy var collision: UICollisionBehavior = {
let collision = UICollisionBehavior()
collision.collisionMode = .items
return collision
}()
lazy var behavior: UIDynamicItemBehavior = {
let behavior = UIDynamicItemBehavior()
behavior.allowsRotation = false
behavior.elasticity = 0.5
behavior.resistance = 5.0
behavior.density = 0.01
return behavior
}()
lazy var gravity: UIFieldBehavior = {
let gravity = UIFieldBehavior.springField()
gravity.strength = 0.008
return gravity
}()
lazy var panGesture: UIPanGestureRecognizer = {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.didPan(_:)))
return panGesture
}()
var snaps = [UISnapBehavior]()
var circles = [CircleView]()
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(panGesture)
animator.setValue(true, forKey: "debugEnabled")
addCircles()
addBehaviors()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
gravity.position = view.center
snaps.forEach {
$0.snapPoint = view.center
}
}
func addCircles() {
(1...30).forEach { index in
let xIndex = index % 2
let yIndex: Int = index / 3
let circle = CircleView(frame: CGRect(origin: CGPoint(x: xIndex == 0 ? CGFloat.random(in: (-300.0 ... -100)) : CGFloat.random(in: (500 ... 800)), y: CGFloat(yIndex) * 200.0), size: CGSize(width: 100, height: 100)))
circle.backgroundColor = .red
circle.text = "\(index)"
circle.textAlignment = .center
view.addSubview(circle)
gravity.addItem(circle)
collision.addItem(circle)
behavior.addItem(circle)
circles.append(circle)
}
}
func addBehaviors() {
animator.addBehavior(collision)
animator.addBehavior(behavior)
animator.addBehavior(gravity)
}
#objc
private func didPan(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: sender.view)
switch sender.state {
case .began:
animator.removeAllBehaviors()
fallthrough
case .changed:
circles.forEach { $0.center = CGPoint(x: $0.center.x + translation.x, y: $0.center.y + translation.y)}
case .possible, .cancelled, .failed:
break
case .ended:
circles.forEach { $0.center = CGPoint(x: $0.center.x + translation.x, y: $0.center.y + translation.y)}
addBehaviors()
#unknown default:
break
}
sender.setTranslation(.zero, in: sender.view)
}
}
final class CircleView: UILabel {
override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .ellipse
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height * 0.5
layer.masksToBounds = true
}
}
For more information I would watch What's New in UIKit Dynamics and Visual Effects from WWDC 2015

Recall drawRect after view is recalled

I have a view that is using drawRect to draw a round timer.
In its superview there's a button that pushes another VC to the screen.
The problem is when the user clicks the back button, the timer (which is drawn using drawRect) is automatically starting with the toValue value instad of the fromValue. I want it to be redrawed exactly as it is drawn when I first launch the view.
Does anybody know how can I achieve that?
Thank you!
Code:
class CountDownTimer: UIView {
public var backgroundStrokeColor: CGColor = UIColor.white.cgColor
public var backgroundFillColor: CGColor = UIColor.clear.cgColor
public var backgroundLineWidth: CGFloat = 15
public var timeLeftSrtokeColor: CGColor = UIColor.red.cgColor
public var timeLeftFillColor: CGColor = UIColor.clear.cgColor
public var timeLeftLineWidth: CGFloat = 10
public var textColor: UIColor = UIColor.white
public var textFont: UIFont = UIFont.balooRegular(10.0)
fileprivate var timeLeft: TimeInterval = 0
fileprivate var endDate: Date?
fileprivate var timeLeftShapeLayer: CAShapeLayer?
fileprivate var bgShapeLayer: CAShapeLayer?
fileprivate var timeLabel: UILabel?
fileprivate var timer = Timer()
fileprivate let strokeIt = CABasicAnimation(keyPath: "strokeEnd")
//MARK: - UIView
override func draw(_ rect: CGRect) {
drawBgShape()
drawTimeLeftShape()
addTimeLabel()
strokeIt.toValue = 1 //"fromValue" is set in "startTimer(duration, timerProgress)
strokeIt.duration = self.timeLeft
// add the animation to your timeLeftShapeLayer
timeLeftShapeLayer?.add(strokeIt, forKey: nil)
// define the future end time by adding the timeLeft to now Date()
}
//MARK: - Public
public func startTimer(duration: TimeInterval, timerProgress: Double) {
self.timeLeft = duration
endDate = Date().addingTimeInterval(timeLeft)
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
strokeIt.fromValue = timerProgress
}
//MARK: - Private
fileprivate func drawBgShape() {
//we initialize and add the layer only if there is not initialized
if(bgShapeLayer == nil){
bgShapeLayer = CAShapeLayer()
self.layer.addSublayer(bgShapeLayer!)
}
bgShapeLayer?.path = UIBezierPath(arcCenter: CGPoint(x: self.frame.midX , y: self.frame.midY), radius:
min((frame.width - self.timeLeftLineWidth)/2, (frame.height - self.timeLeftLineWidth)/2), startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
bgShapeLayer?.strokeColor = self.backgroundStrokeColor
bgShapeLayer?.fillColor = self.backgroundFillColor
bgShapeLayer?.lineWidth = self.backgroundLineWidth
}
fileprivate func drawTimeLeftShape() {
//we initialize and add the layer only if there is not initialized
if(timeLeftShapeLayer == nil){
timeLeftShapeLayer = CAShapeLayer()
self.layer.addSublayer(timeLeftShapeLayer!)
}
timeLeftShapeLayer?.path = UIBezierPath(arcCenter: CGPoint(x: self.frame.midX , y: self.frame.midY), radius:
min((frame.width - self.timeLeftLineWidth)/2, (frame.height - self.timeLeftLineWidth)/2), startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
timeLeftShapeLayer?.strokeColor = self.timeLeftSrtokeColor
timeLeftShapeLayer?.fillColor = self.timeLeftFillColor
timeLeftShapeLayer?.lineWidth = self.timeLeftLineWidth
}
fileprivate func addTimeLabel() {
//we initialize and add the UILabel only if there is not initialized
if(timeLabel == nil){
timeLabel = UILabel()
self.addSubview(timeLabel!)
}
timeLabel?.frame = CGRect(x: self.frame.midX-50 ,y: self.frame.midY-25, width: 100, height: 50)
timeLabel?.adjustsFontSizeToFitWidth = true
timeLabel?.textAlignment = .center
timeLabel?.text = self.timeLeft.stringTime
timeLabel?.textColor = self.textColor
timeLabel?.font = self.textFont
}
//MARK: - Actions
#objc fileprivate func updateTime() {
if timeLeft > 0 {
timeLeft = endDate?.timeIntervalSinceNow ?? 0
timeLabel?.text = self.timeLeft.stringTime
} else {
timeLabel?.text = self.timeLeft.stringTime
timer.invalidate()
}
}
}

How can I set UIGestureRecognizer's Speed as the velocity for my Physicsbody in SpriteKit?

I calculated the speed of UIGestureRecognizer and I want to use it as velocity of Physicsbody in SpriteKit.
How can I do that since Speed is calculated in touches began and touches ended method, while my Physicsbody is wrapped in swipeUp & swipeDown etc functions, which does not have access to the Speed variable?
Here is my code:
class GameScene: SKScene, SKPhysicsContactDelegate, SKViewDelegate, UIGestureRecognizerDelegate, UIViewControllerTransitioningDelegate {
var Kite = SKSpriteNode(imageNamed: "ASC_8025_large.jpg")
#objc let minusButton = SKSpriteNode(imageNamed: "minus.jpg")
#objc let plusButton = SKSpriteNode(imageNamed: "plus.png")
//override init(Kite: SKSpriteNode) {
//For long Long press gesture to work
let longPressGestureRecPlus = UILongPressGestureRecognizer()
let longPressGestureRecMinus = UILongPressGestureRecognizer(target: self, action: #selector(longPressed(press:)))
//First we declare all of our Gestures...
//swipes
let swipeRightRec = UISwipeGestureRecognizer()
let swipeLeftRec = UISwipeGestureRecognizer()
let swipeUpRec = UISwipeGestureRecognizer()
let swipeDownRec = UISwipeGestureRecognizer()
//rotate
let rotateRec = UIRotationGestureRecognizer()
//taps
let tapRec = UITapGestureRecognizer()
let tapRec2 = UITapGestureRecognizer()
override func didMove(to view: SKView) {
// Get label node from scene and store it for use later
Kite.size = CGSize(width: 40.0, height: 40.0)
Kite.position = CGPoint(x: frame.midX, y: frame.midY)
plusButton.size = CGSize(width: 30.0, height: 30.0)
plusButton.position = CGPoint(x: frame.midX, y: frame.minY + 20.0)
plusButton.name = "plusButton"
//plusButton.isUserInteractionEnabled = true
addChild(Kite)
addChild(plusButton)
swipeRightRec.addTarget(self, action: #selector(GameScene.swipedRight) )
swipeRightRec.direction = .right
self.view!.addGestureRecognizer(swipeRightRec)
swipeLeftRec.addTarget(self, action: #selector(GameScene.swipedLeft) )
swipeLeftRec.direction = .left
self.view!.addGestureRecognizer(swipeLeftRec)
swipeUpRec.addTarget(self, action: #selector(GameScene.swipedUp) )
swipeUpRec.direction = .up
self.view!.addGestureRecognizer(swipeUpRec)
swipeDownRec.addTarget(self, action: #selector(GameScene.swipedDown) )
swipeDownRec.direction = .down
self.view!.addGestureRecognizer(swipeDownRec)
//notice the function this calls has (_:) after it because we are passing in info about the gesture itself (the sender)
rotateRec.addTarget(self, action: #selector (GameScene.rotatedView (_:) ))
self.view!.addGestureRecognizer(rotateRec)
// again notice (_:), we'll need this to find out where the tap occurred.
tapRec.addTarget(self, action:#selector(GameScene.tappedView(_:) ))
tapRec.numberOfTouchesRequired = 1
tapRec.numberOfTapsRequired = 1
self.view!.addGestureRecognizer(tapRec)
tapRec2.addTarget(self, action:#selector(GameScene.tappedView2(_:) ))
tapRec2.numberOfTouchesRequired = 1
tapRec2.numberOfTapsRequired = 2 //2 taps instead of 1 this time
self.view!.addGestureRecognizer(tapRec2)
longPressGestureRecPlus.addTarget(self, action: #selector(longPressed(press:)))
longPressGestureRecPlus.minimumPressDuration = 2.0
self.view?.addGestureRecognizer(longPressGestureRecPlus)
}
//the functions that get called when swiping...
#objc func swipedRight() {
print("Right")
Kite.physicsBody?.velocity = CGVector(dx: 60, dy: 60)
//Tilts the Kite towards Right
let tiltRight = SKAction.rotate(toAngle: -1.00, duration: 0.1)
Kite.run(tiltRight)
}
#objc func swipedLeft() {
Kite.physicsBody?.velocity = CGVector(dx: -60, dy: 60)
//Tilts the Kite towards Right
let tiltLeft = SKAction.rotate(toAngle: 1.00, duration: 0.1)
Kite.run(tiltLeft)
print("Left")
}
#objc func swipedUp() {
Kite.physicsBody?.velocity = CGVector(dx: 60, dy: 60)
//Straightens the Kite
let straightens = SKAction.rotate(toAngle: 0.00, duration: 0.1)
Kite.run(straightens)
print("Up")
}
#objc func swipedDown() {
Kite.physicsBody?.velocity = CGVector(dx: 0, dy: -60)
print("Down")
}
// what gets called when there's a single tap...
//notice the sender is a parameter. This is why we added (_:) that part to the selector earlier
#objc func tappedView(_ sender:UITapGestureRecognizer) {
let point:CGPoint = sender.location(in: self.view)
print("Single tap")
print(point)
}
// what gets called when there's a double tap...
//notice the sender is a parameter. This is why we added (_:) that part to the selector earlier
#objc func tappedView2(_ sender:UITapGestureRecognizer) {
let point:CGPoint = sender.location(in: self.view)
print("Double tap")
print(point)
}
//what gets called when there's a rotation gesture
//notice the sender is a parameter. This is why we added (_:) that part to the selector earlier
#objc func rotatedView(_ sender:UIRotationGestureRecognizer) {
if (sender.state == .began) {
print("rotation began")
}
if (sender.state == .changed) {
print("rotation changed")
//you could easily make any sprite's rotation equal this amount like so...
//thePlayer.zRotation = -sender.rotation
//convert rotation to degrees...
let rotateAmount = Measurement(value: Double(sender.rotation), unit: UnitAngle.radians).converted(to: .degrees).value
print("\(rotateAmount) degreess" )
}
if (sender.state == .ended) {
print("rotation ended")
}
}
func removeAllGestures(){
//if you need to remove all gesture recognizers with Swift you can do this....
for gesture in (self.view?.gestureRecognizers)! {
self.view?.removeGestureRecognizer(gesture)
}
//this is good to do before a SKScene transitions to another SKScene.
}
func removeAGesture()
{
//To remove a single gesture you can use...
self.view?.removeGestureRecognizer(swipeUpRec)
}
//Fix the gesture recognizing the plusButton
#objc func longPressed(press: UILongPressGestureRecognizer) {
if press.state == .began {
isUserInteractionEnabled = true
let positionInScene = press.location(in: self.view)
let touchedNode = self.atPoint(positionInScene)
plusButton.name = "plusButton"
Kite.physicsBody?.velocity = CGVector(dx: 0, dy: 60.0*3)
print("Pressed on the screen")
if let name = touchedNode.name {
if name == "plusButton" {
print("LONG TAPPED")
}
}
}
}
var start: CGPoint?
var startTime: TimeInterval?
var taps = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Began")
Kite.physicsBody = SKPhysicsBody(rectangleOf: Kite.size)
Kite.physicsBody?.affectedByGravity = false
//Kite.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
Kite.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 5))
plusButton.name = "plusButton"
plusButton.isUserInteractionEnabled = false
//We figure how to interact with BUTTON in Spritekit
let touch = touches.first
let positionInScene = touch!.location(in: self)
let touchedNode = self.atPoint(positionInScene)
if let name = touchedNode.name {
if name == "plusButton" {
taps += 1
Kite.size = CGSize(width: 200, height: 200)
print("tapped")
print("Taps", taps)
}
}
for touch in touches {
let location:CGPoint = touch.location(in: self.view!)
start = location
startTime = touch.timestamp
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Ended")
//Calculating Speed of the Gestures
for touch in touches {
let location:CGPoint = touch.location(in: self.view!)
var dx:CGFloat = location.x - start!.x;
var dy:CGFloat = location.y - start!.y;
var magnitude:CGFloat = sqrt(dx*dx+dy*dy)
//Calculate Time
var dt:CGFloat = CGFloat(touch.timestamp - startTime!)
//Speed = Distance / Time
var speed:CGFloat = magnitude / dt
var speedX:CGFloat = dx/dt
var speedY:CGFloat = dy/dt
print("SpeedY", speedX)
print("SpeedY", speedY)
}
}
so calculating the speed from a vector like dx and dy is
let speed = sqrtf(Float(pow(dx!, 2) + pow(dy!, 2)))
So if you want apply the speed as velocity to an object you need at least one of the directions.
let newDx = 0 // means in no x direction
// next you can change the formula to your new dy to match the given speed
let newDy = sqrtf(pow(speed, 2) - Float(pow(newDx, 2)))
yourNode.physicsBody?.velocity = CGVector(dx: newDx, dy: CGFloat(newDy))

CALayer animation beginTime

I'm developing a keyframe animation editor for iOS that allows the user to create simple animations and view them, plotted against a timeline. The user is able to drag the timeline in order to change the current time.
This means that I need to be able to start an animation at a user specified time.
Whilst I can achieve this behaviour, i also am experiencing an annoying glitch every time I re-add the animation to the layer. This glitch causes the first frame of the animation to flash very quickly before CoreAnimation respects the begin time. I can mitigate the effects of this ever so slightly by setting the layer.alpha to 0 before the flush, and 1 after the flush however this still results in a nasty single frame(ish) flash!
I have recreated a sample view controller that demonstrates the code required to "change the time" of the running animation but as this project is so simple, you don't really see the negative effects of the flush:
https://gist.github.com/chrisbirch/5cafca50804cf9d778ccd0fdc9e68d56
Basic idea behind the code is as follows:
Each time the user changes the current time, I restart the animation and fiddle with the CALayer timing properties like so (addStoppedAnimation:line 215):
ani = createGroup()
animatableLayer.speed = 0
animatableLayer.add(ani, forKey: "an animation key")
let time = CFTimeInterval(slider.value)
animatableLayer.timeOffset = 0
animatableLayer.beginTime = animatableLayer.superlayer!.convertTime(CACurrentMediaTime(), from: nil) - time
CATransaction.flush()
animatableLayer.timeOffset = time// offset
print("Time changed \(time)")
The glitch is caused by me having to call CATransaction.flush right before I set timeOffset. Failure to call this flush results in the begin time being ignored it would seem.
I feel like I have scoured the entire internet looking for a solution to this problem but alas Im think I'm stuck.
My question is this:
Can anyone shed any light onto why it is I need to call CATransaction.flush in order for the beginTime value that I set to take effect? Looking at apple code I didn't ever see them using flush for this purpose so perhaps I have got something obvious wrong!
Many thanks in advance
Chris
Using your test code from the gist I have updated it to check for an animation so that there is no need to readd it. You could us a unique ID to track all animations and store it in a dictionary with view attributes. I did not implement this part but that is how I would do it. Hopefully I understood your issue enough. Also I used Xcode 9 and I am not sure of the code differences. I changed a few logic pieces so let me know if this fixes the issue.
UUID().uuidString //for unique string in implementation
//from your code just slightly altered.
//
// ViewController.swift
// CATest
//
// CATestViewController.swift
// SimpleCALayerTest
import UIKit
class CATestViewController: UIViewController, CAAnimationDelegate{
var slider : UISlider!
var animatableLayer : CALayer!
var animationContainerView : UIView!
var centerY : CGFloat!
var startTranslationX : CGFloat!
var endTranslationX : CGFloat!
let duration = 10.0
///boring nibless view setup code
override func loadView() {
let marginX = CGFloat(10)
let marginY = CGFloat(10)
let view = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
view.backgroundColor = .lightGray
slider = UISlider(frame: CGRect(x: marginX, y: 0, width: 200, height: 50))
slider.maximumValue = Float(duration)
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
slider.addTarget(self, action: #selector(sliderDragStart(_:)), for: .touchDown)
slider.addTarget(self, action: #selector(sliderDragEnd(_:)), for: .touchUpInside)
//A view to house an animated sublayer
animationContainerView = UIView(frame: CGRect(x: marginX, y: 50, width: 200, height: 70))
//add a play button that will allow the animation to be played without hindrance from the slider
let playButton = UIButton(frame: CGRect(x: marginX, y: animationContainerView.frame.maxY + marginY, width: 200, height: 50))
playButton.setTitle("Change Frame", for: .normal)
playButton.addTarget(self, action: #selector(playAnimation), for: .touchUpInside)
view.addSubview(playButton)
//add a stopped ani button that will allow the animation to be played using slider
let addStoppedAniButton = UIButton(frame: CGRect(x: playButton.frame.origin.x, y: playButton.frame.maxY + marginY, width: playButton.frame.width, height: playButton.frame.size.height))
addStoppedAniButton.setTitle("Pause", for: .normal)
addStoppedAniButton.addTarget(self, action: #selector(cmPauseTapped(_:)), for: .touchUpInside)
view.addSubview(addStoppedAniButton)
let animatableLayerWidth = animationContainerView.bounds.width / CGFloat(4)
centerY = animationContainerView.bounds.midY
startTranslationX = animatableLayerWidth / CGFloat(2)
endTranslationX = animationContainerView.bounds.width - animatableLayerWidth / CGFloat(2)
animationContainerView.backgroundColor = .white
animationContainerView.layer.borderColor = UIColor.black.withAlphaComponent(0.5).cgColor
animationContainerView.layer.borderWidth = 1
view.addSubview(slider)
view.addSubview(animationContainerView)
//Now add a layer to animate to the container
animatableLayer = CALayer()
animatableLayer.backgroundColor = UIColor.yellow.cgColor
animatableLayer.borderWidth = 1
animatableLayer.borderColor = UIColor.black.withAlphaComponent(0.5).cgColor
var r = animationContainerView.bounds.insetBy(dx: 0, dy: 4)
r.size.width = animatableLayerWidth
animatableLayer.frame = r
animationContainerView.layer.addSublayer(animatableLayer)
self.view = view
}
#objc func cmPauseTapped(_ sender : UIButton){
if animatableLayer.speed == 0{
resume()
}else{
pause()
}
}
#objc func sliderChanged(_ sender: UISlider){
if animatableLayer.speed == 0{
let time = CFTimeInterval(sender.value)
animatableLayer.speed = 0
animatableLayer.timeOffset = time// offset
print("Time changed \(time)")
}
}
var animations = [CAAnimation]()
func addAnimations(){
let ani = CAAnimation()
animations.append(ani)
}
#objc func sliderDragStart(_ sender: UISlider)
{
if animatableLayer.speed > 0{
animatableLayer.speed = 0
}
addStoppedAnimation()
}
func pause(){
//just updating slider
if slider.value != Float(animatableLayer.timeOffset){
UIView.animate(withDuration: 0.3, animations: {
self.slider.setValue(Float(self.animatableLayer.timeOffset), animated: true)
})
}
animatableLayer.timeOffset = animatableLayer.convertTime(CACurrentMediaTime(), from: nil)
animatableLayer.speed = 0
}
func resume(){
if let _ = animatableLayer.animationKeys()?.contains("an animation key"){
animatableLayer.speed = 1.0;
let pausedTime = animatableLayer.timeOffset
animatableLayer.beginTime = 0.0;
let timeSincePause = animatableLayer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
animatableLayer.beginTime = timeSincePause;
return
}
print("Drag End with need to readd animation")
ani = createGroup()
animatableLayer.speed = 1
animatableLayer.add(ani, forKey: "an animation key")
let time = CFTimeInterval(slider.value)
animatableLayer.timeOffset = time
animatableLayer.beginTime = CACurrentMediaTime()
}
#objc func sliderDragEnd(_ sender: UISlider){
resume()
}
//Animations
var ani : CAAnimationGroup!
func createGroup() -> CAAnimationGroup
{
let ani = CAAnimationGroup()
ani.isRemovedOnCompletion = false
ani.duration = 10
ani.delegate = self
ani.animations = [createTranslationAnimation(),createColourAnimation()]
return ani
}
func createTranslationAnimation() -> CAKeyframeAnimation
{
let ani = CAKeyframeAnimation(keyPath: "position")
ani.delegate = self
ani.isRemovedOnCompletion = false
ani.duration = 10
ani.values = [CGPoint(x:0,y:centerY),CGPoint(x:endTranslationX,y:centerY)]
ani.keyTimes = [0,1]
return ani
}
func createColourAnimation() -> CAKeyframeAnimation
{
let ani = CAKeyframeAnimation(keyPath: "backgroundColor")
ani.delegate = self
ani.isRemovedOnCompletion = false
ani.duration = 10
ani.values = [UIColor.red.cgColor,UIColor.blue.cgColor]
ani.keyTimes = [0,1]
return ani
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print("Animation Stopped")
}
func animationDidStart(_ anim: CAAnimation) {
print("Animation started")
}
func addStoppedAnimation()
{
if let _ = animatableLayer.animationKeys()?.contains("an animation key"){
slider.value += 0.5
sliderChanged(slider)
return
//we do not want to readd it
}
ani = createGroup()
animatableLayer.speed = 0
animatableLayer.add(ani, forKey: "an animation key")
let time = CFTimeInterval(slider.value)
animatableLayer.timeOffset = time
animatableLayer.beginTime = CACurrentMediaTime()
}
#objc func playAnimation(){
addStoppedAnimation()
}
}

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

Resources