Rating Stars

Rating Stars

5 étoiles pour noter

Chaque étoile est un bouton. Quand une étoile est sélectionnée, sa couleur et celle des étoiles précédentes, changent.

Création des 5 boutons

1 - Ajouter un nouveau fichier "RatingControl.swift"" de type UIStackView. Ajouter les deux initialisateurs

//MARK: - Initialization

        //initialise la vue crée en code
    override init(frame: CGRect) {
        super.init(frame: frame)

    }
        //initialise la vue crée sur le storyboard
    required init(coder: NSCoder) {
        super.init(coder: coder)

    }

2 - Dans le storyboard, ajouter une "Horizontal Stack View", et dans l'inspecteur d'identité lui affecter la class "RatingControl"

3 - Dans le RatingControl.swift, créé un tableau "ratingButtons" pour stocker les 5 boutons

//MARK: Properties
    private var ratingButtons = [UIButton]()

Générer 5 boutons avec la méthode "setupButtons()"

//MARK: Private Methods

    private func setupButtons() {

                //create button and add to array
        for _ in 0..<5 {     //half-open range operator ..<
                //create button
            let button = UIButton()
            button.backgroundColor = UIColor.red   

                //add constraints
            button.translatesAutoresizingMaskIntoConstraints = false  
            button.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
            button.widthAnchor.constraint(equalToConstant: 44.0).isActive = true

                //button action

                //add button to the stack
            addArrangedSubview(button)

                //add new button to the rating button array
            ratingButtons.append(button)
        }
    }

4 - Action du bouton, ajouter une méthode "ratingButtonTapped()"

//MARK: Button Action

    @Objc func ratingButtonTapped(button: UIButton) {
        print("Button pressed")
    }

Appeler cette méthode dans "setupButtons()"

        //button action
button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(button:)), for: .touchUpInside)

Appeler la méthode "SetupButtons()" dans chaque initialisateurs.

5 -Dans le storyboard, sélectionner la stack view, et dans l'inspecteur d'attributs préciser l'espace (Spacing) à 8. Si l'application est lancée, un appui sur chacun d'eux provoque un affichage dans la console.

Capture d’écran 2021-03-10 à 11.25.22.png

@IBDesignable

Dans le stroyboard les boutons rouges n'apparaissent pas dans le canvas. Pour y remédier, il faut permettre à l'interface builder (IB) de prendre une copie du controlleur qui a créé les boutons. C'est le rôle de l'attribut @IBDesignable.

6 - Ajouter @IBDesignable devant la class "RatingControl"

@IBDesignable class RatingControl: UIStackView {

Rebuilder, les boutons apparaissent maintenant dans le storyboard.

Il est également possible d'ajouter des propriétés dans Attributes Inspector du storyboard, avec @IBInspectable

@IBInspectable

La taille des boutons ainsi que leur nombre, vont être définis par des variables. C'est plus simple de modifier ces paramètres par la suite quand ils sont en début de fichier.

7 - Dans le fichier RatingController.swift, ajouter dans la //MARK: Properties

    @IBInspectable var starSize: CGSize = CGSize(width: 44.0, height: 44.0)
    @IBInspectable var starCount: Int = 5

La méthode "setupButtons()" peut être refactoriser en remplacant le "5 " de la boucle par starCount, et les constraints "44.0 " par starSize.width et starSize.height

for _ in 0..<starCount {
...
 button.heightAnchor.constraint(equalToConstant: starSize.height).isActive = true
 button.widthAnchor.constraint(equalToConstant: starSize.width).isActive = true
...

8 - Ces deux valeurs apparaissent maintenant également dans l'Inspector du storyboard, mais ne sont pas interactives avec le controller. Pour que le changement de ces valeurs au niveau du storyboard soit également considérer au niveau du controller qui gére les boutons, il faut réinitialiser ce controller à chaque changement. Pour ce faire, il faut ajouter des "Observateurs de propriétés", property observer.

    @IBInspectable var starSize: CGSize = CGSize(width: 44.0, height: 44.0) {
        didSet { setupButtons() }
    }

    @IBInspectable var starCount: Int = 5 {
        didSet { setupButtons() }
    }

9 - Maintenant un changement de ces valeurs initialise de nouveaux boutons. Mais les anciens ne sont pas effacés. Pour ne considérer que la dernière version des boutons, au début de la méthode "setupButtons()", on ajoute :

    // clear any existing buttons
    for button in ratingButtons {
        removeArrangedSubview(button)
        button.removeFromSuperview()
    }
    ratingButtons.removeAll()

Images des Boutons

Dans Assets.xcassets du projet, ajouter (+) un nouveau dossier (Folder) "Rating Images". Faire glisser les images dans ce dossier, en positionnant les images en 2x. Il y a une étoile vide, une étoile remplie, et une étoile bleue

10 - Récupérer ces images dans la méthode "setupButtons" juste avant la boucle

            //load button images
        let bundle = Bundle(for: type(of: self))
            //on peut utiliser UIImage(named:) - mais pour que les stars apparaissent dans le storyboard, on précise bundle
        let filledStar = UIImage(named: "filledStar", in: bundle, compatibleWith: self.traitCollection)
        let emptyStar = UIImage(named: "emptyStar", in: bundle, compatibleWith: self.traitCollection)
        let highlightedStar = UIImage(named: "highlightedStar", in: bundle, compatibleWith: self.traitCollection)
        // traitCollection for the current environment of the objet

Et remplacer la couleur "red" par les images

    // Set the button images
    button.setImage(emptyStar, for: .normal)
    button.setImage(filledStar, for: .selected)
    button.setImage(highlightedStar, for: .highlighted)
    button.setImage(highlightedStar, for: [.highlighted, .selected])

Les boutons ont 5 états (normal, highlighted, focused, selected, disabled). Au départ les étoiles sont vides, état normal. Si une étoiles est sélectionnée, elle devient remplie.

Action des boutons étoiles

11 - Chaque étoile va correspondre à une note, ajouter une propriété rating

 var rating = 0

Cette note sera définit a partir de l'index du bouton, auquel on ajoute 1 pour commencer la notation à 1. La méthode "ratingButtonTapped()" devient :

@objc func ratingButtonTapped(button: UIButton) {
        guard let index = ratingButtons.firstIndex(of: button) else {
            fatalError("The button, \(button), is not in the ratingButtons array: \(ratingButtons)")
        }

                // Calculate the rating of the selected button
        let selectedRating = index + 1

        if selectedRating == rating {
                // If the selected star represents the current rating, reset the rating to 0.
            rating = 0
        } else {
                // Otherwise set the rating to the selected star
            rating = selectedRating
        }
        }

Apparence des boutons étoiles

12 - Quand une étoile est sélectionnée, son apparence change. Mais également l'apparence des étoiles précédentes. Pour représenter une note de 3, les 3 premières étoiles doivent avoir un aspect rempli.

Ajouter une nouvelle méthode "updateButtonSelectionStates()", qui va changer l'aspect de l'étoile en fonction de son état, et de l'étoile sélectionnée

    private func updateButtonSelectionStates() {
        for (index, button) in ratingButtons.enumerated() {
            // If the index of a button is less than the rating, that button should be selected.
            button.isSelected = index < rating
        }
    }

Un bouton sélectionné définit la note (rating). Dans cette méthode, la boucle inspecte chaque bouton du tableau. Si son index est inférieur à la note, il est considéré comme sélectionné.

Appeler cette dernière méthode à la fin de la méthode "setupButtons()".

...
                  //add new button to the rating button array
            ratingButtons.append(button)
         }
         updateButtonSelectionStates()
    }

Créer un observateur de propriété pour la propriété "rating"

    var rating = 0 {
        didSet {
            updateButtonSelectionStates()
        }
    }

La note peut être récupéré et affiché 3/5

Informations d'accessibilité pour VoiceOver

Les fonctionnalités d'accessibilité (Switch Control, lecture vidéo, ...) sont intégrées à iOS et ne necessite pas de code supplémentaire. Sauf VoiceOver qui lit l'écran pour les utilisateurs malvoyants. Notre vue personnalisée doit fournir trois informations supplémentaires pour chaque bouton :

  • Accessibility label : description de l'objet ("Définir la note 1 étoile")
  • Accessibility value : valeur actuelle (si la note est 4 étoiles, "4 étoiles définies")
  • Accessibility hint : description du résultat ("appuyer pour réinitialiser la note à zéro")

1 - Ajout de Accessibility label : Dans la méthode "setupButtons", au niveau de la loop, remplacer l'underscore "_" par "index"

for index in 0..<starCount {

et ajouter après les contraintes

    // Set the accessibility label
    button.accessibilityLabel = "Set \(index + 1) star rating"

2 - Ajout de Accessibility value et Accessibility hint Dans la méthode "updateButtonSelectionStates()", après "button.isSelected", ajouter :

           // Set the hint string for the currently selected star
    let hintString: String?
    if rating == index + 1 {
        hintString = "Appuyer pour remettre la note à zéro."
    } else {
        hintString = nil
    }

           // Calculate the value string
    let valueString: String
    switch (rating) {
    case 0:
        valueString = "Pas de note."
    case 1:
        valueString = "Noter une étoile."
    default:
        valueString = "Noter \(rating) étoile."
    }

         // Assign the hint string and value string
    button.accessibilityHint = hintString
    button.accessibilityValue = valueString