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 | 31 |
Tags
- RxCocoa
- Refactoring
- 리펙터링
- combine
- Observable
- collectionview
- Xcode
- HIG
- rxswift
- 애니메이션
- tableView
- uitableview
- 리펙토링
- swift documentation
- SWIFT
- UITextView
- Clean Code
- Protocol
- 스위프트
- 리팩토링
- 클린 코드
- Human interface guide
- swiftUI
- ios
- uiscrollview
- MVVM
- UICollectionView
- map
- ribs
- clean architecture
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - Swift] TimerView 구현 방법 (썸네일 테두리 회전 뷰, progress, CAShapeLayer, UIBezierPath) 본문
UI 컴포넌트 (swift)
[iOS - Swift] TimerView 구현 방법 (썸네일 테두리 회전 뷰, progress, CAShapeLayer, UIBezierPath)
jake-kim 2022. 9. 8. 22:59TimerView
- 입력한 초만큼 테두리에 stroke가 칠해지는 뷰
구현 아이디어
- UIBezierPath를 이용하면 뷰의 테두리 부분의 위치를 쉽게 구할 수 있는 점
- CAShapeLayer를 이용하면 테두리의 width값과 fillColor, strokeColor, 거기에다가 CABasicAnimation의 "strokeEnd" 애니메이션도 쉽게 사용이 가능
- 사용하는쪽에서는 단순히 아래에서 구현할 TimerView를 addSubview하고 start(duration:)하여 사용
ex) TimerView를 사용하는쪽
// ViewController.swift
private func addTimerView(on subview: UIView) {
let timerView = TimerView()
subview.addSubview(timerView)
timerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
timerView.leftAnchor.constraint(equalTo: subview.leftAnchor),
timerView.rightAnchor.constraint(equalTo: subview.rightAnchor),
timerView.bottomAnchor.constraint(equalTo: subview.bottomAnchor),
timerView.topAnchor.constraint(equalTo: subview.topAnchor),
])
timerView.start(duration: 20)
}
(사용하는쪽 ViewController.swift 전체 코드)
class ViewController: UIViewController {
let imageView = RoundImageView()
override func viewDidLoad() {
super.viewDidLoad()
self.imageView.image = UIImage(named: "dog")
self.view.addSubview(self.imageView)
self.imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.imageView.heightAnchor.constraint(equalToConstant: 300),
self.imageView.widthAnchor.constraint(equalToConstant: 300),
self.imageView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
self.imageView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.addTimerView(on: self.imageView)
}
private func addTimerView(on subview: UIView) {
let timerView = TimerView()
subview.addSubview(timerView)
timerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
timerView.leftAnchor.constraint(equalTo: subview.leftAnchor),
timerView.rightAnchor.constraint(equalTo: subview.rightAnchor),
timerView.bottomAnchor.constraint(equalTo: subview.bottomAnchor),
timerView.topAnchor.constraint(equalTo: subview.topAnchor),
])
timerView.start(duration: 20)
}
}
final class RoundImageView: UIImageView {
override func layoutSubviews() {
super.layoutSubviews()
self.clipsToBounds = true
self.layer.cornerRadius = self.bounds.height / 2.0
}
}
TimerView 구현
- 필요한 상수 선언
import UIKit
final class TimerView: UIView {
private enum Const {
static let lineWidth = 10.0
static let startAngle = CGFloat(-Double.pi / 2)
static let endAngle = CGFloat(3 * Double.pi / 2)
static let backgroundStrokeColor = UIColor.green.cgColor
static let progressStrokeColor = UIColor.gray.cgColor
}
}
- 필요한 layer 프로퍼티 선언
// TimerView.swift
private let backgroundLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.lineWidth = Const.lineWidth
layer.strokeEnd = 1
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = Const.backgroundStrokeColor
return layer
}()
private let progressLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.lineWidth = Const.lineWidth
layer.strokeEnd = 0
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = Const.progressStrokeColor
return layer
}()
- 뷰의 테두리 위치를 알기 위해서 필요한 UIBezierPath 선언
- 런타임 시에 동적으로 결정되는 frame값을 알기 위해 computed property로 선언
// TimerView.swift
private var circularPath: UIBezierPath {
UIBezierPath(
arcCenter: CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height / 2),
radius: self.frame.size.width / 2,
startAngle: Const.startAngle,
endAngle: Const.endAngle,
clockwise: true
)
}
- 외부에서 색상과 lineWidth를 결정할 수 있도록 인터페이스용도인 프로퍼티 추가
- 해당 프로퍼티들은 독립적으로 상태를 가지고 있지 않고 단순히 값을 넘겨주거나 set용도이므로 computed property로 선언
var backgroundLayerColor: CGColor? {
get { self.backgroundLayer.strokeColor }
set { self.backgroundLayer.strokeColor = newValue }
}
var progressLayerColor: CGColor? {
get { self.progressLayer.strokeColor }
set { self.progressLayer.strokeColor = newValue }
}
var lineWidth: CGFloat {
get { self.backgroundLayer.lineWidth }
set { self.backgroundLayer.lineWidth = newValue }
}
- init에서 layer들을 addSublayer
required init() {
super.init(frame: .zero)
self.backgroundColor = .clear
self.backgroundLayer.path = self.circularPath.cgPath
self.progressLayer.path = self.circularPath.cgPath
self.layer.addSublayer(self.backgroundLayer)
self.layer.addSublayer(self.progressLayer)
}
- 핵심 - 뷰의 레이아웃이 변경될때마다 부르는 layoutSublayers(of:)를 override하여 여기서 circularPath를 호출하여 frame이 결정될때 path정보를 가져와서, layer에 입력
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
self.backgroundLayer.path = self.circularPath.cgPath
self.progressLayer.path = self.circularPath.cgPath
}
- 코드 베이스로 구현할것이므로 coder 초기화는 fatalError
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
- 입력된 시간만큼 애니메이션이 동작하도록, start(duration:) 메소드 정의
func start(duration: TimeInterval) {
self.progressLayer.removeAnimation(forKey: "progress")
let circularProgressAnimation = CABasicAnimation(keyPath: "strokeEnd")
circularProgressAnimation.duration = duration
circularProgressAnimation.toValue = 1.0
circularProgressAnimation.fillMode = .forwards
circularProgressAnimation.isRemovedOnCompletion = false
self.progressLayer.add(circularProgressAnimation, forKey: "progress")
}
'UI 컴포넌트 (swift)' 카테고리의 다른 글
[iOS - swift] extendable tableView 구현 방법 (동적으로 늘어나는 셀 구현), reloadRows(at:with:) (0) | 2022.12.25 |
---|---|
[iOS - Swift] tableView, collectionView 스크롤 시 상단 뷰 흐리게 하는 방법 (네이버 웹툰 상단 뷰, Sticky Header) (0) | 2022.11.27 |
[iOS - swift] Wave Animation (웨이브 애니메이션) (2) | 2022.06.30 |
[iOS - swift] BottomSheet 구현 방법 (bottom sheet, floating panel) (0) | 2022.06.29 |
[iOS - swift] 인스타그램 썸네일 프로필 UI 구현 방법 (0) | 2022.06.28 |
Comments