Un thread est un fil d'exécution. Dans un programme ou une application, plusieurs fils d'exécution peuvent être exécutés en même temps.
Ça sert à quoi?
Prenons un exemple : dès qu'un utilisateur réussit son exercice, on sauvegarde son nombre de victoires. Pour cela, on doit ouvrir un fichier, lire la valeur, écrire dedans et enregistrer ce fichier. Un processeur d'iPhone peut tout à fait gérer ça facilement.
Mais supposons maintenant qu'on veuille enregistrer sur le téléphone de l'utilisateur d'autres fichiers plus gros comme des images, et d'un autre côté, dessiner des objets 3D sur l'écran. Là, le téléphone aura plus de mal. On va donc séparer l'exécution en deux.
dispatch, queues…
Apple a créé une librairie (libdispatch
) qui permet de séparer les exécutions sur un processeur multicœurs. En gros, ça permet d'exécuter des "tâches de fond".
libdispatch
implémente des queues de répartition de tâches. Les premières tâches ajoutées à une queue sont aussi les premières à être exécutées (FIFO : First In First Out).
La queue principale, réservée à l'interface, est la "main queue". Pour exécuter des tâches dans un thread différent, on les ajoute à une queue différente de la main queue. Jusqu'à présent, tout le code que l'on a écrit était exécuté sur le main thread.
Pour finir sur la théorie, on retrouve trois types de queues proposées directement par le système :
Main dispatch : on en a parlé, elle exécute des tâches sur le main thread.
Concurrent : ces queues exécutent plusieurs tâches simultanément même si ces tâches démarrent au même moment.
Serial : exécutent les tâches les unes après les autres. Les premières tâches ajoutées à la queue sont les premières à être exécutées (FIFO). On peut en créer autant que l'on veut et chacune peut exécuter SA tâche au même moment : si je crée trois queues, chaque queue exécute une tâche à la fois mais les trois peuvent opérer en même temps.
Mais vous connaissez déjà les tâches de fond : c'est ce qu'il se passe lorsqu'une appli récupère du contenu d'Internet, ou lorsqu'elle vous dit "Sauvegarde en cours…", ou encore "Chargement…".
À vos pinceaux !
Dans mon exemple, lorsque l'utilisateur entre la bonne réponse, on va sauvegarder sa victoire et dessiner sur l'écran (le processeur pourrait gérer ça sur un seul thread mais c'est pour l'exemple).
C'est vraiment du dessin?
Oui, oui ! Finalement, quand vous dessinez avec votre crayon, on pourrait dire que vous allez de point en point en le laissant appuyé sur la feuille. C'est exactement ce que l'on fait en Swift. :)
Hum… Bon ok, libre à vous de modifier un peu cette image quand vous aurez compris le principe.
On dessine sur des views (oui on fait tout sur des views, c'est comme ça).
On va récupérer les mesures de l'écran pour créer un dessin proportionné à l'écran. On ajoute cette ligne au-dessus de "class Exercice" :
let screenWidth = UIScreen.mainScreen().bounds.width, screenHeight = UIScreen.mainScreen().bounds.height
On va créer la classe VictoryView
qui hérite de UIView
(Clic droit sur First App > New File… > Swift File > VictoryView.swift > Create) :
class VictoryView : UIView
On crée un initialiseur init(frame: CGRect)
et on va directement dessiner lors de l'initialisation. Dessiner avec Swift, c'est comme dans la vraie vie : le crayon est appuyé sur la feuille ou il est relevé pour aller à un autre point. (ATTENTION, la syntaxe change beaucoup avec Swift 3, même si les méthodes sont les mêmes)
// On crée une instance du chemin suivi
let path = CGPathCreateMutable()
// On déplace le crayon
CGPathMoveToPoint(path, nil, x, y)
//On dessine un trait jusqu'au point
CGPathAddLineToPoint(path, nil, x, y)
Voilà une méthode, il suffirait d'utiliser CGPathAddLineToPoint
jusqu'à revenir au point de départ. Dans notre cas, on va plutôt utiliser un tableau de points, c'est plus joli :
J'ai calculé les coordonnées des points sur Paint.net (ou sur GIMP, c'est pareil) en divisant les coordonnées en X et en Y de chaque point par la largeur et la hauteur de l'image. Ça nous donne :
let tabPoints = [
CGPoint(x: 0.3945 * screenWidth, y: 0.2588 * screenHeight), //Point A
CGPoint(x: 0.5028 * screenWidth, y: 0.3447 * screenHeight), //Point B
CGPoint(x: 0.5651 * screenWidth, y: 0.2435 * screenHeight), //Point C
CGPoint(x: 0.6220 * screenWidth, y: 0.3529 * screenHeight), //Point D
CGPoint(x: 0.7761 * screenWidth, y: 0.2494 * screenHeight), //Point E
CGPoint(x: 0.6660 * screenWidth, y: 0.4094 * screenHeight), //Point F
CGPoint(x: 0.9174 * screenWidth, y: 0.4565 * screenHeight), //Point G
CGPoint(x: 0.6477 * screenWidth, y: 0.4929 * screenHeight), //Point H
CGPoint(x: 0.7046 * screenWidth, y: 0.6224 * screenHeight), //Point I
CGPoint(x: 0.5394 * screenWidth, y: 0.5212 * screenHeight), //Point J
CGPoint(x: 0.4092 * screenWidth, y: 0.6647 * screenHeight), //Point K
CGPoint(x: 0.3615 * screenWidth, y: 0.5141 * screenHeight), //Point L
CGPoint(x: 0.1174 * screenWidth, y: 0.5424 * screenHeight), //Point M
CGPoint(x: 0.3229 * screenWidth, y: 0.4165 * screenHeight), //Point N
CGPoint(x: 0.0881 * screenWidth, y: 0.3212 * screenHeight), //Point O
CGPoint(x: 0.3927 * screenWidth, y: 0.3541 * screenHeight) //Point P
CGPoint(x: 0.3945 * screenWidth, y: 0.2588 * screenHeight), //Point A
CGPoint(x: 0.5028 * screenWidth, y: 0.3447 * screenHeight)//Et on termine au point B
]
Et pour dessiner le chemin :
// On crée une instance du chemin suivi
let chemin = CGPathCreateMutable()
//On se déplace jusqu'au point de départ
CGPathMoveToPoint(chemin, nil, tabPoints[0].x, tabPoints[0].y)
//Et on dessine tout d'un coup
CGPathAddLines(chemin, nil, tabPoints, 18)
Avec une boucle for, ça donnerait :
let chemin = CGPathCreateMutable()
//On se déplace jusqu'au point de départ
CGPathMoveToPoint(chemin, nil, tabPoints[0].x, tabPoints[0].y)
for point in tabPoints { //On parcourt notre tableau de points
CGPathAddLineToPoint(chemin, nil, point.x, point.y)
}
Notre chemin est créé, mais c'est un peu comme si on avait essayé de dessiner avec un bout de bois sans graphite… ça ne fera rien apparaître. Pour faire apparaître le chemin, on va créer un calque de type CAShapeLayer
auquel on va appliquer notre chemin. C'est sur le calque que l'on va modifier les couleurs, le fond, etc.
let calque = CAShapeLayer()
//Couleur du fond
calque.fillColor = UIColor.whiteColor().CGColor
//Chemin
calque.path = chemin
//Position
calque.position = CGPoint(x: 0.0, y: 0.0)
//Taille (taille de l'écran)
calque.frame = CGRect(x: 0.0, y: 0.0, width: screenWidth, height: screenHeight)
//Couleur du contour
calque.strokeColor = UIColor.redColor().CGColor
//Epaisseur du contour
calque.lineWidth = 10
//Ajout du "sous-calque" au calque de base
layer.addSublayer(calque)
//Couleur de fond de la vue
backgroundColor = UIColor.whiteColor()
On regarde ce que ça donne ? Avec une petite animation pour ne rien gâcher !
Dans Exercice, j'ai ajouté ça dans clicSurFin
:
if let ans = Int(resultat) {
if ans == calcul.reponse {
let vv = VictoryView(frame: CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight))
vv.alpha = 0
self.view.addSubview(vv)
UIView.animateWithDuration(0.3, animations: {
vv.alpha = 1
})
}
}
Ajoutez une police dans votre appli
Et pour le texte maintenant?
J'ai utilisé la police Kaushan Script pour "Victoire !"
Pour ajouter une police à un projet, téléchargez-la : par exemple sur FontSquirrel, cliquez sur "Download OTF". Déplacez le fichier OTF depuis le finder vers Xcode, dans le volet de gauche, cochez "copy items if needed" et dans "add to targets", cochez "First App", puis cliquez sur Finish.
Ensuite, dans info.plist, ajoutez la ligne "Fonts provided by application" et dans "item 0", écrivez le nom du fichier de police (si vous ajoutez une deuxième police, vous devrez ajouter un autre item) :
Enfin, vous pouvez utiliser une police grâce à son nom. Parfois, le nom de la police est le même que le nom du fichier, parfois non.
On crée ensuite notre label dans VictoryView
, dans l'initialiseur, après avoir fait notre dessin :
let label = UILabel()
label.text = "Victoire !"
//Nom d'usage de la police et taille
label.font = UIFont(name: "KaushanScript-Regular", size: 25)
//Taille du label en fonction de son contenu
label.sizeToFit()
//Placement au milieu de l'étoile
label.frame.origin.y = 0.4112 * screenHeight
label.center.x = self.center.x
//Couleur du texte
label.textColor = UIColor.redColor()
addSubview(label)
Vous pouvez donc ajouter n'importe quelle police à votre appli !
Sauvegardez des données simples sur le téléphone
Quand il s'agit de sauvegarder des valeurs entières ou des petites chaînes de caractères, on utilise généralement les User Defaults. En fait, il s'agit d'un fichier de préférences système, un peu comme votre fichier Info.plist. Il n'a pas été prévu pour gérer des données mais plutôt des paramètres.
On récupère les user defaults avec NSUserDefaults.standardUserDefaults()
. On récupère des données des user defaults avec la méthode objectForKey(key:String)
qui prend comme paramètre une chaîne de caractères. Cette chaîne de caractères est en fait un identifiant unique pour la valeur à enregistrer. Ici, j'ai choisi NB_VICTOIRES
:
// Queue concurrent avec une priorité basique
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
// Exécution en tâche de fond
dispatch_async(queue, {
// On récupère les user defaults
let ud = NSUserDefaults.standardUserDefaults()
// Retourne la valeur existante ou 0
var nVict = ud.integerForKey("NB_VICTOIRES")
// On incrémente le nombre de victoires
nVict += 1
// On enregistre le nouveau nombre de victoires
ud.setInteger(nVict, forKey: "NB_VICTOIRES")
// On synchronise les user defaults pour les enregistrer
ud.synchronize()
})
Voilà les fameux threads. On reconnaît facilement l'usage de queues, ce sont les méthodes les plus bizarres.
dispatch_get_global_queue
retourne une queue de type concurrent à partir de son identifiant et de flags.dispatch_async
permet d'exécuter une instruction sur une queue donnée en paramètre.
La tâche à exécuter étant très courte, vous ne pouvez pas voir de différence mais vous savez maintenant comment lancer une tâche de fond, comme un long calcul ou une requête Internet.