Projet: Aquarium – POO et protocoles

Contenu

aquarium-b2

 

 
 


 

 Note de mise à jour de Xcode 6.0 vers 6.4

Remplacer

par


 

Description

Objectif du laboratoire

Introduire aux concepts de la programmation orientée objet (POO) sous le langage Swift ainsi qu’à la notion de programmation de protocoles.

Contexte de réalisation

Grace à Xcode, au langage Swift, à la POO sous Swift, à une suite d’images d’animation, à un protocole personnalisé, au concept de délégué, à un temporisateur, à la gestuelle d’écran, aux propriétés du cadre d’une vue, à la hiérarchie ‘vue/sous-vues’, nous bâtirons une application qui propose un micro système de type ‘aquarium/poissons à nourrir/plantes produisant de l’oxygène’.

Vidéo de l’application complétée

[embedyt]http://www.youtube.com/watch?v=JhlEASo1AqM[/embedyt]


Le storyBoard de la version finale

 
aquarium-b1
 
Projet-Aquarium-depart


 

Étape 1 – Construction d’une classe personnalisée en Swift

 
Action 1.1 – Ouvrons et exécutons le projet de départ.
aquarium.04
 
 
Action 1.2 – Examinons la console de Xcode
aquarium.05
 
Action 1.3 – Analysons le code de la classe de la scène principale.

//
//  ViewController.swift
//  Classe - Aquarium
//
//  Created by Alain on 2014-10-11.
//  Copyright (c) 2014 Production sur support. All rights reserved.
//
//  ============================================================================================
//  À l'usage exclusif des étudiants et étudiantes de
//  Techniques d'Intégration Multimédia
//  du cégep de Saint-Jérôme.
//  --------------------------------------------------------------------------------------------
//  Il est interdit de reproduire, en tout ou en partie, à des fins commerciales,
//  le code source, les scènes, les éléments graphiques, les classes et
//  tout autre contenu du présent projet sans l’autorisation écrite de l'auteur.
//
//  Pour obtenir l’autorisation de reproduire ou d’utiliser, en tout ou en partie,
//  le présent projet, veuillez communiquer avec:
//
//  Alain Boudreault, aboudrea@cstj.qc.ca
//
//  ============================================================================================
import UIKit
class ViewController: UIViewController{
    // ***********************************************************************
    // Définition des IBOutlet
    // ***********************************************************************
    @IBOutlet weak var labelNBPoissons: UILabel!
    @IBOutlet weak var niveauNourriture: UIProgressView!
    @IBOutlet weak var finPartie: UIView!
    // ***********************************************************************
    // Définition des propriétés de la classe ViewController
    // ***********************************************************************
    let maximumNourritureDisponible = 100
    let unePortionDeNourriture      = 20
    var nourritureDisponible        = 0
    let nbPoissonsDepart            = 5
    var nbPoissons                  = 0
    var modelesPoisson              = [["nom":"p11", "nbImages":"8"],["nom":"p12", "nbImages":"5"], ["nom":"p13", "nbImages":"8"]]
    // ***********************************************************************
    // Définition des IBAction
    // ***********************************************************************
    @IBAction func recommencer(sender: AnyObject) {
        println("recommencer")
        commencerLaPartie()
    } // recommencer
    @IBAction func ajouterNourriture(sender: AnyObject) {
        println("ajouterNourriture")
        // Ajouter à la nourriture disponible un poucentage de la quant actuelle.
        nourritureDisponible += Int(arc4random_uniform(UInt32(nourritureDisponible)))
        // S'assurer que la quant de nourriture ne dépasse pas le maximum
        nourritureDisponible = nourritureDisponible >  maximumNourritureDisponible ? maximumNourritureDisponible : nourritureDisponible
        actualiserProgressViewQuantNourriture()
    }  // ajouterNourriture
    // ***********************************************************************
    // Méthodes de la classe ViewController
    // ***********************************************************************
    override func viewDidLoad() {
        println("viewDidLoad")
        super.viewDidLoad()
        // Ajouter des bulles
        // TODO
        // Préparer l'aquarium
        commencerLaPartie()
    } // viewDidLoad
    /**
      Préparer la scène et placer les poissons.
    */
    func commencerLaPartie(){
        println("commencerLaPartie")
        // Masquer la View de fin de la partie
        finPartie.hidden = true
        nourritureDisponible = maximumNourritureDisponible
        actualiserProgressViewQuantNourriture()
        nbPoissons = nbPoissonsDepart
        actualiserLabelNBPoissons()
        // Ajouter des poissons
        ajouterLesPoissons(nbPoissons)
    } // commencerLaPartie
    /**
      Réactualiser l'indicateur du nombres de poissons.
    */
    func actualiserLabelNBPoissons(){
        println("actualiserLabelNBPoissons")
        labelNBPoissons.text = "\(nbPoissons)"
    } // actualiserLabelNBPoissons
    /**
        Réactualiser l'indicateur quantité de nourriture.
    */
    func actualiserProgressViewQuantNourriture(){
        println("actualiserProgressViewQuantNourriture")
        niveauNourriture.progress = Float(nourritureDisponible) / 100.0
    } // actualiserQuantNourriture
    /**
    Créer des poissons et les ajouter à la scène principale.
    */
    func ajouterLesPoissons(quantite:Int){
        println("ajouterLesPoissons")
        // TODO
    } // ajouterLesPoissons
    override func didReceiveMemoryWarning() {
        println("didReceiveMemoryWarning")
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    } // didReceiveMemoryWarning
}  // Fin de la classe ViewController

 
Action 1.4 – Ajoutons un nouveau fichier Swift au projet.
aquarium.06 aquarium.07
 
Action 1.5 – Ajoutons le code suivant au fichier Poisson.Swift:

/**
  Classe permettant de créer un gentil petit poisson rouge...
*/
class Poisson {
    // Propriétés
    // Constructeur: init()
    // Destructeur: deinit
    // Méthodes
} // class Poisson

 
Action 1.5 – Ajoutons le code suivant à la méthode ViewDidLoad:
Remarquez, notre commentaire apparaît dans le module de complétion du code:
aquarium2.1
 

        let unPoisson = Poisson()

 
 
Voilà, nous venons de créer notre premier poisson! Par contre, c’est un poisson pas très utile car il n’a ni méthode ni propriété.


La méthode init() – constructeur

 
Action 1.6 – Ajoutons le code suivant à la classe Poisson

    // Constructeur
    init() {
       println("Je suis le constructeur de Poisson.")
    } // init

 
Action 1.7 – Exécutons l’application et analysons la console d’Xcode.
 
aquarium.08
 
Action 1.8 – Ajoutons le code suivant à la classe Poisson

    // Propriétés
    /// Une chaine décrivant le poisson.
    var nomPoisson:String
    /// Le nom du fichier qui contient l'image.
    var nomImage:String
    var force:Int
    let seuilDangerDeMort = 10

 
Action 1.9 – Observons l’erreur suivante:
aquarium.09
Explication:  En Swift, à moins d’être de type optionnel (uneVar?), les propriétés doivent-être initialisées avec du contenu, comme dans le cas de la propriété ‘seuilDangerDeMort’.  Cette initialisation est faite dans le constructeur, règle générale, à partir des paramètres reçus.
Action 1.10 – Modifions la classe Poisson.

import Foundation
class Poisson {
    // Propriétés
    var nomPoisson:String
    var nomImage:String
    var force:Int
    let seuilDangerDeMort = 10
    // Constructeur
    /**
      Construire un poisson à partir d'un nom et d'une force
    */
    init(nomPoisson:String, force:Int) {
       println("Je suis le constructeur de Poisson.")
       self.nomPoisson  = nomPoisson
       self.force       = force
       self.nomImage    = "n/a"
    } // init()
    // Méthodes
    /**
      Je retourne une jolie description du poisson
    */
    func quiSuisJe() ->String {
        return "\n------------------\nJe suis le poisson \(nomPoisson) et j'ai une force de \(force)\n"
    } // quiSuisJe()
} // class Poisson

 
Action 1.11 – Ajoutons le code suivante à la méthode ViewDidLoad:

        let unPoisson = Poisson(nomPoisson: "Bob", force: 99)
        println( unPoisson.quiSuisJe() )

 
Action 1.11b – Construisons une nouvelle classe à partir de la classe Poisson

class PoissonVolant: Poisson {
    var hauteurDuVol:Int
    init(nomPoisson: String, force: Int, hauteurDuVol:Int) {
        // Note: Il faut initialiser les propriétés avant l'appel de la super classe.
        self.hauteurDuVol = hauteurDuVol
        super.init(nomPoisson: nomPoisson, force: force)
    } // init
} // PoissonVolant

 
Action 1.11d – Testons avec le code suivant:

       let unPoissonVolant = PoissonVolant(nomPoisson: "Exocoetidae", force: 99, hauteurDuVol: 66)
        println(unPoissonVolant.quiSuisJe())
/// Résultat:
------------------
Je suis le poisson Exocoetidae et j'ai une force de 99

 
Action 1.11e – Surchargeons la méthode description()

    /**
    Je retourne une jolie description du poisson volant
    */
    override func quiSuisJe() ->String {
        return "\n------------------\nJe suis une poisson volant de type:\(nomPoisson) et je saute à \(hauteurDuVol) pied(s)\n"
    } // quiSuisJe()
/// Résultat:
------------------
Je suis une poisson volant de type:Exocoetidae et je saute à 66 pied(s)

 


Référence versus valeur

Le language Swift propose deux façons de regrouper des propriétés et des méthodes: soit par le mot clé ‘class’ ou le mot clé ‘struct’.
Par exemple, il est possible de créer un ensemble ‘propriétés/méthodes’ Poisson de la façon suivante:

/**
  Structure Poisson.  Les instances de ce type seront copiées lorsque passées en paramètre à une fonction ou une méthode.
*/
struct Poisson {
    // Propriétés
    // Constructeur: init()
    // Destructeur: deinit
    // Méthodes
} // struct Poisson

La différence étant que les instances de classe sont passées en référence – c-a-d, le paramètre reçu est l’instance originale de l’objet,  alors que les structures sont passées par copie de l’objet.
 


Visibilité des éléments d’une classe – (Class Access Control)

public

Visible partout.

internal (par défaut)

Visible dans tous les fichiers du projet (app) ou de la librairie.

private

Visible seulement dans le fichier courant.
Exemple,

class Poisson {
    public var nomPoisson:String
    var nomImage:String  // internal par défaut
    private var force:Int
    private let seuilDangerDeMort = 10
    // internal par défaut
    init(nomPoisson:String, force:Int) {
       println("Je suis le constructeur de Poisson.")
       self.nomPoisson  = nomPoisson
       self.force       = force
       self.nomImage    = "n/a"
    } // init()
    private func lireForce()-> Int{
       return self.force
    }
    public func quiSuisJe() ->String {
        return "Poisson de force: \(lireForce())"
    } // quiSuisJe()
} // class Poisson


Accesseurs et mutateurs – get/set (computed properties)

Attention: Ne pas modifier votre classe ‘poisson’

//
//  Poisson.swift
//  Projet - Aquarium
//
//  Created by Alain on 15-10-16.
//  Copyright (c) 2015 Production sur support. All rights reserved.
//
import Foundation
/**
Classe permettant de créer un gentil petit poisson rouge...
*/
class Poisson {
    internal var    _nomPoisson:String
    var             _nomImage:String            // internal par défaut
    private var     _force:Int
    private let     _seuilDangerDeMort = 10
    // computed properties
    var force: Int {
        get {
            println("Dans le getter de 'force' du poisson \(_nomPoisson) avec une force de \(_force)")
            if _force == 0 { return 3141592 }
            return _force
        }
        set (valeur){
            println("Dans le setter de 'force' du poisson \(_nomPoisson) avec une force de \(valeur)")
            if valeur < 0 { _force = 0 }
        }
    }
    // internal par défaut
    init(nomPoisson:String, force:Int) {
        println("Je suis le constructeur de Poisson.")
        _nomPoisson  = nomPoisson
        _force       = force
        _nomImage    = "n/a"
    } // init()
    private func lireForce()-> Int{
        return _force
    }
    internal func quiSuisJe() ->String {
        return "Poisson de force: \(lireForce())"
    } // quiSuisJe()
} // class Poisson

Note: Une propriété de type ‘computed’ sans ‘setter’ sera alors de type ‘lecture seulement’.
 


Observateurs d’accès à une propriété

 

class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            println("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                println("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps
// Note: Référence: https://developer.apple.com/library/ios/documentation/swift/conceptual/swift_programming_language/Properties.html

 
 


Propriété calculée (voir accesseur)

 

// Étant donné:
// self.nom = "Lafrance"
// self.titre = "Monsieur"
var properNom: String {
  return self.titre + " " + self.nom
}
println("Je suis \(properNom).)
// Donne:
Je suis Monsieur Lafrance.

Note:  ‘properNom’ n’est pas considérée comme étant une fonction mais une propriété calculée (computed propertie).
 


Extensions de classe

 
Les extensions de classe permettent d’ajouter à une classe existante:

Syntaxe:

extension UneClasse: UnProtocole, UnAutreProtocole {
    // Programmation des protocoles
    // Implémentation des nouvelles fonctionnalités
}

Note: Il est possible d’ajouter de nouvelles fonctionnalités à une classe mais pas de surcharger les existantes.
Par exemple,

extension String {
   var majuscule: String { return self.uppercaseString }
}
println("bonjour à tous!".majuscule)  // res.: BONJOUR À TOUS!

 


 
Action à réaliser en laboratoire par l’étudiant:

// Exemple d'utilisation de la classe Aquarium
        let aquarium = Aquarium(individus: [
            PoissonVolant(nomPoisson: "Exocoetidae", force: 99, hauteurDuVol: 66),
            Poisson(nomPoisson: "Bob", force: 99)
            ],
            dimension:100)
        println(aquarium.quiSuisJe())
// Résultat:
Je suis un Aquarium de 100 gallon(s) avec les poissons suivants:
------------------
Je suis une poisson volant de type:Exocoetidae et je saute à 66 pied(s)
------------------
Je suis le poisson Bob et j'ai une force de 99

Indice: Le type des éléments du tableau reçu par le constructeur est Array<Poisson>.  Ce tableau pourra stocker  toutes instances dont l’ancêtre est de type Poisson (incluant le PoissonVolant).  Cela s’explique par le concept de polymorphisme.
 


Constructeur de convenance

Action 1.12 

    // Constructeur de convenance
    /**
    Construire un poisson pas de nom et de force entre deux nombres.
    */
    convenience init(deForceEntre:Int, etMax:Int){
        self.init(nomPoisson:"Pas de nom", force: Int(arc4random_uniform(UInt32((etMax - deForceEntre) + 1))) + deForceEntre)
    } // convenience init()

 
Action 1.13 – Ajoutons le code suivante à la méthode ViewDidLoad:

        let junior = Poisson(deForceEntre: 33, etMax: 44)
        println( junior.quiSuisJe() )

 
Ce qui va afficher dans la console:

viewDidLoad Je suis le constructeur de Poisson.
——————
Je suis le poisson Bob et j’ai une force de 99
Je suis le constructeur de Poisson.
——————
Je suis le poisson Pas de nom et j’ai une force de 3

 
Action 1.14 – Ajoutons les deux méthodes suivantes à la classe Poisson

    /**
      Méthode pour épuiser artificiellement le poisson
    */
    func epuiserLePoisson(force:Int){
        self.force -= force
        if self.force < 10 {
            println("Le poisson \(nomPoisson) est très très faible")
        }
    } // epuiserLePoisson
    /**
    Méthode pour nourrir le poisson
    */
    func nourrirLePoisson(nourriture:Int){
        force += 2 * nourriture
    } // nourrirLePoisson

Action 1.15 – Ajoutons le code suivante à la méthode ViewDidLoad:

        let unPoisson = Poisson(nomPoisson: "Bob", force: 99)
        unPoisson.epuiserLePoisson(95)
        unPoisson.nourrirLePoisson(40)
        println(unPoisson.quiSuisJe())

 

Je suis le constructeur de Poisson.
Le poisson Bob est très très faible
——————
Je suis le poisson Bob et j’ai une force de 84

 
Référence Apple
Projet-Aquarium-etape1


Étape 2 – Protocole sur mesure

 
À l’étape précédente, nous avons ajouté une méthode pour affaiblir le poisson.
Sous le seuil de force == 10, le poisson est placé en situation de danger de mort.
Le problème, c’est que notre application n’a aucune idée de cet état de fait.
Les protocoles, sous Swift, permettent d’indiquer à une classe abonnée qu’un intervention est requise par la classe offrant le protocole.
Par exemple, si la classe Poisson propose un protocole nommé « PoissonEtatDeSante » et qu’une autre classe du projet s’abonne à ce protocole alors cette dernière sera avisée des changements d’états du poisson.
Référence Apple


Définir et implémenter un protocole sous Swift – 7 étapes

Dans la classe qui propose le protocole

Dans la classe qui s’abonne au protocole

 
Action 2.1 – Étape 1 et 2 – Ajoutons le code suivant à la classe Poisson.

// Étape 1 - déclarer le protocole
protocol PoissonDelegate {
    // étape 2 - déclarer les méthodes du protocole
    func poissonAFaim(sender:Poisson)
}
// Note - Avant: class Poisson { ...

 
Note:  Il est possible d’identifier des méthodes d’un protocole comme étant facultatives.  Il faut alors déclarer le protocole de la façon suivante:

@objc protocol UnProtocole {
    optional func uneMethodeFacultative(count: Int) -> Int
    fun uneMethodeObligatoire(count: Int) -> Int
}

 
Action 2.2 – Étape 3 – Ajoutons le code suivant à la classe Poisson.

class Poisson {
    // Propriétés
    // Étape 3 - Définir une propriété - delegate - de type 'NomDuProtocole'
    /// Propriété pour stocker le délégué éventuel de l'instance Poisson
    var delegate:PoissonDelegate?

 
Action 2.3 – Étape 4 – Ajoutons le code suivant à la classe Poisson – seulement la ligne 6.

    func epuiserLePoisson(force:Int){
        self.force -= force
        if self.force < 10 {
             println("Le poisson \(nomPoisson) est très très faible - force < 10")
            // Étape 4 - gérer le protocole
            delegate?.poissonAFaim(self)
        }  // self.force < 10
    } // epuiserLePoisson

 
Action 2.4 – Étape 5 – Ajoutons le code suivant à la classe abonnée

// Étape 5 - s'abonner au protocole
class ViewController: UIViewController, PoissonDelegate{

 
.
Action 2.5 – Étape 5 – Ajoutons le code suivant à la classe abonnée – seulement la ligne 5.

    override func viewDidLoad() {
        super.viewDidLoad()
        unPoisson = Poisson(nomPoisson: "Bob", force: 99)
        // Étape 6 - renseigner le delegate
        unPoisson.delegate = self

 
Action 2.6 – Étape 5 – Ajoutons le code suivant à la classe abonnée

    // Étape 7 - Programmer les méthodes du protocole
    func poissonAFaim(sender: Poisson) {
        println("Le poisson \(sender.nomPoisson) a faim, je vais donc le nourrir!")
        sender.nourrirLePoisson(20)
    } // poissonAFaim

 
Action 2.7 – Exécutons l’application
aquarium.29
 
Action à réaliser en laboratoire par l »étudiant:

 
Dans la classe abonnée:

 


point de repère pour la mise à jour


 
 
Projet-Aquarium-etape2


Étape 3 – Classe Poisson à partir de UIImageView

 
Action 3.1 – Dans la classe Poisson, ajoutons le code suivant
Note: À cette étape, l »image du poisson sera codée en dur.

// 1 - Importer le frameWork UIKit
import UIkit
// 2 - Classe Poisson étend UIImageView
class Poisson:UIImageView {

 
Note: Suite à l’ajout d’une classe à étendre dans la déclaration de la classe Poisson, nous obtenons l’erreur suivante:
aquarium.10
 
Action 3.2 – Confirmons le Fix-it*, ce qui ajoutera le code suivant à la classe:

    /**
      Cette méthode est obligatoire lorsque la super classe est de type 'Fondation'
    */
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

*Il faudra aussi faire cette correction avec la classe PoissonVolant.
 
Note:  Il y a encore une erreur à corriger – l’appel du constructeur de la super classe.
aquarium.11
 
Action 3.3 – Modifions le constructeur de la classe Poisson pour y ajouter l »appel du constructeur de la super classe.

// 3 - Modification du constructeur de la classe Poisson
    // Constructeur
    /**
      Construire un poisson à partir d'un nom et d'une force
    */
    init(nomPoisson:String, force:Int) {
       println("Je suis le constructeur de Poisson.")
       self.nomPoisson  = nomPoisson
       self.force       = force
       self.nomImage    = "n/a"
       // Étape 3.3 - Appel de super.init()
       // Note:  Il faut renseigner les propriétés locales
       // avant l'appel de super.init()
       // Il faut utiliser le constructeur de la super classe
       // Il n'est pas possible d'utiliser un init de convenance de la super classe (à valider sous 6.1).
       // Le contructeur de UIImageView est:  init(image: UIImage!)
       // Par contre, il ne permet pas de préciser une taille pour l'image.
       // Nous allons donc faire appel au contructeur de la super classe de UIImageView C-A-D, UIView(frame: CGRect)
       //
       // Créer une View à la position (20,150) de dimension (100, 50)
       super.init(frame: CGRectMake(20, 150, 100, 50))
       image = UIImage(named: "p13-frame-1.gif")
    } // init()

 
Action 3.3b –  Ajoutons le poisson à la vue de la scène.

        view.addSubview(unPoisson)

 
Action 3.4 –  Testons l’application.
aquarium.12
Note: La classe poisson possède maintenant une interface visuelle de type « UIView » et peut être ajoutée à la vue d’une scène.


Animer une série d’image

 
La classe ‘UIImage’ propose des méthodes et des propriétés pour animer une séquence d’images.
 
Action 3.5 –  Ajoutons le code suivant à la fin du constructeur de la classe Poisson.

        // Créer l'animation à partir d'une suite d'images
        var images:[UIImage] = []
        // Note: Il y a 8 images pour l'animation du poisson P13
        for i in 1...8 {
            // Note: Xcode 6.1 - UIImage(named:) retourne une optionnelle.
            images.append(UIImage(named:"p13-frame-\(i).gif"))
        }
        animationImages = images  // Un tableau d'images pour l'animation
        animationDuration = 1.5   // La durée de l'animation
        startAnimating()          // Démarrer l'animation
//    } // init()

 
Action 3.6 –  Testons l’application
Note: Le poisson devrait maintenant être animé à l’écran.
clownfish
Note: Si le nom du dossier contenant les images de l’animation se termine par ‘.atlas’ Xcode va alors créer automatiquement une ‘sprite sheet’.
Exemple d’une sprite sheet:
2D_Tikiman_SpriteSheet
Action 3.7 –  Ajoutons un nouveau constructeur, à notre classe Poisson, qui recevra le nom de l’image, la position, la dimension et le délégué du protocole PoissonDelegate.
 

    // Constructeur
    /**
    Construire un poisson animé à x,y hauteur et largeur et délégué
    */
    init(nomPoisson:String, nomImage:String, nbImages:Int, posX:CGFloat, posY:CGFloat, largeur:CGFloat, hauteur:CGFloat, leDelegate:PoissonDelegate) {
        println("Je suis le constructeur de \(nomPoisson).")
        self.nomPoisson  = nomPoisson
        self.force       = 20
        self.nomImage    = nomImage
        delegate         = leDelegate
        // Note:  Il faut renseigner les propriétés locales
        // avant l'appel super.init()
        super.init(frame: CGRectMake(posX , posY, largeur, hauteur))
        // Image sans anime - poisson mort
        image = UIImage(named: "mort.png")
        // Créer l'animation à partir d'une suite d'images
        var images:[UIImage] = []
        for i in 1...nbImages {
            images.append(UIImage(named: "\(nomImage)-frame-\(i).gif")!)
        } // for nbImages
        animationImages = images
        animationDuration = 1.5
        startAnimating()
    } // init()

 
Action 3.8 – Et remplaçons le code de viewDidLoad par:

    override func viewDidLoad() {
        println("viewDidLoad")
        super.viewDidLoad()
        // TODO: Ajouter des bulles
        let x = Poisson(nomPoisson: "Poisson 1", nomImage: "p11", nbImages: 8, posX: 150, posY: 250, largeur: 100, hauteur: 50, leDelegate: self)
        x.epuiserLePoisson(99)
        view.addSubview(x)
        // Préparer l'aquarium
        commencerLaPartie()
    } // viewDidLoad

 
Action 3.9 – Testons l’application.
aquarium.13
 
Note: Remarquez que le poisson répond au protocole PoissonDelegate.
aquarium.14
 
Action 3.10 –  Créons plusieurs poissons, dans la méthode ‘ajouterPoissons’ à partir des enregistrements du tableau ‘modelesPoisson’.
Note:  Il faut effacer le poisson que nous avons créé dans ViewDidLoad.

    /**
    Créer des poissons et les ajouter à la scène principale.
    */
    func ajouterLesPoissons(quantite:Int){
        println("ajouterLesPoissons")
        // Poisson 1, pour tester le protocole
        let x = Poisson(nomPoisson: "Poisson 1", nomImage: "p11", nbImages: 8, posX: 50, posY: 180, largeur: 100, hauteur: 50, leDelegate: self)
        x.epuiserLePoisson(99)
        view.addSubview(x)
        // Ou bien directement, sans passer par une variable.
        view.addSubview(Poisson(nomPoisson: "Poisson 2", nomImage: "p11", nbImages: 8, posX: 50, posY: 120, largeur: 75 , hauteur: 37, leDelegate: self))
        view.addSubview(Poisson(nomPoisson: "Poisson 3", nomImage: "p11", nbImages: 8, posX: 130, posY: 120, largeur: 100 , hauteur: 50, leDelegate: self))
        view.addSubview(Poisson(nomPoisson: "Poisson 4", nomImage: modelesPoisson[2]["nom"]!, nbImages: (modelesPoisson[2]["nbImages"]!).toInt()!, posX: 75, posY:400, largeur: 100 , hauteur: 50, leDelegate: self))
        view.addSubview(Poisson(nomPoisson: "Poisson 5", nomImage: modelesPoisson[2]["nom"]!, nbImages: (modelesPoisson[2]["nbImages"]!).toInt()!, posX: 160, posY: 450, largeur: 100 , hauteur: 50, leDelegate: self))
        view.addSubview(Poisson(nomPoisson: "Poisson 6", nomImage: modelesPoisson[1]["nom"]!, nbImages: (modelesPoisson[1]["nbImages"]!).toInt()!, posX: 220, posY: 290, largeur: 100 , hauteur: 60, leDelegate: self))
    } // ajouterLesPoissons

Note:  Il faut aussi ajuster le nombre de poissons de départ à 6:

    let nbPoissonsDepart            = 6

 
Action 3.11 – Testons l’application
aquarium.15
aquarium.16
 
Projet-Aquarium-etape3


Étape 4 – Donner vie au Poisson: NSTimer

À cette étape, nous donnerons vie à notre poisson grace à un temporisateur (NSTimer) de 2 secondes.  À chaque intervalle, il sera possible d’exécuter du code dans le but de modifier certains paramètres de la classe. Par exemple, le niveau d’énergie du poisson, son état (vivant/mort), …
Action 4.1 – Ajoutons le code suivant à la classe Poisson.

    // Propriétés de la classe Poisson
    var unTimer = NSTimer()
    let intervalleTemps:NSTimeInterval = 2
    // Ajouter ces lignes à la fin du constructeur
        unTimer = NSTimer.scheduledTimerWithTimeInterval(intervalleTemps, target: self, selector: Selector("gererLeProtocoleDelegate:"), userInfo: nil, repeats: true)
     } // init
   // Ajouter cette méthode à la classe Poisson
    /**
    Méthode de gestion PoissonDelegate
    */
    func gererLeProtocoleDelegate(timer: NSTimer){
        //println("testerDelegate de \(nom)")
        let impact = Int(arc4random_uniform(5))
        force-=impact
        delegate?.poissonAFaim(self)
    } // testerDelegate

 
Action 4.2 – Ajoutons la valeur de la force dans la trace de la méthode poissonAFaim.

       println("Le poisson \(sender.nomPoisson), force de:\(sender.force) a faim, je vais donc le nourrir!")

 
Action 4.3 – Testons l »application.
aquarium.18
 
Action 4.4 – Ajoutons, dans la classe Poisson, la déclaration de deux méthodes au protocole PoissonDelegate.

func poissonEnDangerDeMort(sender:Poisson)
func poissonEstMort(sender:Poisson)

 
Action 4.5 – Modifions la méthode gererLeProtocoleDelegate.

    func gererLeProtocoleDelegate(timer: NSTimer){
        let impact = Int(arc4random_uniform(5))
        force-=impact
        delegate?.poissonAFaim(self)
        if force < seuilDangerDeMort {
            delegate?.poissonEnDangerDeMort(self)
            stopAnimating()
        } // force < 10
        if force <= 0 {
            unTimer.invalidate()
            //timerDeplacement.invalidate()
            delegate?.poissonEstMort(self)
        } // force <= 0
    } // testerDelegate

 
Note: La classe abonnée n’est plus conforme au protocole PoissonDelegate.
 
Action 4.6 – Ajoutons les deux méthodes suivantes à la classe ViewController.

    func poissonEnDangerDeMort(sender: Poisson) {
        println("\(sender.nomPoisson) a une force de \(sender.force) et il va mourrir de faim!")
    }
    func poissonEstMort(sender: Poisson) {
        println("\(sender.nomPoisson) est mort")
    } // poissonEstMort

 
Action 4.7 – Cessons de nourrir le poisson lorsqu’il a faim.

    func poissonAFaim(sender: Poisson) {
        println("Le poisson \(sender.nomPoisson), force de:\(sender.force) a faim, je vais donc le nourrir!")
        // sender.nourrirLePoisson(20)
    } // poissonAFaim

 
Action 4.8 – Testons l’application
aquarium.19
aquarium.20
Éventuellement, les poissons vont tous mourrir.
Il faudrait les enlever de la scène à leur décès et gérer l’indicateur de poissons restants.
Action 4.9 – Effaçons le poisson mort.

// ViewController.swift
    func poissonEstMort(sender: Poisson) {
        println("\(sender.nomPoisson) est mort")
        //sender.unTimer.invalidate()
        sender.removeFromSuperview()
        nbPoissons--
        actualiserLabelNBPoissons()
    } // poissonEstMort

Note:  L’instance de la classe poisson va persister même suite à un .removeFromSuperview car le NSTimer maintient  une référence à la méthode gererLeProtocoleDelegate de la classe poisson.  Le message ‘poissonEstMort’ va continuer à être envoyé à la classe abonnée aussi longtemps que le ‘Timer’ va être actif.
Pour désactiver le Timer il faut appeler la méthode ‘invalidate()’ de ce dernier.


Le destructeur d’une classe – deinit

Action 4.9b – Ajoutons la méthode suivante à la classe Poisson.

    // Destructeur de la classe Poisson
    // Faire le ménage ici.  Par exemple, fermer des connexions, ...
    // Attention: l'objet ne sera pas détruit s'il y a des Timers qui tournent.
    deinit{
        println("Je suis le fossoyeur de l'objet \(nomPoisson)")
    }  // deinit

Action 4.10 – Testons l’application
[table]
aquarium.22, aquarium.23
[/table]
 
 
Action 4.11 – Affichons le panneau de fin de partie lorsque le nombre de poissons sera == 0.
 

    func poissonEstMort(sender: Poisson) {
        println("\(sender.nomPoisson) est mort")
        sender.removeFromSuperview()
        nbPoissons--
        actualiserLabelNBPoissons()
        if nbPoissons == 0 {
            finPartie.hidden = false
        } // if nbPoissons == 0
    } // poissonEstMort

 
Action 4.12 – Testons l’application
aquarium.24
 
Action 4.13 – Analysons la méthode ‘commencerLaPartie’

    func commencerLaPartie(){
        println("commencerLaPartie")
        // Masquer la View de fin de la partie
        finPartie.hidden = true
        nourritureDisponible = maximumNourritureDisponible
        actualiserProgressViewQuantNourriture()
        nbPoissons = nbPoissonsDepart
        actualiserLabelNBPoissons()
        // Ajouter des poissons
        ajouterLesPoissons(nbPoissons)
    } // commencerLaPartie

 
Projet-Aquarium-etape4


Étape 5 – Interagir avec les poissons

 
Action 5.1 – Modifions la propriété ‘userInteractionEnabled’ de la classe poisson, pour permettre à UIImageView de recevoir les interactions de l’utilisateur.

// Constructeur de la classe Poisson
// init(nomPoisson:String, nomImage:String, posX:CGFloat, posY:CGFloat, taille:CGFloat, leDelegate:PoissonDelegate, nbImages:Int) {
   userInteractionEnabled = true
// } // init

 
Action 5.2 –  Nourrir le poisson suite à une gestuelle

    // Étape 5 - Ajouter la méthode suivante:
    // Méthode exécutée lorsque l'écran est touché.
    // touch.view pointe sur la View qui a reçu l'événement
    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        // println(touches.anyObject())
        super.touchesBegan(touches, withEvent: event)
        let touch : UITouch = touches.first as! UITouch
        /*
        if touch.view.isKindOfClass(Poisson)
        {
        }
        */
        if let poissonCourant = touch.view as? Poisson {
            println("C'est un poisson!")
            if nourritureDisponible >= unePortionDeNourriture {
                poissonCourant.nourrirLePoisson(unePortionDeNourriture)
                nourritureDisponible -= unePortionDeNourriture
                actualiserProgressViewQuantNourriture()
            }
        } // if touch.view as? Poisson
    } // touchesBegan

 
Action 5.3 – Modifions la classe Poisson.

    /**
    Méthode pour nourrir le poisson
    */
    func nourrirLePoisson(nourriture:Int){
        force = nourriture
        if force > seuilDangerDeMort {
            startAnimating()
        }
    } // nourrirLePoisson

 
Action 5.4 – Testons l’application (regardez l’indicateur de nourriture descendre et les poissons revenir à la vie)
Note:  la méthode ajouterNourriture() est déjà fonctionnelle.
 
d2mip
 
Projet-Aquarium-etape5


Étape 6 – À réaliser en laboratoire

 
Note: Voici comment masquer la barre d’état du téléphone

override func prefersStatusBarHidden() -> Bool {
    return true
}

 
Il faut créer une classe Plante qui:

Il faut:

// Effacer les plantes
for unObjetDeLascene in view.subviews {
  if let plante = unObjetDeLascene as? Plante {
    plante.unTimer.invalidate()
    plante.removeFromSuperview()
  }
} // for unObjetDeLascene

 


État 7 – Déplacement du poisson, son et twitter

 
Action 7.1 –  Ajoutons des propriétés à la classe poisson

    // Étape 7
    var timerDeplacement = NSTimer()
    let intervalleTempsDeplacement:NSTimeInterval = 1/15
    var _largeurEcran:CGFloat = 0
    var _hauteurEcran:CGFloat = 0

 
Action 7.2 – Ajoutons une méthode pour obtenir la dimension de l’appareil

    // Etape 7
    func determinerDimensionsEcran(){
        // Déterminer la résolution de l'appareil
        let screenRect:CGRect = UIScreen.mainScreen().bounds
        _largeurEcran = screenRect.width
        _hauteurEcran = screenRect.height
        println(screenRect)
    } // determinerDimensionsEcran

 
Action 7.3 – Ajoutons le code suivant au constructeur de Poisson

        // Étape 7 - Créer un NSTimer pour le déplacement du poisson
        timerDeplacement = NSTimer.scheduledTimerWithTimeInterval(intervalleTempsDeplacement, target: self, selector: Selector("deplacerObjet:"), userInfo: nil, repeats: true)
        determinerDimensionsEcran()

 
Action 7.4 – Calculons la nouvelle position du poisson
 

    func deplacerObjet(timer: NSTimer){
        let vitesse:CGFloat = 3
        let largeurImage = self.frame.width / 2
        // Note: Les poissons vont de droite à gauche, sauf pour p12
        let _direction:CGFloat =  nomImage == "p12" ? -1 : 1
        var position = self.center
        position.x = self.center.x - (vitesse * _direction)
        // Ajuster la position si le poisson sort de l'écran
        if (_direction == 1 && (position.x + largeurImage) < 0){
            // À la fin, ajuster avec la largeur du view
            position.x = _largeurEcran + largeurImage
        }
        if (_direction == -1 && (position.x - largeurImage) > _largeurEcran){
            // Au debut, ajuster avec la largeur du view
            position.x = -largeurImage
        }
        // Renseigner la nouvelle position du poisson
        self.center = position;
    } // deplacerObjet

 
Action 7.5 – Testons l’application
Note:  Voici comment programmer un mouvement un peu plus naturel aux poissons:

position.y = sin(self.center.x / 100) * 30 + postionYDepart

 


Ajout de bandes sonores

 
Action 7.5 – Ajoutons les fichiers suivants au projet: aquarium.sons
Action 7.6 – Ajoutons le code suivant au projet.

// 1 - Importer le kit AudioVideo
import AVFoundation
// 2 - À ajouter dans la zone de définition des propriétés de la classe ViewController
    var playerTrameSonoreAquarium       = AVAudioPlayer()
// 3 - Méthode à ajouter dans la classe ViewController
    func ChargerTramesSonores(){
        // Permettre la lecture en arrière plan.
        // AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, error: nil)
        // AVAudioSession.sharedInstance().setActive(true, error: nil)
        let URLTrameSonoreAquarium = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("eau", ofType: "mp3")!)
            var error:NSError?
            playerTrameSonoreAquarium = AVAudioPlayer(contentsOfURL: URLTrameSonoreAquarium, error: &error)
            playerTrameSonoreAquarium.prepareToPlay()     // Charger le fichier son en mémoire
            playerTrameSonoreAquarium.numberOfLoops = -1  // Boucle à l'infini avec une valeur négative
            playerTrameSonoreAquarium.volume = 0.2
    }  // ChargerTramesSonores
// 4 - À ajouter dans la méthode ViewDidLoad()
        ChargerTramesSonores()
// 5 - À ajouter dans la méthode commercerLaPartie()
        playerTrameSonoreAquarium.play()

Projet-Aquarium-fin-etape-7-sans-parties-labo


8 – Laboratoire

À faire en laboratoire par l’étudiant:

  1. mort.mp3 – à la mort d’un poisson.
  2. tick.mp3 – lorsqu’un poisson est nourri .
  3. buzzer.mp3 – s’il n’y a pas assez d’oxygène à l’achat de nourriture.  L’achat de nourriture coûte 50 oxygènes.
  4. buzzer.mp3 – s’il n’y a pas assez de nourriture pour nourrir le poisson touché.
  5. achat.mp3 – à l’achat de nourriture.

 

aquarium-b4
 

Note:  Placer un poisson avant une plante: il est possible d’insérer une View avant une autre:

view.insertSubview(Poisson(

                nomPoisson: « Poisson \(compteur++)« ,
nomImage: poissonCourant.nom,
nbImages: poissonCourant.nbImagesAnimation,
posX: _positionX * poissonCourant.directionDeplacement,
posY: _positionY,
largeur: _largeur,
hauteur: _hauteur,
vitesseDeplacement:_vitesse,
directionDeplacement:poissonCourant.directionDeplacement ,
leDelegate: self),
belowSubview: planteAuHasard)
mort-droite
 
FIN du document


Document préparé par Alain Boudreault (c) 2014-2015