カテゴリー
iOS Swift

Core AnimationでChartをアニメーションする

import UIKit

class ViewController: UIViewController {
  // dummy y values
  let yValue: [CGFloat] = [3.0, 2, 5, 6, 1, 3]	

  // bezierPath to use as the fromValue for animation
  var oldDotPath = UIBezierPath()
  var oldLinePath = UIBezierPath()  

  // basic computed properties
  var topInset: CGFloat {
    viewHeight/2 - appAreaHeight/2
  }

  var xOffset: CGFloat {
    appAreaWidth/CGFloat(yValue.count-1)
  }
	
  var dotX: CGFloat {
    viewWidth/2 - appAreaWidth/2
  }

  var rate: CGFloat {
    appAreaHeight/yValue.max()!
  }

  var viewHeight: CGFloat {
		view.bounds.height
  }
  
  var viewWidth: CGFloat {
    view.bounds.width
  }

  var appAreaHeight: CGFloat {
    viewHeight*0.8
  }
	
  var appAreaWidth: CGFloat {
    viewWidth*0.9
  }
	
  let dotLayer = CAShapeLayer()
  let lineLayer = CAShapeLayer()
	
  lazy var button : UIButton = {
    let button = UIButton()
    button.setTitle("tap", for: .normal)
    button.setTitleColor(.blue, for: .normal)
    button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
    return button
  }()
	
  override func viewDidLoad() {
    super.viewDidLoad()
    addToView()
    drawInitialLines()
  }
	
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    layoutButton()
  }
	
  @objc func buttonTapped() {
    updatePathValues()
  }
	
  private func addToView() {
    view.addSubview(button)
  }
	
  private func layoutButton() {
    let buttonWidth: CGFloat = 100
    let buttonHeight: CGFloat = 50
    let buttonX = viewWidth/2 - buttonWidth/2
    let buttonY = viewHeight/2 + appAreaHeight/2 - buttonHeight
    button.frame = CGRect(x: buttonX, y: buttonY, width: buttonWidth, height: buttonHeight)
  }
	
  private func updatePathValues() {
    let linePath = UIBezierPath()
		
      // for dots
    let dotPath = UIBezierPath()
    let dotRadius: CGFloat = 5
		
    for i in 0..<yValue.count {
      let point = CGPoint(x: dotX + xOffset*CGFloat(i), y: topInset+appAreaHeight-rate*yValue[i])
      if i == 0 {
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
        linePath.move(to: point)
      } else {
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
        linePath.addLine(to: point)
      }
    }
		
    let dotAnimation = CABasicAnimation(keyPath: "path")
    dotAnimation.fromValue = oldDotPath.cgPath
    dotAnimation.toValue = dotPath.cgPath
    dotAnimation.duration = 0.9
		
    let lineAnimation = CABasicAnimation(keyPath: "path")
    lineAnimation.fromValue = oldLinePath.cgPath
    lineAnimation.toValue = linePath.cgPath
    lineAnimation.duration = 0.9
	
    // add animation to the layers	
    dotLayer.add(dotAnimation, forKey: "path")
    lineLayer.add(lineAnimation, forKey: "path")
	
    // persisting path changes by assigning the info	
    dotLayer.path = dotPath.cgPath
    lineLayer.path = linePath.cgPath
  }
	
  private func drawInitialLines() {
    // for lines
    let linePath = UIBezierPath()
		
    // for dots
    let dotPath = UIBezierPath()
    let dotRadius: CGFloat = 5
	
    // drawing lines and dots that fit dummy y values	
    for i in 0..<yValue.count {
      let point = CGPoint(x: dotX + xOffset*CGFloat(i), y: topInset+appAreaHeight/2)
      if i == 0 {
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
        linePath.move(to: point)
      } else {
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
        linePath.addLine(to: point)
      }		
    }
		
    // retaining the paths for the later use in animation
    oldDotPath = dotPath
    oldLinePath = linePath
		
    dotLayer.fillColor = UIColor.systemPink.cgColor
    dotLayer.lineWidth = 3
    // setting the path here. by changing the path, can animate the dots.
    dotLayer.path = dotPath.cgPath
		
    lineLayer.strokeColor = UIColor.systemCyan.cgColor
    lineLayer.fillColor = UIColor.clear.cgColor 
    lineLayer.lineWidth = 3
    lineLayer.lineJoin = .round
    lineLayer.lineCap = .round
    // setting the path here...
    lineLayer.path = linePath.cgPath
	
    // adding these layers to the view's backing layer	
    view.layer.addSublayer(lineLayer)
    view.layer.addSublayer(dotLayer)
  }
}