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 :
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ésx
ety
.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éthodetranslation(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
deCGAffineTransform
:
let combinedTransform = transform1.concatenating(transform2)