Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> iOS

Tạo Circular Progress Bar (Progress Bar tròn) trong iOS

Circular Progress Bar rất phổ biến: hiển thị tiến trình tải, download, loading... Bài viết hướng dẫn cách tạo đơn giản, dễ hiểu nhất dùng CAShapeLayer và CABasicAnimation.

Kiến trúc

  • Track Layer: Vòng tròn nền (background track)
  • Progress Layer: Vòng tròn tiến độ (animated)
  • Cùng path tròn, khác strokeEnd (0.0 → 1.0)

Các bước

Bước 1: Tạo dự án

Xcode → Single View App → Tên: "CircularProgress".

Bước 2: Tạo Custom View Class

File → New → File → Cocoa Touch Class → Tên: CircularProgressView → Subclass: UIView.

Bước 3: Thiết kế UI

Storyboard: Kéo UIView → Set Custom Class = CircularProgressView → Thêm 3 Button: "30%", "60%", "95%".

Tạo @IBOutlet: circularProgress (CircularProgressView)

Tạo @IBAction: btn30, btn60, btn95

Tạo Circular Progress Bar (Progress Bar tròn) trong iOS

Bước 4: Code CircularProgressView.swift


import UIKit

class CircularProgressView: UIView {
    
    // MARK: - Layers
    private let progressLayer = CAShapeLayer()
    private let trackLayer = CAShapeLayer()
    
    // MARK: - Inspectable Properties (xem trên Storyboard)
    @IBInspectable var progressColor: UIColor = .systemBlue {
        didSet { progressLayer.strokeColor = progressColor.cgColor }
    }
    
    @IBInspectable var trackColor: UIColor = .systemGray5 {
        didSet { trackLayer.strokeColor = trackColor.cgColor }
    }
    
    @IBInspectable var lineWidth: CGFloat = 10 {
        didSet {
            progressLayer.lineWidth = lineWidth
            trackLayer.lineWidth = lineWidth - 5
        }
    }
    
    // MARK: - Init
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLayers()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupLayers()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        updateCircularPath()
    }
    
    // MARK: - Setup
    private func setupLayers() {
        // Track layer (nền)
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.strokeEnd = 1.0 // Vẽ đầy đủ vòng tròn
        layer.addSublayer(trackLayer)
        
        // Progress layer (tiến độ)
        progressLayer.fillColor = UIColor.clear.cgColor
        progressLayer.strokeEnd = 0.0 // Bắt đầu từ 0
        progressLayer.lineCap = .round // Đầu tròn mượt
        layer.addSublayer(progressLayer)
    }
    
    private func updateCircularPath() {
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let radius = min(bounds.width, bounds.height) / 2 - lineWidth / 2
        
        let circularPath = UIBezierPath(
            arcCenter: center,
            radius: radius,
            startAngle: -CGFloat.pi / 2, // Bắt đầu từ 12h (top)
            endAngle: 3 * CGFloat.pi / 2,
            clockwise: true
        )
        
        trackLayer.path = circularPath.cgPath
        progressLayer.path = circularPath.cgPath
    }
    
    // MARK: - Public API
    /// Animation tiến độ từ 0 đến value (0.0 - 1.0)
    func setProgress(_ value: Float, duration: TimeInterval = 1.0) {
        let clampedValue = max(0, min(1, value)) // Clamp 0-1
        
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.duration = duration
        animation.fromValue = progressLayer.strokeEnd
        animation.toValue = clampedValue
        animation.timingFunction = CAMediaTimingFunction(name: .linear)
        animation.fillMode = .forwards
        animation.isRemovedOnCompletion = false
        
        progressLayer.strokeEnd = CGFloat(clampedValue)
        progressLayer.add(animation, forKey: "progressAnimation")
    }
    
    /// Reset về 0
    func resetProgress() {
        progressLayer.strokeEnd = 0
        progressLayer.removeAnimation(forKey: "progressAnimation")
    }
}

Bước 5: Code ViewController.swift


import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var circularProgress: CircularProgressView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Tùy chỉnh màu (có thể set trên Storyboard nhờ @IBInspectable)
        circularProgress.progressColor = .systemPurple
        circularProgress.trackColor = .systemCyan
        circularProgress.lineWidth = 12
    }
    
    @IBAction func btn30(_ sender: Any) {
        circularProgress.setProgress(0.30, duration: 1.0)
    }
    
    @IBAction func btn60(_ sender: Any) {
        circularProgress.setProgress(0.60, duration: 1.0)
    }
    
    @IBAction func btn95(_ sender: Any) {
        circularProgress.setProgress(0.95, duration: 1.0)
    }
}

Kết quả

Nhấn button → Vòng tròn animate mượt từ 0 đến % tương ứng.

Tạo Circular Progress Bar (Progress Bar tròn) trong iOS

Mở rộng: Hiển thị text % ở giữa


// Thêm vào CircularProgressView
private let percentageLabel = UILabel()

private func setupLabel() {
    percentageLabel.textAlignment = .center
    percentageLabel.font = .systemFont(ofSize: 24, weight: .bold)
    percentageLabel.textColor = .label
    addSubview(percentageLabel)
    percentageLabel.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        percentageLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
        percentageLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
    ])
}

func setProgress(_ value: Float, duration: TimeInterval = 1.0, showPercentage: Bool = true) {
    // ... animation code ...
    
    if showPercentage {
        // Animate label text
        let formatter = NumberFormatter()
        formatter.numberStyle = .percent
        formatter.maximumFractionDigits = 0
        
        // Simple approach: update label at end
        DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
            self.percentageLabel.text = formatter.string(from: NSNumber(value: value))
        }
    }
}

Lưu ý

  • lineCap = .round tạo đầu tròn đẹp mắt
  • Start angle -π/2 bắt đầu từ 12h (top) - chuẩn UX
  • Clamp value 0-1 tránh crash
  • Dùng @IBInspectable để config trên Storyboard không cần code
  • Production: cân nhắc thư viện Charts hoặc CircularProgressView pod