iOS 13 breaks UIViewPropertyAnimator transition - ios

We have a broken transition in WeatherKit only reproducible on iOS 13 beta. We're unsure if this is an UIKit bug or we're doing something awfully wrong.
With an array of UIViewPropertyAnimator working before iOS 13, ever since iOS 13 (through all of the betas) the animation frame is not updating correctly. For example, I have an UIViewPropertyAnimator called labelAnimator which animates a label to some specific CGRect, that CGRect is not respected and the label animates somewhere else as shown in the video.
Curious enough, if I mess around with the order of the transitions in the array, the bottom sheet works fine and the only one that animates wrong is the temperature label.
Here's the code that animates that whole view:
class MainView: UIViewController {
var panGesture = UIPanGestureRecognizer()
var tapGesture = UITapGestureRecognizer()
let animationDuration: TimeInterval = 0.75
var diff: CGFloat = 150
#IBOutlet weak var gradientView: GradientView!
#IBOutlet weak var detailedViewContainer: UIView!
#IBOutlet weak var blurView: UIVisualEffectView!
override func viewDidLoad() {
self.panGesture.addTarget(self, action: #selector(MainView.handlePanGesture(gesture:)))
self.detailedViewContainer.addGestureRecognizer(self.panGesture)
self.tapGesture.addTarget(self, action: #selector(MainView.handleTapGesture(gesture:)))
self.detailedViewContainer.addGestureRecognizer(self.tapGesture)
}
enum PanelState {
case expanded
case collapsed
}
var nextState: PanelState {
return panelIsVisible ? .collapsed : .expanded
}
var panelIsVisible: Bool = false
var runningAnimations = [UIViewPropertyAnimator]()
var animationProgressWhenInterrupted: CGFloat = 0.0
#objc func handleTapGesture(gesture: UITapGestureRecognizer) {
switch gesture.state {
case .ended:
tapAnimation()
default: break
}
}
#objc func tapAnimation(){
self.panGesture.isEnabled = false
self.tapGesture.isEnabled = false
startInteractiveTransition(state: nextState, duration: animationDuration)
updateInteractiveTransition(fractionComplete: 0)
let linearTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.8, y: -0.16), controlPoint2: CGPoint(x: 0.22, y: 1.18))
continueInteractiveTransition(timingParameters: linearTiming){
self.panGesture.isEnabled = true
self.tapGesture.isEnabled = true
}
}
#objc func handlePanGesture(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
if !panelIsVisible ? gesture.velocity(in: nil).y < 0 : gesture.velocity(in: nil).y > 0 {
startInteractiveTransition(state: nextState, duration: animationDuration)
}
case .changed:
let translation = gesture.translation(in: self.detailedViewContainer)
var fractionComplete = (translation.y / view.bounds.height * 2)
fractionComplete = !panelIsVisible ? -fractionComplete : fractionComplete
updateInteractiveTransition(fractionComplete: fractionComplete)
case .ended:
let linearTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.8, y: -0.16), controlPoint2: CGPoint(x: 0.22, y: 1.18))
continueInteractiveTransition(timingParameters: linearTiming) {
self.panGesture.isEnabled = true
self.tapGesture.isEnabled = true
}
NotificationCenter.default.post(name: .resetHeaders, object: nil)
NotificationCenter.default.post(name: .disableScrolling, object: nil, userInfo: ["isDisabled": nextState == .collapsed])
default:
break
}
}
// MARK: - Animations
func animateTransitionIfNeeded(state: PanelState, duration: TimeInterval) {
if runningAnimations.isEmpty {
// MARK: Frame
var linearTiming = UICubicTimingParameters(animationCurve: .easeOut)
linearTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.1, y: 0.1), controlPoint2: CGPoint(x: 0.1, y: 0.1))
let frameAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: linearTiming)
frameAnimator.addAnimations {
switch state {
case .expanded:
self.detailedViewContainer.frame = CGRect(x: 0, y: self.diff, width: self.view.bounds.width, height: self.view.bounds.height - self.diff)
case .collapsed:
self.detailedViewContainer.frame = CGRect(x: 0, y: self.view.bounds.height - self.view.safeAreaInsets.bottom - 165, width: self.view.bounds.width, height: 200)
}
}
// MARK: Arrow
let arrowAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: linearTiming)
arrowAnimator.addAnimations {
switch state {
case .expanded:
self.leftArrowPath.transform = CGAffineTransform(rotationAngle: 15 * CGFloat.pi / 180)
self.rightArrowPath.transform = CGAffineTransform(rotationAngle: 15 * -CGFloat.pi / 180)
case .collapsed:
self.leftArrowPath.transform = CGAffineTransform(rotationAngle: 15 * -CGFloat.pi / 180)
self.rightArrowPath.transform = CGAffineTransform(rotationAngle: 15 * CGFloat.pi / 180)
}
self.leftArrowPath.center.y = self.detailedViewContainer.frame.origin.y + 15
self.rightArrowPath.center.y = self.detailedViewContainer.frame.origin.y + 15
}
// MARK: Scale
let radiusAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: linearTiming)
radiusAnimator.addAnimations{
switch state {
case .expanded:
self.gradientView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
self.gradientView.layer.maskedCorners = [.layerMaxXMinYCorner,.layerMinXMinYCorner]
self.gradientView.layer.cornerRadius = dataS.hasTopNotch ? 20 : 14
case .collapsed:
self.gradientView.transform = CGAffineTransform.identity
self.gradientView.layer.maskedCorners = [.layerMaxXMinYCorner,.layerMinXMinYCorner]
self.gradientView.layer.cornerRadius = 0
}
}
// MARK: Blur
let blurTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.5, y: 0.25), controlPoint2: CGPoint(x: 0.5, y: 0.75))
let blurAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: blurTiming)
blurAnimator.addAnimations {
switch state {
case .expanded:
self.blurView.effect = UIBlurEffect(style: .dark)
case .collapsed:
self.blurView.effect = nil
}
}
// MARK: Text
let textAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: linearTiming)
textAnimator.addAnimations({
switch state{
case .expanded:
self.tempLabel.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
self.tempLabel.frame = CGRect(origin: CGPoint(x: 15, y: self.diff / 2 - self.tempLabel.frame.height / 2), size: self.tempLabel.frame.size)
self.descriptionLabel.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
self.descriptionLabel.alpha = 0
self.descriptionLabel.transform = CGAffineTransform(translationX: 0, y: -100)
self.summaryLabel.frame = CGRect(origin: CGPoint(x: self.blurView.contentView.center.x, y: 10), size: self.summaryLabel.frame.size)
case .collapsed:
self.descriptionLabel.transform = CGAffineTransform.identity
self.descriptionLabel.alpha = 1
self.tempLabel.transform = CGAffineTransform.identity
self.tempLabel.frame = CGRect(origin: CGPoint(x: 15, y: self.view.frame.height / 2 - self.tempLabel.frame.height / 2 - 30), size: self.tempLabel.frame.size)
self.summaryLabel.frame = CGRect(origin: CGPoint(x: self.blurView.contentView.center.x, y: self.tempLabel.center.y - self.summaryLabel.frame.height / 2), size: self.summaryLabel.frame.size)
}
}, delayFactor: 0.0)
let summaryLabelTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.05, y: 0.95), controlPoint2: CGPoint(x: 0.15, y: 0.95))
let summaryLabelTimingReverse = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.95, y: 0.5), controlPoint2: CGPoint(x: 0.85, y: 0.05))
// MARK: Summary Label
let summaryLabelAnimator = UIViewPropertyAnimator(duration: duration, timingParameters: state == .collapsed ? summaryLabelTiming : summaryLabelTimingReverse)
summaryLabelAnimator.addAnimations {
switch state{
case .expanded:
self.summaryLabel.alpha = 1
case .collapsed:
self.summaryLabel.alpha = 0
}
}
radiusAnimator.startAnimation()
runningAnimations.append(radiusAnimator)
blurAnimator.scrubsLinearly = false
blurAnimator.startAnimation()
runningAnimations.append(blurAnimator)
summaryLabelAnimator.scrubsLinearly = false
summaryLabelAnimator.startAnimation()
runningAnimations.append(summaryLabelAnimator)
frameAnimator.startAnimation()
runningAnimations.append(frameAnimator)
textAnimator.startAnimation()
textAnimator.pauseAnimation()
runningAnimations.append(textAnimator)
arrowAnimator.startAnimation()
runningAnimations.append(arrowAnimator)
// Clear animations when completed
runningAnimations.last?.addCompletion { _ in
self.runningAnimations.removeAll()
self.panelIsVisible = !self.panelIsVisible
textAnimator.startAnimation()
}
}
}
/// Called on pan .began
func startInteractiveTransition(state: PanelState, duration: TimeInterval) {
if runningAnimations.isEmpty {
animateTransitionIfNeeded(state: state, duration: duration)
for animator in runningAnimations {
animator.pauseAnimation()
animationProgressWhenInterrupted = animator.fractionComplete
}
}
let hapticSelection = SelectionFeedbackGenerator()
hapticSelection.prepare()
hapticSelection.selectionChanged()
}
/// Called on pan .changed
func updateInteractiveTransition(fractionComplete: CGFloat) {
for animator in runningAnimations {
animator.fractionComplete = fractionComplete + animationProgressWhenInterrupted
}
}
/// Called on pan .ended
func continueInteractiveTransition(timingParameters: UICubicTimingParameters? = nil, durationFactor: CGFloat = 0, completion: #escaping ()->()) {
for animator in runningAnimations {
animator.continueAnimation(withTimingParameters: timingParameters, durationFactor: durationFactor)
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + animationDuration) {
completion()
}
}
}
And here's a video of the issue in iOS 13 and how it currently works in iOS 12.

I have same issue, for me UIViewPropertyAnimator continueAnimation durationFactor parameter is the issue, whenever it is not 0, after few animation the table view goes crazy.

Related

How do you avoid janky animation when using CATransform3DRotate in iOS?

I have added the code for it below, basically the animation works where I can have it animate on touching certain points but the issue is that after animation it starts transforming in a weird way. The motion animation on the other hand works fine as you move the device. I am trying to figure out how to prevent it from going from 3D to that flat image that appears afterwords.
class ViewController: UIViewController {
var redView: SpecialView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
redView = SpecialView()
redView.backgroundColor = .black
view.addSubview(redView)
let label = UILabel()
label.text = "Some Text"
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
redView.addSubview(label)
let label2 = UILabel()
label2.text = "Some Description"
label2.textColor = .white
label2.numberOfLines = 0
label2.translatesAutoresizingMaskIntoConstraints = false
redView.addSubview(label2)
redView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.trailingAnchor.constraint(equalTo: redView.trailingAnchor),
label.topAnchor.constraint(equalTo: redView.topAnchor, constant: 20),
label.leadingAnchor.constraint(equalTo: redView.leadingAnchor, constant: 20),
label2.trailingAnchor.constraint(equalTo: label.trailingAnchor),
label2.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 20),
label2.leadingAnchor.constraint(equalTo: label.leadingAnchor),
redView.heightAnchor.constraint(equalToConstant: 150),
redView.widthAnchor.constraint(equalTo: redView.heightAnchor),
redView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
redView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
addParallaxToView(vw: redView)
}
}
final class SpecialView: UIView {
func findCorner(from point: CGPoint) -> SPPerspectiveHighlightCorner? {
let width = bounds.width
let height = bounds.height
let mediumXLeft = width/4
let mediumXRight = width/2 + width/4
let mediumYTop = height/4
let mediumYBot = height/2 + height/4
switch (point.x, point.y) {
case (0...mediumXLeft, 0...mediumYTop):
return .topLeft
case (mediumXLeft...mediumXRight, 0...mediumYTop):
return .topMedium
case (mediumXRight...width, 0...mediumYTop):
return .topRight
case (0...mediumXLeft, mediumYTop...mediumYBot):
return .mediumLeft
case (mediumXRight...width, mediumYTop...mediumYBot):
return .mediumRight
case (0...mediumXLeft, mediumYBot...height):
return .bottomLeft
case (mediumXLeft...mediumXRight, mediumYBot...height):
return .bottomMedium
case (mediumXRight...width, mediumYBot...height):
return .bottomRight
default:
return nil
}
}
fileprivate func makeVector(for corner: SPPerspectiveHighlightCorner, step: CGFloat) -> Vector {
switch corner {
case .topMedium: return Vector(x: step * 2, y: 0, z: 0)
case .topRight: return Vector(x: step, y: step, z: 0)
case .mediumRight: return Vector(x: 0, y: step * 2, z: 0)
case .bottomRight: return Vector(x: -step, y: step, z: 0)
case .bottomMedium: return Vector(x: -step * 2, y: 0, z: 0)
case .bottomLeft: return Vector(x: -step, y: -step, z: 0)
case .mediumLeft: return Vector(x: 0, y: -step * 2, z: 0)
case .topLeft: return Vector(x: step, y: -step, z: 0)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
startMoving(touches: touches)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
startMoving(touches: touches)
}
func startMoving(touches: Set<UITouch>) {
guard let location = touches.first?.location(in: self) else { return }
var identity = CATransform3DIdentity
identity.m34 = -1 / 500.0
guard let highlightCorner = findCorner(from: location) else { return }
let corner = makeVector(for: highlightCorner, step: 3.14)
print(corner)
UIView.animate(withDuration: 0.5) {
self.layer.transform = CATransform3DRotate(identity, (10 * .pi) / 180, corner.x, corner.y, corner.z)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
var identity = CATransform3DIdentity
identity.m34 = -1 / 500.0
UIView.animate(withDuration: 1) {
self.layer.transform = CATransform3DRotate(identity, (0 * .pi) / 180, 1.0, 0.0, 0.0)
}
}
}
func addParallaxToView(vw: UIView) {
var identity = CATransform3DIdentity
identity.m34 = -1 / 500.0
let minimum = CATransform3DRotate(identity, (315 * .pi) / 180, 1.0, 0.0, 0.0)
let maximum = CATransform3DRotate(identity, (45 * .pi) / 180, 1.0, 0.0, 0.0)
let minimum2 = CATransform3DRotate(identity, (135 * .pi) / 90, 0, 1, 0.0)
let maximum2 = CATransform3DRotate(identity, (225 * .pi) / 90, 0, 1, 0.0)
vw.layer.transform = identity
let effect = UIInterpolatingMotionEffect(
keyPath: "layer.transform",
type: .tiltAlongVerticalAxis)
effect.minimumRelativeValue = minimum
effect.maximumRelativeValue = maximum
let effect2 = UIInterpolatingMotionEffect(
keyPath: "layer.transform",
type: .tiltAlongHorizontalAxis)
effect2.minimumRelativeValue = minimum2
effect2.maximumRelativeValue = maximum2
let groupMotion = UIMotionEffectGroup()
groupMotion.motionEffects = [effect, effect2]
vw.addMotionEffect(groupMotion)
}
struct Vector {
public var x: CGFloat
public var y: CGFloat
public var z: CGFloat
}
Seems like removing the motion effect fixed it. Seems very odd that I was facing this issue on the simulator.

Gestures not getting registered

I have a Pan and Tap gestures, They don't seem to get recognized when I try to activate them on the simulator.
I don't get any error, tried to debugging, it seems that it does assign the gestures, but won't call the methods I try to use when they happened, like it just don't recognize them at all.
This is what I have:
dismissIndicator: (Attaching the Pan recognizer to this view):
let dismissIndicator: UIView = {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = UIColor.appWhite
view.layer.cornerRadius = 15
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
let dismissImg = UIImageView(image: UIImage(named: "closeArrow"))
dismissImg.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dismissImg)
dismissImg.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
dismissImg.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
return view
}()
This is how I set the gestures:
let dimView = UIView()
let detailView = DetailsView()
var currentPost: Post? = nil
init(post: Post) {
super.init()
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleDismiss)))
let panGesture = PanDirectionGestureRecognizer(direction: .vertical(.down), target: self, action: #selector(panGestureRecognizerHandler(_:)))
detailView.addGestureRecognizer(panGesture)
setPostDetails(post: post)
}
This is handleDismiss (Which does not get called when tapping on the DimView:
#objc func handleDismiss(){
UIView.animate(withDuration: 0.5) {
self.dimView.alpha = 0
if let window = UIApplication.shared.keyWindow {
self.detailView.frame = CGRect(x: 0, y: window.frame.height, width: self.detailView.frame.width, height: self.detailView.frame.height)
}
}
}
This is my panGestureRecognizerHandler:
#objc func panGestureRecognizerHandler(_ sender: UIPanGestureRecognizer) {
let touchPoint = sender.location(in: detailView.dismissIndicator.window)
var initialTouchPoint = CGPoint.zero
switch sender.state {
case .began:
initialTouchPoint = touchPoint
case .changed:
if touchPoint.y > initialTouchPoint.y {
detailView.frame.origin.y = initialTouchPoint.y + touchPoint.y
}
case .ended, .cancelled:
if touchPoint.y - initialTouchPoint.y > 300 {
handleDismiss()
} else {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
if let window = UIApplication.shared.keyWindow {
let height = window.frame.height - 80
let y = window.frame.height - height
self.dimView.alpha = 1
self.detailView.frame = CGRect(x: 0, y: y, width: self.detailView.frame.width, height: self.detailView.frame.height)
}
})
}
case .failed, .possible:
break
default:
break
}
}
This is where I add dimView and detailView to the screen:
func showDetailView(){
if let window = UIApplication.shared.keyWindow {
dimView.backgroundColor = UIColor(white: 0, alpha: 0.5)
window.addSubview(dimView)
window.addSubview(detailView)
let height = window.frame.height - 80
let y = window.frame.height - height // This is so we can later slide detailView all the way down.
detailView.frame = CGRect(x: 0, y: window.frame.height, width: window.frame.width, height: height)
dimView.frame = window.frame
dimView.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.dimView.alpha = 1
self.detailView.frame = CGRect(x: 0, y: y, width: self.detailView.frame.width, height: self.detailView.frame.height)
})
}
}

Circular Transition, change initial circle size

I have a circular transition you can find in the code below. This transition creates an extending circle with the next view controller inside.
It works fine, but I've been trying to increase the circle size on start for a while now.
To be exact, currently on execution the circle will be drawn from zero, but I want it to have an initial size which appears immediately, from where it starts extending.
How can I change the initial size of the circle?
import UIKit
class CircularTransition: NSObject {
var circle = UIView()
var startingPoint = CGPoint.zero {
didSet {
circle.center = startingPoint
}
}
var circleColor = UIColor.white
var duration = 0.2
enum CircularTransitionMode:Int {
case present, dismiss, pop
}
var transitionMode:CircularTransitionMode = .present
}
extension CircularTransition:UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
if transitionMode == .present {
if let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to) {
let viewCenter = presentedView.center
let viewSize = presentedView.frame.size
circle = UIView()
circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
circle.layer.cornerRadius = circle.frame.size.height / 2
circle.center = startingPoint
circle.backgroundColor = circleColor
circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
containerView.addSubview(circle)
presentedView.center = startingPoint
presentedView.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
presentedView.alpha = 0
containerView.addSubview(presentedView)
UIView.animate(withDuration: duration, animations: {
self.circle.transform = CGAffineTransform.identity
presentedView.transform = CGAffineTransform.identity
presentedView.alpha = 1
presentedView.center = viewCenter
}, completion: { (success:Bool) in
transitionContext.completeTransition(success)
})
}
}else{
let transitionModeKey = (transitionMode == .pop) ? UITransitionContextViewKey.to : UITransitionContextViewKey.from
if let returningView = transitionContext.view(forKey: transitionModeKey) {
let viewCenter = returningView.center
let viewSize = returningView.frame.size
circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
circle.layer.cornerRadius = circle.frame.size.height / 2
circle.center = startingPoint
UIView.animate(withDuration: duration, animations: {
self.circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
returningView.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
returningView.center = self.startingPoint
returningView.alpha = 0
if self.transitionMode == .pop {
containerView.insertSubview(returningView, belowSubview: returningView)
containerView.insertSubview(self.circle, belowSubview: returningView)
}
}, completion: { (success:Bool) in
returningView.center = viewCenter
returningView.removeFromSuperview()
self.circle.removeFromSuperview()
transitionContext.completeTransition(success)
})
}
}
}
func frameForCircle (withViewCenter viewCenter:CGPoint, size viewSize:CGSize, startPoint:CGPoint) -> CGRect {
let xLength = fmax(startPoint.x, viewSize.width - startPoint.x)
let yLength = fmax(startPoint.y, viewSize.height - startPoint.y)
let offestVector = sqrt(xLength * xLength + yLength * yLength) * 2
let size = CGSize(width: offestVector, height: offestVector)
return CGRect(origin: CGPoint.zero, size: size)
}
}

How to move line?

How to move horizontal line in a vertical direction?
I can draw a straight horizontal line and I need to move it using long press.
How can I do that?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
if let touch = touches.first
{
let currentPoint = touch.location(in: self.view)
DrawLine(FromPoint: currentPoint, toPoint: currentPoint)
}
}
func DrawLine(FromPoint: CGPoint, toPoint: CGPoint)
{
UIGraphicsBeginImageContext(self.view.frame.size)
imageView.image?.draw(in: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
let context = UIGraphicsGetCurrentContext()
let lineWidth : CGFloat = 5
context?.move(to: CGPoint(x: FromPoint.x, y: FromPoint.y))
context?.addLine(to: CGPoint(x: FromPoint.x + 200, y: FromPoint.y))
context?.setBlendMode(CGBlendMode.normal)
context?.setLineCap(CGLineCap.round)
context?.setLineWidth(lineWidth)
context?.setStrokeColor(UIColor(red: 0, green: 0, blue: 0, alpha: 1.0).cgColor)
context?.strokePath()
imageView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
you can draw a horizontal line using UIView and can move it in vertical direction using UIView.animate method by providing the required options.
In your ViewController define the following variables:
var scanlineRect = CGRect.zero
var scanlineStartY: CGFloat = 0
var scanlineStopY: CGFloat = 0
var topBottomMargin: CGFloat = 30
var scanLine: UIView = UIView()
Call drawLine method inside viewDidLoad method and call moveVertically method on your touch event:
func drawLine() {
self.addSubview(scanLine)
scanLine.backgroundColor = UIColor(red: 0.4, green: 0.8, blue: 0.4, alpha: 1.0) // green color
scanlineRect = CGRect(x: 0, y: 0, width: self.frame.width, height: 2)
scanlineStartY = topBottomMargin
scanlineStopY = self.frame.size.height - topBottomMargin
}
func moveVertically() {
scanLine.frame = scanlineRect
scanLine.center = CGPoint(x: scanLine.center.x, y: scanlineStartY)
scanLine.isHidden = false
weak var weakSelf = scanLine
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.repeat, .autoreverse, .beginFromCurrentState], animations: {() -> Void in
weakSelf!.center = CGPoint(x: weakSelf!.center.x, y: scanlineStopY)
}, completion: nil)
}
what you can do is to draw this line on a UIView, and move the UIView upwards or to any position you want like so:
UIView.animate(withDuration: 0.3, animations: {
yourView.origin = CGPoint(yourView.frame.origin.x, yourYPosition)
}, completion: { (value: Bool) in
//do anything after animation is done
})

How to update toValue/fromValue of CABasicAnimation to be smoother when changed?

I've 2 animations applied to CAShapeLayer(let's name it pulseLayer), with this code :
let scaledAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
scaledAnimation.duration = 0.75
scaledAnimation.repeatCount = Float.infinity
scaledAnimation.autoreverses = true
scaledAnimation.fromValue = 4
scaledAnimation.toValue = 4
scaledAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
let heartBeatAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
heartBeatAnimation.duration = 0.75
heartBeatAnimation.repeatCount = Float.infinity
heartBeatAnimation.autoreverses = true
heartBeatAnimation.fromValue = 1.0
heartBeatAnimation.toValue = 1.2
heartBeatAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
pulseLayer.add(heartBeatAnimation, forKey: "heartBeatAnimation")
at some point while heartBeatAnimation is on I need to remove heart beat animation and add the scaled animation with this code :
pulseLayer.add(self.scaledAnimation, forKey: "scaledAnimation")
pulseLayer.opacity = 0.55
pulseLayer.removeAnimation(forKey: "heartBeatAnimation")
but I didn't get any smooth transition between this two animation, even with UIView.animate()
so I tried to stay with only one animation heartBeatAnimation and change its toValue fromValue to the same value to get as scaledAnimation with this code :
heartBeatAnimation.toValue = 4
heartBeatAnimation.fromValue = 4
nothing happened while the animation is beating after the animation gone and the user does some gesture to start the animation I got the scaled steady animation...!
so any ideas how to update these values to make the scaled animation smoother!
Try this and see. Complete animation and switching between both is depends upon
duration, count, fromValue and toValue properties of CABasicAnimation with UIView.animate closure
#IBOutlet var vwAnimation: UIView!
let initialScale: CGFloat = 1.0
let animatingScale: CGFloat = 2.0
let finalScale: CGFloat = 3.0
override func viewDidLoad() {
super.viewDidLoad()
addAnimation()
}
func addAnimation(){
heartBeatAnimation()
self.perform(#selector(self.switchAnimation), with: nil, afterDelay: 3.0)
}
#objc func switchAnimation(){
UIView.animate(withDuration: 0.25, animations: {
self.vwAnimation.layer.removeAnimation(forKey: "heartBeatAnimation")
self.scaledAnimation()
self.vwAnimation.layoutIfNeeded()
}) { (isCompleted) in
}
}
func scaledAnimation() -> Void {
let scaledAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
scaledAnimation.duration = 0.5
scaledAnimation.repeatCount = 0.5
scaledAnimation.autoreverses = true
scaledAnimation.fromValue = initialScale
scaledAnimation.toValue = finalScale
scaledAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
vwAnimation.layer.add(scaledAnimation, forKey: "scaledAnimation")
self.perform(#selector(self.adjustScale), with: nil, afterDelay: 0.5)
}
#objc func adjustScale(){
self.vwAnimation.layer.removeAnimation(forKey: "scaledAnimation")
let scaleTransform = CGAffineTransform(scaleX: finalScale, y: finalScale)
vwAnimation.transform = scaleTransform
}
func heartBeatAnimation() -> Void {
let heartBeatAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
heartBeatAnimation.duration = 0.5
heartBeatAnimation.repeatCount = Float.infinity
heartBeatAnimation.autoreverses = true
heartBeatAnimation.fromValue = initialScale
heartBeatAnimation.toValue = animatingScale
heartBeatAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
vwAnimation.layer.add(heartBeatAnimation, forKey: "heartBeatAnimation")
}
Here is result of above code and let me know if want changes in this result:
Without using delays and such and using CATransaction and CASpringAnimations.
import UIKit
class ViewController: UIViewController {
var shapeLayer : CAShapeLayer!
var button : UIButton!
override func viewDidLoad() {
super.viewDidLoad()
//add a shapelayer
shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width/4, height: self.view.bounds.width/4)
shapeLayer.position = self.view.center
shapeLayer.path = drawStarPath(frame: shapeLayer.bounds).cgPath
let color = UIColor(red: 0.989, green: 1.000, blue: 0.000, alpha: 1.000)
shapeLayer.fillColor = color.cgColor
self.view.layer.addSublayer(shapeLayer)
//button for action
button = UIButton(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width - 20, height: 50))
button.center = self.view.center
button.center.y = self.view.bounds.height - 70
button.addTarget(self, action: #selector(ViewController.pressed(sender:)), for: .touchUpInside)
button.setTitle("Animate", for: .normal)
button.setTitleColor(.blue, for: .normal)
self.view.addSubview(button)
}
func pressed(sender:UIButton) {
if button.titleLabel?.text == "Reset Layer"{
reset()
return
}
//perform Animation
button.isEnabled = false
button.setTitle("Animating...", for: .normal)
let heartBeatAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
heartBeatAnimation.duration = 0.5
heartBeatAnimation.repeatCount = 2
heartBeatAnimation.autoreverses = true
heartBeatAnimation.fromValue = 1.0
heartBeatAnimation.toValue = 2.0
heartBeatAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
CATransaction.begin()
CATransaction.setCompletionBlock {
//call when finished
[weak self] in
if let vc = self{
vc.scaleUpToComplete()
}
}
shapeLayer.add(heartBeatAnimation, forKey: "beatAnimation")
CATransaction.commit()
}
func scaleUpToComplete(){
let scaledAnimation = CASpringAnimation(keyPath: "transform.scale.xy")
scaledAnimation.duration = 0.7
scaledAnimation.fromValue = 1.0
scaledAnimation.toValue = 2.0
scaledAnimation.damping = 8.0
scaledAnimation.initialVelocity = 9
scaledAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
CATransaction.begin()
CATransaction.setCompletionBlock {
//set transform
[weak self] in
if let vc = self{
let scaleTransform = CATransform3DScale(CATransform3DIdentity, 2.0, 2.0, 1)
vc.shapeLayer.transform = scaleTransform
vc.shapeLayer.removeAllAnimations()
//button title and enabled
vc.button.isEnabled = true
vc.button.setTitle("Reset Layer", for: .normal)
}
}
shapeLayer.add(scaledAnimation, forKey: "scaleUp")
CATransaction.commit()
}
func reset(){
let scaledAnimation = CASpringAnimation(keyPath: "transform.scale.xy")
scaledAnimation.duration = 0.7
scaledAnimation.fromValue = 2.0
scaledAnimation.toValue = 1.0
scaledAnimation.damping = 8.0
scaledAnimation.initialVelocity = 9
scaledAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
CATransaction.begin()
CATransaction.setCompletionBlock {
//set transform
[weak self] in
if let vc = self{
let scaleTransform = CATransform3DScale(CATransform3DIdentity, 1.0, 1.0, 1)
vc.shapeLayer.transform = scaleTransform
vc.shapeLayer.removeAllAnimations()
vc.button.setTitle("Animate", for: .normal)
}
}
shapeLayer.add(scaledAnimation, forKey: "scaleDown")
CATransaction.commit()
}
func drawStarPath(frame: CGRect = CGRect(x: 0, y: 0, width: 140, height: 140)) ->UIBezierPath{
//// Star Drawing
let starPath = UIBezierPath()
starPath.move(to: CGPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.21071 * frame.height))
starPath.addLine(to: CGPoint(x: frame.minX + 0.60202 * frame.width, y: frame.minY + 0.35958 * frame.height))
starPath.addLine(to: CGPoint(x: frame.minX + 0.77513 * frame.width, y: frame.minY + 0.41061 * frame.height))
starPath.addLine(to: CGPoint(x: frame.minX + 0.66508 * frame.width, y: frame.minY + 0.55364 * frame.height))
starPath.addLine(to: CGPoint(x: frame.minX + 0.67004 * frame.width, y: frame.minY + 0.73404 * frame.height))
starPath.addLine(to: CGPoint(x: frame.minX + 0.50000 * frame.width, y: frame.minY + 0.67357 * frame.height))
starPath.addLine(to: CGPoint(x: frame.minX + 0.32996 * frame.width, y: frame.minY + 0.73404 * frame.height))
starPath.addLine(to: CGPoint(x: frame.minX + 0.33492 * frame.width, y: frame.minY + 0.55364 * frame.height))
starPath.addLine(to: CGPoint(x: frame.minX + 0.22487 * frame.width, y: frame.minY + 0.41061 * frame.height))
starPath.addLine(to: CGPoint(x: frame.minX + 0.39798 * frame.width, y: frame.minY + 0.35958 * frame.height))
return starPath
}
}

Resources