관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] border gradation animation, conic animation (테두리 회전 그라데이션 애니메이션) CABasicAnimation 본문

UI 컴포넌트 (swift)

[iOS - swift] border gradation animation, conic animation (테두리 회전 그라데이션 애니메이션) CABasicAnimation

jake-kim 2022. 4. 8. 03:44

사전 지식 1) CAGradientLayer

사전 지식 2) UIBezierPath, CAShapeLayer

  • UIBezierPath는 선을 그리는 역할
  • CAShapeLayer에 UIBezierPath 인스턴스를 주입하고, CAShapeLayer에서 선에대한 속성을 부여
    • 뷰가 있을 때 뷰의 테두리만 접근하여 테두리에만 특정 애니메이션을 적용시키고 싶은 경우, CAShapeLayer 인스턴스를 통해 테두리만에만 접근가능
    • 테두리만 접근하여, 테두리의 width값이나 색상 값등을 추가가 가능
  • 보통 CALayer 인스턴스의 mask 프로퍼티에 CAShapeLayer인스턴스를 주입하면, 테두리에 관한 처리가 용이
    • strokeColor: 테두리 선의 색상 (해당 색상이 clear가 되면 아예 테두리 색상이 표출안되므로 임의의 색상으로 할당)
    • fillColor: path끼리 교차하여 생기는 공간의 색상 (테두리만 신경쓰고 안에 내용은 기존 뷰의 내용을 보여줘야하므로, clear로 설정)
// CAShapeLayer 인스턴스 생성 & 테두리에 관한 속성 정의
let shape = CAShapeLayer()
shape.lineWidth = 10
shape.path = UIBezierPath(rect: self.myView.bounds).cgPath
shape.strokeColor = UIColor.white.cgColor
shape.fillColor = UIColor.clear.cgColor

// view.layer.mask에 CAShapeLayer 인스턴스 주입
self.myView.layer.mask = myShapeLayer

사전 지식 3) CABasicAnimation

테두리를 돌고있는 애니메이션 구현 아이디어

  • gradient의 conic 속성을 통해 아래처럼 gradient 형태 준비

  • 뷰 전체에 색상이 들어가 있으므로, 테두리만 적용하고싶기 때문에 CAShapeLayer 인스턴스를 만들어서 이 인스턴스를 gradient.mask에 프로퍼티 주입
gradient.mask = shapeLayer
  • Timer를 돌려서, 매초마다 CABasicAnimation(keyPath: "colors")를 이용하여 gradient 색상을 변경
    • 색상은 x0, x1, x2, x3, x2, x1과 같이 배열로 이루어져 있고, 이 색상들을 마치 circular queue처럼 돌려가면서 계속 gradient의 색상 순서를 변경해주면 돌아가는 애니메이션 구현 완료

구현

  • 필요한 상수 정의
class ViewController: UIViewController {
  private enum Color {
    static var gradientColors = [
      UIColor.systemBlue,
      UIColor.systemBlue.withAlphaComponent(0.7),
      UIColor.systemBlue.withAlphaComponent(0.4),
      UIColor.systemGreen.withAlphaComponent(0.3),
      UIColor.systemGreen.withAlphaComponent(0.7),
      UIColor.systemGreen.withAlphaComponent(0.3),
      UIColor.systemBlue.withAlphaComponent(0.4),
      UIColor.systemBlue.withAlphaComponent(0.7),
    ]
  }
  private enum Constants {
    static let gradientLocation = [Int](0..<Color.gradientColors.count)
      .map(Double.init)
      .map { $0 / Double(Color.gradientColors.count) }
      .map(NSNumber.init)
    static let cornerRadius = 30.0
    static let cornerWidth = 10.0
    static let viewSize = CGSize(width: 100, height: 350)
  }
  
}

 

  • 샘플 뷰
  private lazy var sampleView: UIView = {
    let view = UIView()
    view.backgroundColor = .clear
    view.layer.cornerRadius = Constants.cornerRadius
    view.translatesAutoresizingMaskIntoConstraints = false
    self.view.addSubview(view)
    return view
  }()
  • viewDidAppear에서 애니메이션 효과 메소드 호출
    • 런타임시 sampleView의 bounds가 결정되어야, CAShapeLayer, CAGradientLayer에 sampleView.bounds값을 사용할 수 있기 때문
    • viewDidAppear에서 해도 괜찮지만, layoutSubviews()를 오버라이딩하여 여기다 작성해도 무방
override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  
  self.animateBorderGradation() // TODO: 구현
}

// layoutSubviews를 사용하는 방법
override func layoutSubviews() {
  super.layoutSubviews()
  // 여기다 구현해도 무방
}
    • animateBorderGradation() 구현
      1. 경계선에만 색상을 넣기 위해서 CAShapeLayer 인스턴스 생성
      2. conic 그라데이션 효과를 주기 위해서 CAGradientLayer 인스턴스 생성 후 mask에 CAShapeLayer 대입
      3. 매 0.2초마다 마치 circular queue처럼 색상을 번갈아서 바뀌도록 구현
private var timer: Timer?

deinit {
  self.timer?.invalidate()
  self.timer = nil
}

func animateBorderGradation() {
  // 1. 경계선에만 색상을 넣기 위해서 CAShapeLayer 인스턴스 생성
  let shape = CAShapeLayer()
  shape.path = UIBezierPath(
    roundedRect: self.sampleView.bounds.insetBy(dx: Constants.cornerWidth, dy: Constants.cornerWidth),
    cornerRadius: self.sampleView.layer.cornerRadius
  ).cgPath
  shape.lineWidth = Constants.cornerWidth
  shape.cornerRadius = Constants.cornerRadius
  shape.strokeColor = UIColor.white.cgColor
  shape.fillColor = UIColor.clear.cgColor
  
  // 2. conic 그라데이션 효과를 주기 위해서 CAGradientLayer 인스턴스 생성 후 mask에 CAShapeLayer 대입
  let gradient = CAGradientLayer()
  gradient.frame = self.sampleView.bounds
  gradient.type = .conic
  gradient.colors = Color.gradientColors.map(\.cgColor) as [Any]
  gradient.locations = Constants.gradientLocation
  gradient.startPoint = CGPoint(x: 0.5, y: 0.5)
  gradient.endPoint = CGPoint(x: 1, y: 1)
  gradient.mask = shape
  gradient.cornerRadius = Constants.cornerRadius
  self.sampleView.layer.addSublayer(gradient)
  
  // 3. 매 0.2초마다 마치 circular queue처럼 색상을 번갈아서 바뀌도록 구현
  self.timer?.invalidate()
  self.timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { _ in
    gradient.removeAnimation(forKey: "myAnimation")
    let previous = Color.gradientColors.map(\.cgColor)
    let last = Color.gradientColors.removeLast()
    Color.gradientColors.insert(last, at: 0)
    let lastColors = Color.gradientColors.map(\.cgColor)
    
    let colorsAnimation = CABasicAnimation(keyPath: "colors")
    colorsAnimation.fromValue = previous
    colorsAnimation.toValue = lastColors
    colorsAnimation.repeatCount = 1
    colorsAnimation.duration = 0.2
    colorsAnimation.isRemovedOnCompletion = false
    colorsAnimation.fillMode = .both
    gradient.add(colorsAnimation, forKey: "myAnimation")
  }
}

cf) Timer를 사용할때는 메모릭에 주의해야하며, 전역에 timer 인스턴스를 선언해 놓고, deinit { } 될 때 timer.invalidate(), timer = nil로 초기화시켜주지 않으면, view가 deinit되어도 타이머가 계속 살아있으므로 주의할것


* 전체 코드: https://github.com/JK0369/ExBorderAnimation

Comments