Dans ce tutoriel, nous allons créer une jolie barre de progression circulaire.

Qu’allons-nous réaliser exactement ?

Avant de commencer, laissez-moi vous montrer le rendu souhaité :

Progress bar animation

La barre doit être animée et pouvoir être mise à jour.

Comme je n’ai jamais apprécié les tutoriels qui commencent par vous demander d’allumer votre Mac, je ne vous demanderai pas de créer un nouveau projet.

Nous utiliserons deux formes — une pour la bordure, et une pour la barre de progression — et utiliserons la propriété strokeEnd pour animer les mises à jour.

La classe de la barre de progression

Votre temps est précieux, donc je vous donne la classe complète directement. Si vous avez besoin d’explications, vous les trouverez après le bloc de code.

@IBDesignable public class CircularProgressBar: UIView {
  
  private(set) var progress: Double = 0.0
  
  private var borderLayer = CAShapeLayer()
  private var progressLayer = CAShapeLayer()
  
  // MARK: Life cycle
  
  override public init(frame: CGRect) {
    super.init(frame: frame)
    createLayers()
  }
  
  required public init?(coder: NSCoder) {
    super.init(coder: coder)
    createLayers()
  }
  
  // MARK: Drawing
  
  private func createLayers() {
    let lineWidth = 2.0
    let radius = min(frame.size.width / 2, frame.size.height / 2)
    let viewCenter = CGPoint(x: frame.midX, y: frame.midY)
    let borderPath = UIBezierPath(arcCenter: componentCenter, radius: radius - lineWidth / 2, startAngle: -.pi / 2, endAngle: 3 * .pi / 2, clockwise: true)
    let progressRadius = radius - 2 * lineWidth
    let progressPath = UIBezierPath(arcCenter: componentCenter, radius: progressRadius / 2, startAngle: -.pi / 2, endAngle: 3 * .pi / 2, clockwise: true)
    borderLayer.path = circlePath.cgPath
    borderLayer.lineWidth = lineWidth
    progressLayer.path = progressPath.cgPath
    progressLayer.lineWidth = progressRadius
    progressLayer.strokeEnd = 0
    
    borderLayer.fillColor = UIColor.clear.cgColor
    borderLayer.strokeColor = .blue
    progressLayer.fillColor = UIColor.clear.cgColor
    progressLayer.strokeColor = .blue
    
    layer.addSublayer(borderLayer)
    layer.addSublayer(progressLayer)
  }
  
  // MARK: Component update
  
  func updateProgress(to percentage: Double, animated: Bool) {
    if percentage == progress { return }
    CATransaction.begin()
    if !animated {
      CATransaction.setDisableActions(true)
    }
    progressLayer.strokeEnd = CGFloat(percentage)
    CATransaction.commit()
    progress = percentage
  }
  
}

Layer paths

La première étape de la fonction createLayers sert à dessiner les calques.

Dans un premier temps, nous définissons la largeur de ligne que nous souhaitons. Ce sera utilisé pour l’épaisseur de la bordure, et l’espace entre la bordure et la barre de progression.

let lineWidth = 2.0

Ensuite, nous définissons le rayon maximum de notre composant. Il sera limité par la plus petite hauteur ou largeur de la frame. Nous stockons également le centre de la vue.

let radius = min(frame.size.width / 2, frame.size.height / 2)
let viewCenter = CGPoint(x: frame.midX, y: frame.midY)

Maintenant, dessinons la bordure — c’est la partie facile.

Le chemin sera un cercle centré autour de viewCenter.

La bordure est centrée sur le chemin. Cela signifie qu’elle dépassera à l’intérieur et à l’extérieur. C’est pourquoi le chemin doit être plus petit que le rayon souhaité de lineWidth / 2.

Border structure

Les angles sont exprimés en radians ; vous pouvez jeter un œil à la page Wikipedia si vous n’êtes pas familiers avec ce concept. Pour réaliser un cercle complet, vous devez commencer à -π et terminer à 3×π/2. Je hais les radians.

let borderPath = UIBezierPath(arcCenter: componentCenter, radius: radius - lineWidth / 2, startAngle: -.pi / 2, endAngle: 3 * .pi / 2, clockwise: true)
borderLayer.path = circlePath.cgPath
borderLayer.lineWidth = lineWidth

Ensuite, parlons de la barre de progression. Pour utiliser la propriété strokeEnd pour animer nos transitions — ce qui est extrêmement pratique — nous allons devoir utilise une ligne très épaisse plutôt qu’une couleur de remplissage.

Nous allons définir le rayon de la barre de progression, et la structure sera au final la même que la bordure ; simplement, l’épaisseur du trait sera égale au rayon de la barre. La forme sera enroulée sur elle-même, et ressemblera à un cercle rempli, alors qu’elle ne sera qu’un trait très épais.

Ensuit, nous allons initialiser la propriété strokeEnd à 0.0 — ce qui correspond à ne rien dessiner.

let progressRadius = radius - 2 * lineWidth
let progressPath = UIBezierPath(arcCenter: componentCenter, radius: progressRadius / 2, startAngle: -.pi / 2, endAngle: 3 * .pi / 2, clockwise: true)
progressLayer.path = progressPath.cgPath
progressLayer.lineWidth = progressRadius
progressLayer.strokeEnd = 0

La gestion des couleurs

La gestion des couleurs pour la bordure extérieure est simple : vous devez assigner la couleur de remplissage à .clear et choisir uniquement la couleur de la bordure.

borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = .blue

Le seul point auquel vous devez prêter attention est au progressLayer. Comme je l’ai dit précédemment, bien qu’il ressemble à un cercle rempli, il ne s’agit que d’une ligne très épaisse et enroulée sur elle-même.

C’est pourquoi pour lui également, il ne faut définir que la couleur de la bordure.

progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = .blue

Animer les mises à jour

Nous utiliserons CATransaction pour réaliser les animations. C’est très simple et ça peut être désactivé si besoin.

Le code principal des animations est :

CATransaction.begin()
progressLayer.strokeEnd = CGFloat(percentage)
CATransaction.commit()

Lorsque nous changeons la valeur de strokeEnd, la transaction va automatiquement l’animer — et oui.

Si vous souhaitez sauter l’animation, vous pouvez demander à CATransaction de les désactiver.

CATransaction.setDisableActions(true)

Pour aller plus loin

CATransaction est simple à personnaliser. Vous pouvez changer la durée de l’animation, la fonction temporelle… je vous laisser regarder la documentation d’Apple.

Les valeurs codées en dur dans le tutoriel peuvent également être changées en variables : la couleur, la taille, l’épaisseur…

Vous pouvez enfin vous abonner aux changement de la frame de la vue pour mettre à jour le composant si la taille est modifiée ; vous pouvez également créer une fonction pour animer le changement de couleur, voire changer la couleur en fonction de la valeur affichée.

La solution clef en main

Si vous aimez ce composant, vous pouvez utiliser la bibliothèque NoveCircularProgressBar. Elle fait tout ce qui est décrit dans ce tutoriel, et même plus…

Vous pouvez également jeter un œil au code source pour trouver de l’inspiration pour votre propre implémentation.