이전 글에서 CABasicAnimation을 이용하여 custom property에 애니메이션을 적용하는 법을 알아보았습니다.
2022.07.04 - [iOS/기타] - iOS CABasicAnimation을 이용한 custom property 애니메이션 처리
헌데 이를 이용하기 위해서는 CALayer가 반드시 필요한데요, 때에 따라서는 CALayer 없이 custom property 애니메이션 처리가 필요한 경우도 있을 수 있습니다.
그래서, 오늘은 CABasicAnimation이나 UIView.animate를 이용하지 않고 직접 애니메이션 처리를 해 보고자 합니다.
(Android에서 제공하는 ValueAnimator와 유사하게 이용할 수 있는 animator를 만들 예정입니다.)
오늘도 프로그래스바 애니메이션을 구현해 볼 건데요, 우선 UIView에서 직접 라인을 그려보도록 하겠습니다.
class ProgressView: UIView {
override func draw(_ rect: CGRect) {
UIColor.red.setStroke()
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: rect.height / 2))
path.addLine(to: CGPoint(x: rect.width, y: rect.height / 2))
path.lineWidth = rect.height * 0.01
path.stroke()
}
}
위 코드는 다음과 같은 화면을 그리게 되는데요, 이 line을 0부터 rect.width까지 점점 늘려가며 그리면 progress가 진행되는 것처럼 애니메이션 처리를 할 수 있게 됩니다.
타이머 구성
다음과 같이 ValueAnimator란 클래스를 구성하여, 무한 동작하는 타이머를 구성해 보았습니다.
class ValueAnimator {
func start() {
// 60fps로 동작할수 있도록 timeInterval 설정
Timer.scheduledTimer(timeInterval: 1.0 / 60, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
}
@objc
func tick(_ timer: Timer) {
}
}
ValueAnimator().start()로 생성된 타이머는 60pfs로 tick()함수를 계속해서 호출해주게 되는데요, 30fps로 동작하게 하고 싶으면 timeInterval을 1.0 / 30으로 설정하면 되겠지요.
위 코드는 타이머를 무한동작시키고 있는데요, 보통 애니메이션은 언제 시작하여 얼마간의 시간만큼 동작할 것인지 설정하게 됩니다.
따라서, 이를 위해 ValueAnimator에 delay 및 duration정보를 추가해 보겠습니다.
class ValueAnimator {
var startDelay: TimeInterval = 0
var duration: TimeInterval = 0
func start() {
Timer.scheduledTimer(timeInterval: 1.0 / 60, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
startDelay += Date.timeIntervalSinceReferenceDate
}
@objc
func tick(_ timer: Timer) {
let curTime = Date.timeIntervalSinceReferenceDate
if curTime < startDelay {
// delay시간이 지난 후 애니메이션이 동작할수 있도록 함
return
}
let elapsedTime = curTime - startDelay
if elapsedTime >= duration {
// 경과시간이 duration보다 커지면 애니메이션 종료
timer.invalidate()
} else {
// 경과시간이 duration보다 작으므로 애니메이션 유지
}
}
}
위 ValueAnimator의 start()가 호출되는 순간 startDelay에 현재 시간을 더해주고 있습니다.
때문에, tick() 호출 시 현재시간과 startDelay를 비교하여, 애니메이션 요청시 설정한 delay시간만큼 애니메이션 시작을 지연시킬 수 있게 됩니다.
이후 delay시간이 지난 경우 경과시간을 구하여 duration동안만 타이머가 동작할 수 있도록 처리하면 됩니다.
value값 애니메이션 처리
이제 fromValue와 toValue를 설정하고 값의 변화를 전달받을 수 있도록 콜백을 등록할수 있도록 추가해 보겠습니다.
class ValueAnimator {
var startDelay: TimeInterval = 0
var duration: TimeInterval = 0
var fromValue: Double = 0
var toValue: Double = 0
private var callback: ((Double) -> Void)?
func start(_ callback: @escaping ((Double) -> Void)) {
// 60fps로 동작할수 있도록 timeInterval 설정
Timer.scheduledTimer(timeInterval: 1.0 / 60, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
self.callback = callback
startDelay += Date.timeIntervalSinceReferenceDate
}
@objc
func tick(_ timer: Timer) {
let curTime = Date.timeIntervalSinceReferenceDate
if curTime < startDelay {
// delay시간이 지난 후 애니메이션이 동작할수 있도록 함
return
}
let elapsedTime = curTime - startDelay
var value: Double = 0
if elapsedTime >= duration {
// 경과시간이 duration보다 커지면 value를 toValue로 설정하고 타이머 종료
value = toValue
timer.invalidate()
} else {
let v = elapsedTime / duration // duration대비 경과시간 비율 구하기
let term = toValue - fromValue // 총 얼만큼의 값이 변경되어야 하는지 계산
value = fromValue + term * v // fromValue에 경과시간에 따른 값을 더함
}
callback?(value)
}
}
기본 개념은 간단합니다.
duration대비 경과시간 비율을 구하여, toValue가 되기 위해 fromValue에 얼만큼씩 값을 변화시켜 주어야 하는지 계산 후 콜백을 호출해 주면 됩니다.
(duration = 5초, fromValue = 0, toValue = 100)으로 설정했다고 가정하면,
경과시간이 1초인 경우 fromValue에 20을 더하면 되고, 경과시간이 2.5초인 경우 fromValue에 50을 더해주면 되는 것이지요.
이렇게 만든 ValueAnimator를 다음과 같이 이용해 볼 수 있습니다.
class ProgressView: UIView {
private var progress: CGFloat = 0
override func draw(_ rect: CGRect) {
UIColor.red.setStroke()
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: rect.height / 2))
path.addLine(to: CGPoint(x: rect.width * progress, y: rect.height / 2))
path.lineWidth = rect.height * 0.01
path.stroke()
}
func trigger() {
let animator = ValueAnimator()
animator.duration = 3
animator.fromValue = 0.0
animator.toValue = 1.0
animator.start { [weak self] value in
self?.progress = value
self?.setNeedsDisplay() // 값이 변경될 때마다 UIView 갱신 요청을 한다
}
}
}
자 이렇게 프로그래스바 애니메이션이 동작하는 것을 확인할 수 있습니다.
easing 함수
tick()함수내 value를 계산하는 코드를 보면 시간에 따라 value가 일정하게 늘어나도록 계산하고 있는 것을 확인할 수 있습니다.
이렇게 경과시간 대비 값의 변화를 계산해 주는 함수는 easing함수라고 얘기하는데요, 이 함수의 종류는 굉장히 많습니다.
이는 다음 웹페이지에서 확인해 보실 수 있습니다.
Easing Functions Cheat Sheet
Easing functions specify the speed of animation to make the movement more natural. Real objects don’t just move at a constant speed, and do not start and stop in an instant. This page helps you choose the right easing function.
easings.net
위 웹페이지에서 몇 가지만 참조하여 다음과 같이 easing함수를 구현해 보았습니다.
(원하는 항목을 선택하면 제일 아래에 "수학 함수"를 확인 할 수 있습니다.)
enum Easing {
case linear
case easeOutCubic
case easeOutBounce
}
class ValueAnimator {
......
var easing = Easing.linear
......
@objc
func tick(_ timer: Timer) {
......
if elapsedTime >= duration {
// 경과시간이 duration보다 커지면 value를 toValue로 설정하고 타이머 종료
value = toValue
timer.invalidate()
} else {
let term = toValue - fromValue
value = fromValue + term * easing(elapsedTime, duration: duration)
}
callback?(value)
}
private func easing(_ elapsedTime: TimeInterval, duration: TimeInterval) -> Double {
var x = elapsedTime / duration
switch easing {
case .linear:
return x
case .easeOutCubic:
return (1 - pow(1 - x, 3))
case .easeOutBounce:
let n1 = 7.5625
let d1 = 2.75
if (x < 1 / d1) {
return n1 * x * x;
} else if (x < 2 / d1) {
x -= 1.5 / d1
return n1 * x * x + 0.75;
} else if (x < 2.5 / d1) {
x -= 2.25 / d1
return n1 * x * x + 0.9375;
} else {
x -= 2.625 / d1
return n1 * x * x + 0.984375;
}
}
}
}
easing property를 추가하여 애니메이션 요청 시 easing함수를 선택할 수 있도록 하였는데요, 각 easing함수 별 동작이 어떻게 다른지 영상으로 확인해 보시죠.
![]() |
![]() |
![]() |
차례대로 linear, easeOutCubic, easeOutBounce 순서입니다.
- linear : 경과시간에 따라 일정하게 값이 증가됩니다.
- easeOutCubic : 처음엔 급하게 증가하다가 나중에는 점점 느리게 증가합니다.
- easeOutBounce : 공이 바닥에 튀기듯이 값이 변화합니다.
다음은 지금까지 구현한 전체 코드입니다.
import UIKit
enum Easing {
case linear
case easeOutCubic
case easeOutBounce
}
class ValueAnimator {
var startDelay: TimeInterval = 0
var duration: TimeInterval = 0
var fromValue: Double = 0
var toValue: Double = 0
var easing = Easing.easeOutBounce
private var callback: ((Double) -> Void)?
func start(_ callback: @escaping ((Double) -> Void)) {
// 60fps로 동작할수 있도록 timeInterval 설정
Timer.scheduledTimer(timeInterval: 1.0 / 60, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
self.callback = callback
startDelay += Date.timeIntervalSinceReferenceDate
}
@objc
func tick(_ timer: Timer) {
let curTime = Date.timeIntervalSinceReferenceDate
if curTime < startDelay {
// delay시간이 지난 후 애니메이션이 동작할수 있도록 함
return
}
let elapsedTime = curTime - startDelay
var value: Double = 0
if elapsedTime >= duration {
// 경과시간이 duration보다 커지면 value를 toValue로 설정하고 타이머 종료
value = toValue
timer.invalidate()
} else {
let term = toValue - fromValue
value = fromValue + term * easing(elapsedTime, duration: duration)
}
callback?(value)
}
private func easing(_ elapsedTime: TimeInterval, duration: TimeInterval) -> Double {
var x = elapsedTime / duration
switch easing {
case .linear:
return x
case .easeOutCubic:
return (1 - pow(1 - x, 3))
case .easeOutBounce:
let n1 = 7.5625
let d1 = 2.75
if (x < 1 / d1) {
return n1 * x * x;
} else if (x < 2 / d1) {
x -= 1.5 / d1
return n1 * x * x + 0.75;
} else if (x < 2.5 / d1) {
x -= 2.25 / d1
return n1 * x * x + 0.9375;
} else {
x -= 2.625 / d1
return n1 * x * x + 0.984375;
}
}
}
}
class ProgressView: UIView {
private var progress: CGFloat = 0
override func draw(_ rect: CGRect) {
UIColor.red.setStroke()
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: rect.height / 2))
path.addLine(to: CGPoint(x: rect.width * progress, y: rect.height / 2))
path.lineWidth = rect.height * 0.01
path.stroke()
}
func trigger() {
let animator = ValueAnimator2()
animator.duration = 3
animator.fromValue = 0.0
animator.toValue = 1.0
animator.start { value in
self.progress = value
self.setNeedsDisplay()
}
}
}
'iOS > 기타' 카테고리의 다른 글
ios 플링 애니메이션 만들기 - 1 (0) | 2025.02.03 |
---|---|
iOS ValueAnimator 만들기 - 2 (0) | 2022.07.05 |
iOS CABasicAnimation을 이용한 custom property 애니메이션 처리 (0) | 2022.07.04 |
iOS 14 사진 접근권한 요청하기 (0) | 2022.06.29 |
WKWebView 쿠키 공유하기 (2) | 2022.06.10 |