MorpionView est sur l'Apple Store

Oups … voici le fichier DamierView :

import SwiftUI

/// Description
/// actif : flag binaire indiquant si le DamierView doit réagir ou non à la sélection d'une case
/// damier: Liste des cases à dessiner en fonction des lignes et des colonnes
/// action: closure a exécuter quand le joueur sélectionne une case
struct DamierView: View {
    var actif: Bool
    var damier: Damier
    var action: (_ index:IndexCase) -> Void
    
    var body: some View {
        VStack {
          ForEach(0..<damier.cases.count, id: \.self) { ligne in
                HStack {
                  ForEach(0..<self.damier.cases[ligne].count, id:\.self) { colonne in
                    CaseView(description: self.damier.cases[ligne][colonne])
                            .onTapGesture {
                                if self.actif {
                                  self.action(IndexCase(ligne: ligne, colonne: colonne))
                                }
                        }
                    }
                }
            }
        }
    }
}

Fileprivate :
Une structure de données définie de manière globale est accessible partout dans le code. Pas terrible pour la sécurité et l’encapuslation des informations. C’est pourtant bien pratique pour simplifier l’écriture du code.

Fileprivate est une réponse à ce problème. Elle permet de définir des objets globaux, accessibles uniquement aux lignes de codes présentes dans le même fichier.

/// Dictionnaire contenant les association TypeCase-Couleur
fileprivate let couleursCase:[TypeCase:Color] = [
  .vide:Color.gray,
  .joueur:Color.blue,
  .ia:Color.red
]

Le dictionnaire associant un type de Case avec une couleur est une structure de données globales, défini dans le fichier CaseView.swift est global, mais son attribut fileprivate fait qu’il n’existe pas en dehors du fichier. C’est le meilleur des mondes : la lisibilité d’un objet global et la sécurité d’un objet privé à utilisation local.


Static :

C’est un mot clé permettant de définir des méthodes de classe, c’est-à-dire des méthodes n’ayant pas besoin d’une instance de la classe pour fonctionner.

La manière habituelle d’utiliser une classe c’est :

a/ je crée un objet uneClasse de la classe MaClasse
b/ j’appelle une fonction pour faire quelque chose avec l’objet

var objet1 = MaClasse()
objet1.faireQuelqueChose()

On dis que faireQuelqueChose est une méthode d’instance, car elle a besoin qu’un objet de type MaClasse existe (l’instance) pour fonctionner.

Une méthode de classe n’a pas besoin d’une instance pour fonctionner. Pas besoin de créer un objet pour l’utiliser.

Si faireQuelqueChose est défini avec l’attribut Static, on peut l’utiliser directement, sans créer un objet, comme ceci :

MaClasse.faireQuelqueChose()

Swift est casse-pied avec les initialisations dans une classe ou une structure. On ne peut utiliser une variable ou une propriété utilisant Self qu’après que la classe soit entièrement définie.

Exemple :

je crée un mini générateur de couleurs aléatoire.

class GenerateurCouleurAleatoire {

private let couleurs = [Color.blue, Color.yellow, Color.green, Color.red]

  func creerCouleur() -> Color {
    return couleurs.randomElement()!
  }
}

Cela fonctionne très bien avec ce code :

struct ContentView: View {

var generateurCouleur = GenerateurCouleurAleatoire()
@State var couleur = Color.blue

var body: some View {
  VStack {
    Text("Hello, World!")
        .font(.largeTitle)
        .foregroundColor(couleur)
      .padding()
    Button(action: {
      self.couleur = self.generateurCouleur.creerCouleur() }) {
      Text("Nouvelle couleur")
    }
  }
}

}

A chaque pression sur le bouton, l’application génère une couleur aléatoire et modifie l’allure du texte à l’écran. Que du classique !

Au lancement de l’application, la couleur du texte est le bleu. Que se passe-t-il si si je cherche à remplacer cette couleur fixe par une couleur aléatoire ?

A priori, c’est facile à faire :

var generateurCouleur = GenerateurCouleurAleatoire()
@State var couleur = generateurCouleur.creerCouleur()

Sauf que Xcode n’est pas d’accord, hurlant à la mort !

La variable generateurCouleur contenant l’objet créé à la ligne précédente ne peut pas être utilisée comme ça, car la propriété Self définissant l’objet lui-même n’existe qu’à la fin de l’initialisation.

Cela fonctionne dans l’exemple précédent parce que le Bouton utilise la variable generateurCouleur APRES l’initialisation du ContentView, pas PENDANT cette initialisation.

La solution est de modifier le générateur de couleurs aléatoire, avec l’attribut Static pour ajouter une méthode de classe au générateur de couleur.

class GenerateurCouleurAleatoire {

  static func creerCouleur() -> Color {
    let couleurs = [Color.blue, Color.yellow, Color.green, Color.red]
    return couleurs.randomElement()!
  }
}

Petite subtilité : les méthodes de classe (static) ne peuvent accéder aux données de la classe, puisqu’elles n’ont pas étés initialisées. C’est pourquoi j’ai du transférer le tableau couleurs[] dans le corps de la fonction. Sinon, cela ne fonctionne pas.

Le programme modifié fonctionne correctement, sans avoir besoin de créer un objet générateur de couleurs.

struct ContentView: View {

@State var couleur = GenerateurCouleurAleatoire.creerCouleur()

var body: some View {
  VStack {
    Text("Hello, World!")
        .font(.largeTitle)
        .foregroundColor(couleur)
      .padding()
    Button(action: {
      self.couleur = GenerateurCouleurAleatoire.creerCouleur() }) {
      Text("Nouvelle couleur")
    }
  }
}

}

C’est la même logique pour la création d’un damier. La méthode de classe (static) permet de générer directement un tableau, sans passer par un objet intermédiaire et sans faire hurler Xcode à l’initialisation à cause de ce maudis Self pas encore finalisé.

Note technique : C’est là qu’on voit l’intérêt d’utiliser une lettre majuscule pour les noms de classe et une minuscule pour les objets.

@State var couleur = GenerateurCouleurAleatoire.creerCouleur()

D’un simple regard on comprend tout de suite que creerCouleur() est une méthode de classe, ne nécessitant pas d’initialisation.

La suite au prochain numéro …

Bonjour

J’y vais graduellement
J’ai entré ton source et mis une version sur GitHub, cela fonctionne

Il manque juste l’incrémentation du compteur IA que je ne sais pas trop ou mettre, avant elle était dans l’automate

que mettre dans DamierView_preview pour afficher un première preview

merci pour l’explication de static

Je met un _ devant le nom des variables quand ce sont des variables locales à une classe. C’est un reliquat du C++, où on ajoutait m_ (m pour member) devant ces variables.

Certains développeurs mettent un _ systématique, d’autres jamais. Pour ma part j’ai tendance a employer cette notation quand cette variable locale est utilisée dans plusieurs fonctions internes à la classe.

Tu peux définir compteurJoueur et compteurIA comme des variables @Published de l’automate. Cela permet d’y accéder de n’importe où, y compris dans le code gérant la transition vers l’état .tourIA. Je viens de tester, ça marche …

class Automate : ObservableObject {
  @Published var damier = Damier(nbLignes: 3, nbColonnes: 3)
  @Published var interfaceActive = false
  @Published var opaciteDamier = 1.0
  @Published var compteurJoueur = 0
  @Published var compteurIA = 0
   .....

Dans le code de changement d’état :

      case .tourIA:
        opaciteDamier = 1.0
        interfaceActive = false
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
          self.compteurIA += 1
          self.ia.jouerTour(damier: &self.damier)
          self.activer(etat: .tourJoueur)
    }

Dans le contentView :

   ... 
   HStack {
      Text("IA: \(automate.compteurIA)")
      Text("Joueur: \(automate.compteurJoueur)")
    }
    ...
struct DamierView_Previews: PreviewProvider {
  static var previews: some View {
    DamierView(actif: true,
               damier: Damier.init(nbLignes: 3, nbColonnes: 3),
               action: { _ in })
  }
}

Super :ok_hand: merci pour tout. Le programme est maintenant complètement fonctionnel, je l’ai remis sur GitHub pour ceux que cela intéresse.

j’ai mis // Complètement réécrit avec les conseils de Draken sur purplegiraffe.fr dans l’entête
est-ce que cela te convient.

il ne reste plus que l’auto layout en fonction de la dimension du Damier et de l’affichage en mode paysage.

Je vais essayer:

  • d’ajouter qui a gagné
  • de réintégrer l’intelligence de l’IA

Tu devrais faire des formations.

En fait c’est ce qui manque le plus: expliquer qu’est-ce qui ne va pas dans un programme déjà fait
Cela demande beaucoup d’attention et de temps pour voir ce que la personne à bien voulu faire !

j’ai ajouté qui a gagné: qui fonctionne, n’affiche pas les cases en vert
par contre j’ai la fonction quiagagné() 2 fois un fois dans contentView une fois dans automate

var winComb: [[Int]] = [[0,1,2],[3,4,5],[6,7,8], // Horizontal
[0,3,6],[1,4,7],[2,5,8], // Vertical
[0,4,8],[2,4,6]] // Diagonale

/// <#Description#>
/// Teste qui a gagné et affiche les cases en vert
/// - Parameters:
/// - nbLineRaw: nombre de ligne-colonne
/// - winComb: tableau des combinaisons gagnantes
func quiAGagne(nbLineRaw: Int, winComb: [[Int]]) → Gagnant {
var gagnant = Gagnant.personne

    for combinaison in winComb {
        var nbJoueur = 0
        var nbIA = 0

        for i in 0..<nbLineRaw {
            let index = twoDim(nombre: combinaison[i], nbLineRaw: nbLineRaw)
            let descriptionCase = automate.damier.lireCase(index: index)

            if descriptionCase?.contenu == TypeCase.joueur { nbJoueur += 1 }
            if descriptionCase?.contenu == TypeCase.ia { nbIA += 1 }
        }

        if nbJoueur == nbLineRaw {
            gagnant = .joueur
            // On affiche les cases en vert
            for i in 0..<nbLineRaw {
                // Mise des case en vert

// let index = twoDim(nombre: combinaison[i], nbLineRaw: nbLineRaw)
// DescriptionCase.contenu == TypeCase.gagnant
}
} else if nbIA == nbLineRaw {
gagnant = .IA
// Mise des case en vert
}
}
return gagnant
}

case .tourIA:
opaciteDamier = 1.0
interfaceActive = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.compteurIA += 1
self.ia.jouerTour(damier: &self.damier)
let quiGagne = self.quiAGagne(nbLineRaw: 3, winComb: winComb)
if quiGagne == .IA {
self.activer(etat: .IAGagnant)
} else {
self.activer(etat: .tourJoueur)
}
}

// MARK: - Converti une dimension en 2 dimentions
/// <#Description#>
/// - Parameters:
/// - nombre: nombre correspondant à la position
/// - nbLineRaw: nombre de colonnes
/// - Returns: index ligne colonne
func twoDim(nombre: Int, nbLineRaw: Int) → IndexCase {
var ind1 = 0
var ind2 = 0
if nombre < nbLineRaw {
ind1 = 0
ind2 = nombre
} else if nombre < nbLineRaw * 2 {
ind1 = 1
ind2 = nombre - nbLineRaw
} else if nombre < nbLineRaw * 3 {
ind1 = 2
ind2 = nombre - nbLineRaw * 2
} else if nombre < nbLineRaw * 4 {
ind1 = 3
ind2 = nombre - nbLineRaw * 3
} else if nombre < nbLineRaw * 5 {
ind1 = 4
ind2 = nombre - nbLineRaw * 4
}else if nombre < nbLineRaw * 6 {
ind1 = 5
ind2 = nombre - nbLineRaw * 5
}
return IndexCase(ligne: ind1, colonne: ind2)
}

Comme tu veux, je ne fais pas ça pour la gloire, mais pour améliorer ma compréhension du langage.

C’est pour ça que je préfère montrer comment moi j’aurais traité un problème, plutôt que de plonger dans les entrailles d’un code écris par une autre personnage. Repartir du cahier de charges est nettement plus simple.

Monsieur Bug approuve cette duplication de code !

Ca y est j’ai trouvé comment faire pour enlever ce problème
et afficher les cases gagnantes en vert

Reste l’autoLayout en fonction du nombre de cases du damier et de l’orientation

1/ Supprime la frame dans la CaseView, pour que le rectangle fixe lui-même sa taille en fonction de son environnement.

2/ Utilise cette nouvelle version de DamierView :

struct DamierViewBis: View {
    var actif: Bool
    var damier: Damier
    var action: (_ index:IndexCase) -> Void
    
    var body: some View {
      VStack(spacing:5) {
          ForEach(0..<damier.cases.count, id: \.self) { ligne in
            HStack(spacing:5) {
                  ForEach(0..<self.damier.cases[ligne].count, id:\.self) { colonne in
                    CaseView(description: self.damier.cases[ligne][colonne])
                            .onTapGesture {
                                if self.actif {
                                  self.action(IndexCase(ligne: ligne, colonne: colonne))
                                }
                        }
                    }
                }
            }
      } .aspectRatio(1.0, contentMode: .fit)
        .padding(.horizontal, 5)
    }
}

ça fonctionne en mode portrait. Je n’ai pas testé en paysage, mon application de test étant verrouillé en portrait (iPhone et iPad).

La propriété .aspectRatio() ajuste la taille (hauteur et largeur) du VStack contenant les cases en fonction de la largeur de l’écran, avec un petit espace de 5 points entre chaque objet.

La même technique devrais fonctionner avec des images.

Bonjour

Effectivement çà fonctionne, même avec des images j’ai essayé sur simulateur et sur mon iPhone X
et un simulateur sur iPad
je pensais qu’il fallait passer par GeometryReader mais ce n’est même pas la peine
c’est .aspectRatio qui fait tout le travail
tres simple à mettre en oeuvre
super! :ok_hand:

C’est ce que je pensais aussi, mais comme c’est une approche « old tech » je me suis dit qu’il fallait l’utiliser le moins possible. J’ai alors commencé à placer des rectangles sur l’écran pour visualiser leurs relations et tâcher d’en faire le maximum sans code. Et finalement … bingo !