Imaginez que vous travaillez sur une application dans laquelle vous devez effectuer périodiquement certaines actions. C'est exactement pour cela que Swift utilise la classe
Timer .
La minuterie est utilisée pour planifier des actions dans l'application. Cela peut être une action ponctuelle ou une procédure répétitive.
Dans ce guide, vous apprendrez comment le minuteur fonctionne dans iOS, comment il peut affecter la réactivité de l'interface utilisateur, comment optimiser la consommation de la batterie lors de l'utilisation d'un minuteur et comment utiliser
CADisplayLink pour l'animation.
En tant que site de test, nous utiliserons l'application - un planificateur de tâches primitif.
Pour commencer
Téléchargez le
projet source. Ouvrez-le dans Xcode, regardez sa structure, compilez et exécutez. Vous verrez le planificateur de tâches le plus simple:

Ajoutez-y une nouvelle tâche. Appuyez sur l'icône +, entrez le nom de la tâche, appuyez sur OK.
Les tâches ajoutées ont un horodatage. La nouvelle tâche que vous venez de créer est marquée de zéro seconde. Comme vous pouvez le voir, cette valeur n'augmente pas.
Chaque tâche peut être marquée comme terminée. Appuyez sur la tâche. Le nom de la tâche sera barré et il sera marqué comme terminé.
Créez notre première minuterie
Créons la minuterie principale de notre application. La classe
Timer , également connue sous le nom de
NSTimer, est un moyen pratique de planifier une action pour un moment spécifique, à la fois unique et périodique.
Ouvrez
TaskListViewController.swift et ajoutez cette variable Ă
TaskListViewController :
var timer: Timer?
Ajoutez ensuite l'extension ici:
Et collez ce code dans l'extension:
@objc func updateTimer() {
Dans cette méthode, nous:
- Vérifiez s'il existe des lignes visibles dans la table des tâches.
- Appelez updateTime pour chaque cellule visible. Cette méthode met à jour l'horodatage dans la cellule (voir TaskTableViewCell.swift ).
Ajoutez ensuite ce code Ă l'extension:
func createTimer() {
Nous voici:
- Vérifiez si le temporisateur contient une instance de la classe Timer .
- Sinon, créez une minuterie qui appelle updateTimer () toutes les secondes.
Ensuite, nous devons créer une minuterie dès que l'utilisateur ajoute la première tâche. Ajoutez
createTimer () au tout début de la
méthode presentAlertController (_ :) .
Lancez l'application et créez quelques nouvelles tâches. Vous verrez que l'horodatage de chaque tâche change à chaque seconde.

Ajouter une tolérance de minuterie
L'augmentation du nombre de minuteurs entraîne une réactivité de l'interface utilisateur plus mauvaise et une consommation de batterie plus importante. Chaque temporisateur essaie de s'exécuter exactement à l'heure qui lui est attribuée, car par défaut sa tolérance est nulle.
L'ajout d'une tolérance de minuterie est un moyen simple de réduire la consommation d'énergie. Cela permet au système d'effectuer une action de minuterie entre le temps attribué et le temps attribué
plus le temps de tolérance - mais jamais avant l'intervalle attribué.
Pour les temporisateurs qui ne s'exécutent qu'une seule fois, la valeur de tolérance est ignorée.
Dans la méthode
createTimer () , immédiatement après l'affectation du minuteur, ajoutez cette ligne:
timer?.tolerance = 0.1
Lancez l'appli. Dans ce cas particulier, l'effet ne sera pas évident (nous n'avons qu'un seul minuteur), cependant, dans la situation réelle de plusieurs minuteurs, vos utilisateurs auront une interface plus réactive et l'application sera plus économe en énergie.

Minuteries en arrière-plan
Fait intéressant, qu'arrive-t-il aux minuteries lorsqu'une application passe en arrière-plan? Pour y faire face, ajoutez ce code au tout début de la méthode
updateTimer () :
if let fireDateDescription = timer?.fireDate.description { print(fireDateDescription) }
Cela nous permettra de suivre les événements du minuteur dans la console.
Exécutez l'application, ajoutez la tâche. Appuyez maintenant sur le bouton Accueil de votre appareil, puis revenez à notre application.
Dans la console, vous verrez quelque chose comme ceci:

Comme vous pouvez le voir, lorsque l'application passe en arrière-plan, iOS suspend tous les temporisateurs d'application en cours d'exécution. Lorsque l'application devient active, iOS reprend les minuteurs.
Comprendre les boucles d'exécution
Une boucle d'exécution est une boucle d'événements qui planifie le travail et gère les événements entrants. Le cycle maintient le thread occupé pendant qu'il est en cours d'exécution et le met dans un état "en veille" lorsqu'il n'y a pas de travail pour lui.
Chaque fois que vous lancez l'application, le système crée le thread principal de l'application, chaque thread a une boucle d'exécution créée automatiquement pour lui.
Mais pourquoi toutes ces informations sont-elles importantes pour vous maintenant? Désormais, chaque temporisateur démarre dans le thread principal et rejoint la boucle d'exécution. Vous savez probablement que le thread principal est engagé dans le rendu de l'interface utilisateur, le traitement des touches, etc. Si le thread principal est occupé par quelque chose, l'interface de votre application peut devenir "sans réponse" (se bloquer).
Avez-vous remarqué que l'horodatage de la cellule n'est pas mis à jour lorsque vous faites glisser la vue du tableau?

Vous pouvez résoudre ce problème en indiquant au cycle d'exécution de démarrer les temporisateurs dans un mode différent.
Comprendre les modes de cycle d'exécution
Le mode de cycle d'exécution est un ensemble de sources d'entrée, telles que toucher l'écran ou cliquer sur la souris, qui peut être réglé pour surveiller et un ensemble «d'observateurs» qui reçoivent des notifications.
Il existe trois modes d'exécution dans iOS:
par défaut : les sources d'entrée qui ne sont pas des NSConnectionObjects sont traitées.
commun : un ensemble de cycles d'entrée est en cours de traitement, pour lequel vous pouvez définir un ensemble de sources d'entrée, de temporisations, d '"observateurs".
suivi : l'interface utilisateur de l'application est en cours de traitement.
Pour notre application, le mode le plus adapté est
courant . Pour l'utiliser, remplacez le contenu de la méthode
createTimer () par ce qui suit:
if timer == nil { let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) RunLoop.current.add(timer, forMode: .common) timer.tolerance = 0.1 self.timer = timer }
La principale différence avec le code précédent est qu'avant d'assigner le temporisateur au TaskListViewController, nous ajoutons ce temporisateur à la boucle d'exécution en mode
commun .
Compilez et exécutez l'application.

Désormais, les horodatages des cellules sont mis à jour même si le tableau défile.
Ajouter une animation pour terminer toutes les tâches
Maintenant, nous ajoutons une animation de félicitations pour que l'utilisateur termine toutes les tâches - la balle se lèvera du bas de l'écran jusqu'en haut.
Ajoutez ces variables au début du TaskListViewController:
Le but de ces variables est:
- stockage de minuterie d'animation.
- mémorisation du temps du début et de la fin de l'animation.
- durée de l'animation.
- hauteur de l'animation.
Ajoutez maintenant l'
extension TaskListViewController suivante Ă la fin du fichier
TaskListViewController.swift :
Ici, nous faisons ce qui suit:
- calculer la hauteur de l'animation, obtenir la hauteur de l'écran de l'appareil
- centrer le ballon à l'extérieur de l'écran et définir sa visibilité
- attribuer l'heure de début et de fin de l'animation
- nous démarrons la minuterie d'animation et mettons à jour l'animation 60 fois par seconde
Nous devons maintenant créer la logique réelle de mise à jour de l'animation de félicitations. Ajoutez ce code après
showCongratulationAnimation () :
func updateAnimation() {
Ce que nous faisons:
- vérifier que endTime et startTime sont attribués
- enregistrer l'heure actuelle en constante
- nous nous assurons que l'heure finale n'est pas encore arrivée. S'il est déjà arrivé, mettez à jour le chronomètre et cachez notre balle
- calculer la nouvelle coordonnée y de la balle
- la position horizontale du ballon est calculée par rapport à la position précédente
Remplacez maintenant
// TODO: Animation ici dans
showCongratulationAnimation () par ce code:
self.updateAnimation()
Maintenant,
updateAnimation () est appelée chaque fois qu'un événement timer se produit.
Hourra, nous venons de créer une animation. Cependant, lorsque l'application démarre, rien de nouveau ne se produit ...
Affichage d'une animation
Comme vous l'avez probablement deviné, il n'y a rien pour «lancer» notre nouvelle animation. Pour ce faire, nous avons besoin d'une autre méthode. Ajoutez ce code à l'
extension d' animation TaskListViewController:
func showCongratulationsIfNeeded() { if taskList.filter({ !$0.completed }).count == 0 { showCongratulationAnimation() } }
Nous appellerons cette méthode chaque fois que l'utilisateur marque la tâche terminée, il vérifie si toutes les tâches sont terminées. Si c'est le cas, il appellera
showCongratulationAnimation () .
En conclusion, ajoutez un appel à cette méthode à la fin de
tableView (_: didSelectRowAt :) :
showCongratulationsIfNeeded()
Lancez l'application, créez quelques tâches, marquez-les comme terminées - et vous verrez notre animation!

Nous arrêtons le chronomètre
Si vous regardez la console, vous verrez que, bien que l'utilisateur ait marqué toutes les tâches terminées, le minuteur continue de fonctionner. Ceci est complètement inutile, il est donc logique d'arrêter la minuterie lorsqu'elle n'est pas nécessaire.
Créez d'abord une nouvelle méthode pour arrêter le chronomètre:
func cancelTimer() { timer?.invalidate() timer = nil }
Cela mettra à jour le minuteur et le remettra à zéro afin que nous puissions le recréer correctement plus tard.
invalidate () est le seul moyen de supprimer Timer de la boucle d'exécution. La boucle d'exécution supprimera la référence de minuterie forte immédiatement après avoir appelé
invalidate () ou un peu plus tard.
Remplacez maintenant la méthode showCongratulationsIfNeeded () comme suit:
func showCongratulationsIfNeeded() { if taskList.filter({ !$0.completed }).count == 0 { cancelTimer() showCongratulationAnimation() } else { createTimer() } }
Maintenant, si l'utilisateur termine toutes les tâches, l'application va d'abord réinitialiser le minuteur puis afficher l'animation, sinon il essaiera de créer un nouveau minuteur s'il n'est pas déjà là .
Lancez l'appli.

Maintenant, la minuterie s'arrête et redémarre comme il se doit.
CADisplayLink pour une animation fluide
La minuterie n'est pas un choix idéal pour contrôler l'animation. Vous avez peut-être remarqué quelques images qui sautent l'animation, surtout si vous exécutez l'application dans le simulateur.
Nous avons réglé la minuterie à 60 Hz. Ainsi, le temporisateur met à jour l'animation toutes les 16 ms. Examinez de plus près la situation:

Lorsque vous utilisez
Timer, nous ne savons pas l'heure exacte à laquelle l'action a commencé. Cela peut se produire au début ou à la fin du cadre. Disons que la minuterie s'exécute au milieu de chaque image (points bleus sur l'image). La seule chose que nous savons avec certitude est que l'appel sera toutes les 16 ms.
Maintenant, nous n'avons que 8 ms pour exécuter l'animation, et cela peut ne pas être suffisant pour notre animation. Regardons le deuxième cadre de la figure. La deuxième image ne peut pas être terminée dans le temps imparti, donc l'application réinitialisera la deuxième image de l'animation.
CADisplayLink nous aidera
CADisplayLink est appelé une fois par image et essaie de synchroniser autant que possible les images d'animation réelles. Maintenant, vous aurez tous les 16 ms à votre disposition et iOS ne perdra pas une seule image.
Pour utiliser
CADisplayLink , vous devez remplacer
animationTimer par un nouveau type.
Remplacez ce code
var animationTimer: Timer?
sur celui-ci:
var displayLink: CADisplayLink?
Vous avez remplacé
Timer par
CADisplayLink .
CADisplayLink est une vue de minuterie liée au balayage vertical de l'affichage. Cela signifie que le GPU de l'appareil s'arrêtera jusqu'à ce que l'écran puisse continuer à traiter les commandes du GPU. De cette façon, nous obtenons une animation fluide.
Remplacez ce code
var startTime: TimeInterval?, endTime: TimeInterval?
sur celui-ci:
var startTime: CFTimeInterval?, endTime: CFTimeInterval?
Vous avez remplacé
TimeInterval par
CFTimeInterval , nécessaire pour travailler avec CADisplayLink.
Remplacez le texte de la méthode
showCongratulationAnimation () par ceci:
func showCongratulationAnimation() {
Que faisons-nous ici:
- Définissez la hauteur de l'animation, les coordonnées de la balle et la visibilité - à peu près les mêmes qu'auparavant.
- Initialisez startTime avec CACurrentMediaTime () (au lieu de Date ()).
- Nous créons une instance de la classe CADisplayLink et l'ajoutons à la boucle d'exécution en mode commun .
Remplacez maintenant
updateAnimation () par le code suivant:
- Ajoutez objc à la signature de la méthode (pour CADisplayLink, le paramètre de sélection nécessite une telle signature).
- Remplacez l'initialisation par Date () pour initialiser la date de CoreAnimation .
- Remplacez l'appel animationTimer.invalidate () par une pause de CADisplayLink et invalidez. Cela supprimera également CADisplayLink de la boucle d'exécution.
Lancez l'application!

Super! Nous avons réussi à remplacer l'animation basée sur le
minuteur par un
CADisplayLink plus approprié - et à obtenir une animation plus fluide, sans secousses.
Conclusion
Dans ce guide, vous avez compris comment fonctionne la classe
Timer dans iOS, quel est le cycle d'exécution et comment il peut rendre votre application plus réactive en termes d'interface, et comment utiliser
CADisplayLink au lieu de Timer pour une animation fluide.