Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- Observable
- UITextView
- Human interface guide
- uiscrollview
- UICollectionView
- swift documentation
- Protocol
- SWIFT
- 리펙토링
- combine
- RxCocoa
- 클린 코드
- 애니메이션
- Xcode
- 리펙터링
- HIG
- 스위프트
- swiftUI
- MVVM
- Clean Code
- 리팩토링
- map
- uitableview
- clean architecture
- rxswift
- Refactoring
- tableView
- ios
- collectionview
- ribs
Archives
- Today
- Total
김종권의 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
- gradation을 주어서 뷰의 테두리까지 발산되게 해야하므로, conic gradation을 사용
- gradation의 종류(axial, radial, conic) 포스팅 글 참고
사전 지식 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
- layer의 색상을 애니메이션과 함께 변경하고 싶은 경우, CABasicAnimation을 사용하여 변경
- 개념은 이전 포스팅 글, CABasicAnimation을 이용한 shimmer animation 구현 참고
테두리를 돌고있는 애니메이션 구현 아이디어
- 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() 구현
- 경계선에만 색상을 넣기 위해서 CAShapeLayer 인스턴스 생성
- conic 그라데이션 효과를 주기 위해서 CAGradientLayer 인스턴스 생성 후 mask에 CAShapeLayer 대입
- 매 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되어도 타이머가 계속 살아있으므로 주의할것
'UI 컴포넌트 (swift)' 카테고리의 다른 글
Comments