• 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 02/11/2023

Transformez votre vue

Nous avons ajouté un geste à notre vue question. Mais pour le moment, ce geste n'est pas interprété. Dans ce chapitre, nous allons interpréter le geste pour déplacer notre vue question et permettre à l'utilisateur d'y répondre. Et nous allons commencer par récupérer les informations de notre geste.

Récupérer les informations du geste

Notre geste a été passé en paramètre dans la méthode transformQuestionViewWith. Nous allons récupérer les informations qu'il contient pour déplacer notre vue en fonction du geste. La classe UIPanGestureRecognizer a une méthode translation(in: UIView). Cette méthode prend en paramètre la vue dont on veut obtenir le déplacement. Et renvoie un CGPoint qui représente le déplacement.

Récupérons donc cette translation :

private func transformQuestionViewWith(gesture: UIPanGestureRecognizer) {
    let translation = gesture.translation(in: questionView)
}

La propriété transform

Nous allons maintenant utiliser cette information de translation pour déplacer la vue. Et pour déplacer la vue, nous allons utiliser la propriété transform de UIView de type CGAffineTransform. Cette propriété a un rôle très précis, elle permet de modifier l'apparence d'une vue de trois façons différentes :

  • changer la position de la vue en lui appliquant une translation

  • changer la taille de la vue en lui appliquant une échelle

  • changer l'orientation de la vue en lui appliquant une rotation

La translation

Pour le moment, c'est la translation qui nous intéresse. Pour créer une translation, on utilise l'initialiseur dédié de CGAffineTransform :

CGAffineTransform(translationX: CGFloat, y: CGFloat)

Les valeurs x et y de la translation sont celles obtenues précisément à partir des informations du geste. Donc on peut écrire :

let translation = gesture.translation(in: questionView)
questionView.transform = CGAffineTransform(translationX: translation.x, y: translation.y)

Prenons une pause pour bien assimiler ce qu'il vient de se passer :

  1. On récupère la translation effectuée par le doigt sur l'écran dans la première ligne. Cette translation a pour type CGPoint qui est une structure que nous avons vue et qui a deux propriétés x et y.

  2. On crée une transformation de notre vue question. Cette transformation est de type translation. On lui donne les paramètres de la translation de notre doigt.

Ainsi la translation du doigt sur l'écran et la translation de la vue correspondent. Si on lance le simulateur, on peut voir que maintenant la vue suit notre doigt (la souris) :

La rotation

Allons plus loin avec cette propriété transform et amusons-nous. Nous allons appliquer une rotation à la vue selon les règles suivantes :

  • Plus on est loin du centre, plus la rotation est forte

  • Vers la droite, la vue est tournée vers la droite et inversement

Pour que la rotation ait un effet satisfaisant, nous allons appliquer une rotation de -30° quand la vue est à l'extrémité gauche de l'écran et +30° à l'extrémité droite. Si vous avez des petits restes de trigonométrie, nous allons donc faire la translation entre -π/6 et +π/6.

Nous allons commencer par récupérer la largeur de l'écran. Pour cela, on utilise la classe UIScreen et sa propriété de classe main. De cette propriété, on peut récupérer la propriété bounds que vous connaissez :

var screenWidth = UIScreen.main.bounds.width

Avec cette information, nous allons pouvoir calculer l'angle en fonction de la translation de la vue :

let translationPercent = translation.x/(UIScreen.main.bounds.width / 2)
let rotationAngle = (CGFloat.pi / 6) * translationPercent

Je calcule d'abord où je suis par rapport au bord de l'écran. La valeur translationPercent peut varier entre -100% et +100%. Et ensuite j'applique ce pourcentage à π/6.

Maintenant nous allons pouvoir créer notre transformation en utilisant cet angle de rotation. Nous allons utiliser un autre initialiseur de CGAffineTransform :

let rotationTransform = CGAffineTransform(rotationAngle: rotationAngle)

Combiner les transformations

Nous avons maintenant une transformation de rotation et une transformation de translation. Il faut combiner les deux pour obtenir la transformation complète que l'on veut affecter à notre vue. Et pour cela, nous allons utiliser la méthode concatenating de CGAffineTransform :

let transform = translationTransform.concatenating(rotationTransform)
questionView.transform = transform

Et voilà ! On n'a plus qu'à tester dans notre simulateur :

Joli, non ? :D

Changer le style

Pour compléter cette animation, il nous reste une petite chose à faire. Il faut changer le style de notre vue en fonction de sa position :

  • Si elle est à droite, il faut afficher le style réponse correcte (en vert)

  • Si elle est à gauche, il faut afficher le style réponse incorrecte (en rouge)

Et on va faire ça facilement grâce à notre belle QuestionView ! Il nous suffit de savoir si la vue est à droite ou à gauche. Et pour cela nous allons regarder la valeur x de notre translation :

  • Si elle est positive, la vue est à droite.

  • Si elle est négative, la vue est à gauche.

On écrit donc :

if translation.x > 0 {
    questionView.style = .correct
} else {
    questionView.style = .incorrect
}

Et voilà, nous avons créé une nouveau geste beau et pratique !

Pour que vous ayez la vue d'ensemble, voici le code final de notre fonction :

private func transformQuestionViewWith(gesture: UIPanGestureRecognizer) {
    let translation = gesture.translation(in: questionView)

    let translationTransform = CGAffineTransform(translationX: translation.x, y: translation.y)

    let translationPercent = translation.x/(UIScreen.main.bounds.width / 2)
    let rotationAngle = (CGFloat.pi / 3) * translationPercent
    let rotationTransform = CGAffineTransform(rotationAngle: rotationAngle)

    let transform = translationTransform.concatenating(rotationTransform)
    questionView.transform = transform

    if translation.x > 0 {
        questionView.style = .correct
    } else {
        questionView.style = .incorrect
    }
}

Répondre à la question

Notre geste est bien beau, mais lorsqu'on lâche la vue, il ne se passe rien. Et c'est parce que nous n'avons pas implémenté la deuxième méthode que nous avions préparée : answerQuestion. Alors, allons-y !

Envoyer la réponse au modèle

Tout d'abord nous allons envoyer la réponse au modèle qui va se charger de mettre à jour le score. Nous allons utiliser la méthode answerCurrentQuestion que nous avions préparée dans la classe Game.

Cette question prend en paramètre la réponse de l'utilisateur : vrai ou faux. Nous allons déduire cette réponse du style de la vue question :

private func answerQuestion() {
    switch questionView.style {
    case .correct:
        game.answerCurrentQuestion(with: true)
    case .incorrect:
        game.answerCurrentQuestion(with: false)
    case .standard:
        break
    }
}

Si la vue question est dans le style correct, l'utilisateur répond "vrai" à la question et inversement.

Mettre à jour le score

La méthode answerCurrentQuestion met à jour le score de la partie. Donc nous pouvons ensuite afficher le score mis à jour :

scoreLabel.text = "\(game.score) / 10"

Afficher la nouvelle question

Enfin, il faut afficher la question suivante. Pour cela, nous allons commencer par replacer la vue question à sa place d'origine. Pour cela, nous allons utiliser une instance spéciale de CGAffineTransform : identity. Cette transformation est la transformation identité et permet donc de ramener la vue à son état d'origine.

questionView.transform = .identity

La vue est revenue à sa place. Il faut également lui redonner son style standard pour qu'elle redevienne grise.

questionView.style = .standard

Enfin, il nous faut modifier son titre pour afficher la nouvelle question :

questionView.title = game.currentQuestion.title

Et voilà ! Nous affichons la nouvelle question après avoir enregistré la réponse de l'utilisateur.

Game Over

Minute papillon !

Bah quoi ?

Nous avons oublié de traiter un cas. Que se passe-t-il si la partie est terminée ? Nous ne pouvons plus afficher la question suivante ! L'application va planter. Donc nous devons afficher à la place que la partie est terminée. Pour cela, nous allons contrôler la propriété state de game.

switch game.state {
case .ongoing:
    questionView.title = game.currentQuestion.title
case .over:
    questionView.title = "Game Over"
}

Et voilà ! Nous indiquons à l'utilisateur que la partie est terminée ! C'est quand même mieux que l'application qui plante !

A la fin, le code de notre fonction ressemble donc à ceci :

private func answerQuestion() {
    switch questionView.style {
    case .correct:
        game.answerCurrentQuestion(with: true)
    case .incorrect:
        game.answerCurrentQuestion(with: false)
     case .standard:
        break
    }

    scoreLabel.text = "\(game.score) / 10"

    questionView.transform = .identity
    questionView.style = .standard

    switch game.state {
    case .ongoing:
        questionView.title = game.currentQuestion.title
    case .over:
        questionView.title = "Game Over"
    }
}

En résumé

  • Pour obtenir la translation du doigt sur l'écran, lors d'un UIPanGestureRecognizer, on utilise la méthode translation(in: UIView).

  • UIView a une propriété transform qui permet de changer la taille, la position et l'orientation de la vue.

  • Pour créer une transformation, nous avons vu deux initialiseurs de CGAffineTransform :

CGAffineTransform(translationX: CGFloat, y: CGFloat)
CGAffineTransform(rotationAngle: CGFloat)
  • Pour combiner deux transformations, nous avons vu la méthode concatenating de CGAffineTransform :

let combinedTransform = transform1.concatenating(transform2)
Exemple de certificat de réussite
Exemple de certificat de réussite