Notice
Recent Posts
Recent Comments
Link
관리 메뉴

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

[iOS - Swift] Progress Button 만드는 방법 (카운트 다운 버튼) 본문

iOS 응용 (swift)

[iOS - Swift] Progress Button 만드는 방법 (카운트 다운 버튼)

jake-kim 2022. 12. 26. 22:32

Progress Button

ProgressButton

  • 특정 기능을 수행하고난 후 특정 시간내에 취소할 수 있는 카운트 다운을 시각적으로 보여주는 버튼 기능에 사용

구현 아이디어

  • 원의 둘레는 CAShapeLayer와 CABasicAnimation을 통해 그리게끔 구현
  • 애니메이션 끝난 경우 이벤트 수신은 CAAnimationDelegate를 통해 알림
  • 안의 x 이미지는 UIImage 사용

구현

  • 사용하는쪽
    • ProgressButton을 초기화하고, progress를 돌리고 싶은 경우 animate(startRatio:) 사용
class ViewController: UIViewController {
    private var progressButton: ProgressButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        progressButton = ProgressButton(radius: 50, completion: { [weak self] in
            self?.progressButton.isHidden = true
        })
        progressButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(progressButton)
        NSLayoutConstraint.activate([
            self.progressButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            self.progressButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            self.progressButton.widthAnchor.constraint(equalToConstant: 100),
            self.progressButton.heightAnchor.constraint(equalToConstant: 100),
        ])
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        self.progressButton.animate(startRatio: 0.0)
    }
}
  • ProgressButton 구현
    • 필요한 property와 초기화 부분
    • completion은 CABasicAnimation의 델리게이트인 CAAnimationDelegate의 animationDidStop에서 호출 (아래에서 계속)
final class ProgressButton: UIButton {
    static let durationSeconds = 3.0
    private let radius: CGFloat
    private let color: UIColor
    var completion: (() -> Void)?
  
    init(
        color: UIColor = .systemRed,
        radius: CGFloat = 16.0,
        completion: (() -> Void)? = nil
    ) {
        self.radius = radius
        self.color = color
        self.completion = completion
        
        super.init(frame: CGRect(x: 0, y: 0, width: radius * 2, height: radius * 2))
        
        backgroundColor = .clear
        tintColor = color
        
        imageView?.contentMode = .scaleAspectFit
        let image = UIImage(systemName: "xmark")?.withRenderingMode(.alwaysTemplate)
        setImage(image, for: .normal)
    }
}
  • 애니메이션 기능을 멈추는 cancel() 기능 구현
    • sublayer들을 돌면서, animation을 삭제하고 layer를 지우는 메소드
    func cancel() {
        layer.sublayers?.forEach { layer in
            if layer is CAShapeLayer {
                layer.removeAllAnimations()
                layer.removeFromSuperlayer()
            }
        }
    }
  • animate(startRatio:) 기능 구현
    • CAShapeLayer와 UIBezierPath로 원 둘레 path 구하기
    • "strokeEnd" 애니메이션 사용
    func animate(startRatio: CGFloat) {
        cancel()
        
        // 1. 원 둘레 path 구하기
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = UIColor.clear.cgColor
        shapeLayer.lineWidth = 1.0
        
        let circlePath = UIBezierPath(
            arcCenter: CGPoint(x: radius, y: radius),
            radius: radius,
            startAngle: 3 * .pi / 2,
            endAngle: -.pi / 2,
            clockwise: false
        )
        circlePath.lineWidth = 1
        shapeLayer.path = circlePath.cgPath
        
        // 2. 애니메이션 실행
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = startRatio
        animation.toValue = 1.0
        animation.duration = CFTimeInterval(Self.durationSeconds * (1 - startRatio))
        animation.delegate = self
        
        // 3. 원 둘레 path에다 animation 추가
        shapeLayer.add(animation, forKey: "strokeEnd")
        
        // 4. 현재 layer에 추가하여 적용
        layer.addSublayer(shapeLayer)
    }
  • CAAnimationDelegate
    • layer의 strokeColor에 색상 적용
    • 애니메이션이 끝난 경우 completion 수행
extension ProgressButton: CAAnimationDelegate {
    func animationDidStart(_ anim: CAAnimation) {
        let layer = layer.sublayers?.last as? CAShapeLayer
        layer?.strokeColor = color.cgColor
    }
    func animationDidStop(_ anim: CAAnimation, finished: Bool) {
        guard finished else { return }
        completion?()
    }
}

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

Comments