MorpionView est sur l'Apple Store

J’apprends en résolvant des problèmes. J’imagine des minis-applications répondant à un besoin spécifique, puis je les développe. A chaque fois que j’en une difficulté technique, j’utilise un moteur de recherche pour trouver des infos sur le sujets, ce qui me mène à la doc Apple, à des minis-tutos/articles écrits par des développeurs et/ou des vidéos.

Pas toujours facile, parce que je suis une bille en anglais, mais la lecture de codes sources et Google traduction sont de puissants alliés.

J’ai aussi acheté un ouvrage numérique sur SwiftUI, mais c’est en anglais et c’est une approche trop théorique pour moi.

Je ne maitrise pas le b.a.ba pourquoi des mutating struct plutôt que des classes
comment initialiser

j’ai voulu utiliser une structure plutôt qu’une classe pour le damier
mais c’est la brézina

J’ai un code qui fonctionne pour le damier version structure. Mais pas le temps là, de te le donner. Je suis super-charrette. Je te donne ça dans l’après-midi.

1 « J'aime »

Voici un Damier à base de structure qui fonctionne.
Attention, des noms ont changés, comme par celui du tableau contenant les cases qui se nomme maintenant cases[].

C’est une version provisoire de la remise à niveau de l’application, avec une nouvelle architecture.

import SwiftUI

struct IndexCase {
    var ligne = 0
    var colonne = 0
}

struct DescriptionCase: Identifiable {
    var id = UUID()
    var couleur = Color.gray
    var index = IndexCase()
}

struct Damier {
  var cases : [[DescriptionCase]]
  
  init(nbLignes: Int, nbColonnes: Int) {
      cases = Damier.creer(nbLignes: nbLignes, nbColonnes: nbColonnes)
  }
  
  // Creation du tableau en fonction du nombre de lignes et de colonnes
  private static func creer(nbLignes: Int, nbColonnes: Int) -> [[DescriptionCase]] {
      var tableau = [[DescriptionCase]]()
      for ligne in 0..<nbLignes {
          var ligneTableau = [DescriptionCase]()
          for colonne in 0..<nbColonnes {
              var description = DescriptionCase()
              description.index = IndexCase(ligne: ligne, colonne: colonne)
              ligneTableau.append(description)
          }
          tableau.append(ligneTableau)
      }
      return tableau
  }
  
  // Creation nouvelle grille vierge
  mutating func nouvelleGrille(nbLignes: Int, nbColonnes: Int) {
      cases = Damier.creer(nbLignes: nbLignes, nbColonnes: nbColonnes)
  }
  
  // Action
  mutating func toucheCases(index:IndexCase) {
      let ligne = index.ligne
      let colonne = index.colonne
      let description = cases[ligne][colonne]
    
      if description.couleur == Color.gray {
          cases[ligne][colonne].couleur = Color.green
      }
  }
  
  mutating func ordiCase() {
      while true {
          let ligne = Int.random(in: 0..<3)
          let colonne = Int.random(in: 0..<3)
          
          if cases[ligne][colonne].couleur == Color.gray {
              cases[ligne][colonne].couleur = Color.red
              break
          }
      }
  }
  
}

Je ne connaissais pas non plus mutating, jusqu’à ce qu’XCode ne me hurle dessus ce WE… Pourquoi des structures et non des classes ? Parce que les classes ne gèrent pas le @Binding, tellement pratique en SwiftUI.

A quoi sert mutating ? C’est un filet de sécurité pour le développeur. Les premiers langages de programmation étaient de vrais bordels, où l’on pouvait faire (et faisait généralement) n’importe quoi …

Au fur et à mesure de l’évolution des langages, différents mécanismes de protection contre les erreurs de programmation ont été développés. Swift, le petit dernier de la famille, possède énormément de pare-feux.

C’est pour cela qu’une fonction intégrée dans une structure ne peut modifier le contenu de cette dernière. Si on tente de le faire, l’éditeur considère cela comme une erreur, affichant un panneau d’avertissement.

Mutating est un paramètre indiquant au compilateur « Oui, j’ai bien lu votre avertissement. Oui cette pratique est potentiellement dangereuse. Mais j’en ai besoin quand même. Merci de m’avoir prévenu ».

https://www.hackingwithswift.com/sixty/7/5/mutating-methods

merci

j’ai mis ce que j’ai intégré sur GitHub, j’espère que les binding sont bien utilisés

2 problèmes:

dans grille view
damier.toucheCases(index: liste[ligne][colonne].index)
cannot use mutating member on immutable value self is immutable

dans scenedelegate
let contentView missing arguments pour l’integration des bindings

Je n’aurais peut-être pas du te donner le fichier du Damier comme ça. Comme je l’ai écris, c’est une version provisoire. L’entête est modifié par rapport à la version précédente. C’était juste pour te montrer un exemple de code d’une structure, pas un fichier à insérer comme tel dans le projet en mode Plug&Play.

Le GameView est légèrement différent aussi, ainsi que le ContentView.

Attend demain, que je finalise la nouvelle version, au lieu d’une version provisoire.

C’est pas un problème, çà me fait essayer

Nouvelle version, en 6 fichiers :

Pour commencer j’ai modifié CaseView(). Le contenu des cases n’est plus défini par des couleurs, mais avec un type d’état pouvant prendre les valeurs .vide, .joueur et .ia.

Cela permet de séparer la représentation graphique finale le stockage générique des données dans le modèle de l’application.

On peut facilement modifier CaseView() pour utiliser d’autres couleurs, des images ou des formes géométrique, sans toucher aux autres classes/structs/modéles de données de l’application !

import SwiftUI

// La représentation graphique finale est faite ici,
// en fonction du contenu de la case (.vide, .joueur, .ia)

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

/// Description
/// Affichage d'une case du damier
struct CaseView: View {
    var description: DescriptionCase
    
    var body: some View {
        Rectangle()
          .foregroundColor(couleursCase[description.contenu])
        .frame(width: 80, height: 80)
    }
}

struct CaseView_Previews: PreviewProvider {
    static var previews: some View {
      CaseView(description: DescriptionCase(contenu:.vide, index: IndexCase(ligne: 0, colonne: 0)))
    }
}

Le ContentView :

import SwiftUI

struct ContentView: View {
  @ObservedObject var automate = Automate()
  @State var compteurJoueur    = 0
  @State var compteurIA        = 0
  
    var body: some View {
      VStack {
        Text(automate.etatCourant.rawValue)
            .font(.system(size: 20))
            .bold()
        
        Spacer()
        
        HStack {
            Text("IA: \(compteurIA)")
            Text("Joueur: \(compteurJoueur)")
        }
        
        DamierView(actif:  automate.interfaceActive,
                   damier: automate.damier,
                   action: actionTouch)
          .opacity(automate.opaciteDamier)
        
        Spacer()
        
        HStack {
            Button(action:{
              self.resetgrilleJeu()
              self.automate.activer(etat: .tourJoueur)
                
            }) {
                
                BoutonPerso(text: "Joueur Commence", couleur: .green)
            }
            
            Button(action:{
              self.resetgrilleJeu()
              self.automate.activer(etat: .tourIA)
                
            }) {
                BoutonPerso(text: "IA Commence", couleur: .red)
            }
        }
      }
    }
  
  // Evénement joueur touche une case
  // Fonction exécuté par DamierView dans une closure
  func actionTouch(_ index:IndexCase) {
    // Lecture de la case et vérification qu'elle est .vide
    guard let caseTouch = automate.damier.lireCase(index: index),
          caseTouch.contenu == .vide
    else { return }
    // Modification état de la Case
    automate.damier.changerCase(index: index, contenu: .joueur)
    compteurJoueur += 1
    if compteurJoueur == 5 {
      automate.activer(etat: .finDujeu)
    } else {
      automate.activer(etat: .tourIA)
    }
  }
  
  func resetgrilleJeu() {
      automate.damier.nouvelleGrille(nbLignes: 3, nbColonnes: 3)
      self.compteurIA = 0
      self.compteurJoueur = 0
  }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Plus simple et plus lisible que le précédent de la semaine dernière.

Je suis revenu à un automate dans une classe, parce que j’ai eu quelques problèmes avec les structures. Quelques problèmes d’initialisation et notamment, l’impossibilité d’utiliser un délai dans une structure pour simuler la réfléxion de l’IA.

L’affichage se fait avec le composant DamierView().

        DamierView(actif:  automate.interfaceActive,
                   damier: automate.damier,
                   action: actionTouch)
          .opacity(automate.opaciteDamier)

J’ai ajouté un paramètre d’opacité, pour quasiment faire disparaître le damier au début du jeu, lorsque l’utilisateur doit choisir s’il commence à joueur, ou c’est l’IA.

Lorsque le joueur sélectionne une case, DamierView() exécute la closure actionTouch() en lui envoyant l’index de celle-ci.

  func actionTouch(_ index:IndexCase) {
    // Lecture de la case et vérification qu'elle est .vide
    guard let caseTouch = automate.damier.lireCase(index: index),
          caseTouch.contenu == .vide
    else { return }
    // Modification état de la Case
    automate.damier.changerCase(index: index, contenu: .joueur)
    compteurJoueur += 1
    if compteurJoueur == 5 {
      automate.activer(etat: .finDujeu)
    } else {
      automate.activer(etat: .tourIA)
    }
  }

C’est presque similaire à ce que tu faisais avant, sauf qu’il y a une vérification sur le type de case. Seules les cases .vide sont prises en compte.

Damier :

Il faut utiliser maintenant des méthodes de lecture/écriture pour y accéder. Cela permet de faire des tests de validité des index, mais surtout de mettre à jour une variable comptabilisant le nombre de cases libres.

import SwiftUI

enum TypeCase : String {
  case vide   = "case vide"
  case joueur = "case joueur"
  case ia     = "case ia"
}

struct IndexCase {
    var ligne = 0
    var colonne = 0
}

struct DescriptionCase: Identifiable {
  var id = UUID()
  var contenu = TypeCase.vide
  var index = IndexCase()
}

// --------------------------------------

struct Damier {
  var cases: [[DescriptionCase]]
  
  private var _nbCasesLibres = 0
  private var _nbLignes:Int = 0
  private var _nbColonnes:Int = 0
  // Variables en lecture seule
  var nbCasesLibres : Int { return _nbCasesLibres }
  var nbLignes      : Int { return _nbLignes }
  var nbColonnes    : Int { return _nbColonnes }
  
  init(nbLignes: Int, nbColonnes: Int) {
      _nbLignes   = nbLignes
      _nbColonnes = nbColonnes
      cases = Damier.creer(nbLignes: nbLignes, nbColonnes: nbColonnes)
      _nbCasesLibres = _nbLignes*_nbColonnes
  }
  
  // Creation nouvelle grille vierge
  mutating func nouvelleGrille(nbLignes: Int, nbColonnes: Int) {
      _nbLignes   = nbLignes
      _nbColonnes = nbColonnes
      cases = Damier.creer(nbLignes: nbLignes, nbColonnes: nbColonnes)
      _nbCasesLibres = _nbLignes*_nbColonnes
  }
  
  
  // -----------------------------------------------
  // Outils divers
  
  // Recherche aléatoire d'une case vide
  // avec retour d'un optional nil si ce n'est pas possible
  // (pour éviter une boucle infinie)
  func rechercheCaseVide() -> IndexCase? {
    guard _nbCasesLibres > 0 else { return nil }
    while true {
        let ligne = Int.random(in: 0..<_nbLignes)
        let colonne = Int.random(in: 0..<_nbColonnes)
      
      if cases[ligne][colonne].contenu == .vide {
            return IndexCase(ligne: ligne, colonne: colonne)
        }
    }
  }
  
  // Lecture case
  func lireCase(index:IndexCase) -> DescriptionCase? {
    if verificationIndex(index: index) {
      return cases[index.ligne][index.colonne]
    } else { return nil }
  }
  
  // On ne peut changer que l'état d'une case .vide
  mutating func changerCase(index:IndexCase, contenu:TypeCase) {
    if verificationIndex(index: index) {
      if cases[index.ligne][index.colonne].contenu == .vide {
        cases[index.ligne][index.colonne].contenu = contenu
        _nbCasesLibres -= 1
      }
    }
  }
  
  // Verification si un Index est valable
  // par rapport à la taille du tableau
  func verificationIndex(index:IndexCase) -> Bool {
    if Set(0..<_nbLignes).contains(index.ligne) {
      if Set(0..<_nbColonnes).contains(index.colonne) {
        return true
      }
    }
    return false
  }
  
  // Creation du tableau en fonction du nombre de lignes et de colonnes
  private static func creer(nbLignes: Int, nbColonnes: Int) -> [[DescriptionCase]] {
      var tableau = [[DescriptionCase]]()
      for ligne in 0..<nbLignes {
          var ligneTableau = [DescriptionCase]()
          for colonne in 0..<nbColonnes {
              var description = DescriptionCase()
              description.index = IndexCase(ligne: ligne, colonne: colonne)
              ligneTableau.append(description)
          }
          tableau.append(ligneTableau)
      }
      return tableau
  }
  
}

Une innovation intéressante est la fonction permettant de chercher une case vide, notamment utilisée par l’IA :

  func rechercheCaseVide() -> IndexCase? {
    guard _nbCasesLibres > 0 else { return nil }
    while true {
        let ligne = Int.random(in: 0..<_nbLignes)
        let colonne = Int.random(in: 0..<_nbColonnes)
      
      if cases[ligne][colonne].contenu == .vide {
            return IndexCase(ligne: ligne, colonne: colonne)
        }
    }
  }

Elle ne risque pas de boucler indéfiniment, si le tableau est plein, grâce à l’instruction guard vérifiant s’il reste au moins une case de libre, avant de lancer la recherche.

C’est dans ce genre de code que l’on voit tout l’intérêt des variables optionnelles pour prévenir le code appelant que l’opération s’est mal passé.

EDIT : Et zut, je ne peux plus poster dans ce topic. Le système anti-spam me bloque. Il faut que quelqu’un poste au moins une fois, pour que je puisse recommencer à poster.

Il me semblait logique de faire un post par fichier, pour bien expliquer les choses :nerd_face::cry:

REPONDEZ-MOI ! SAUVEZ MES POSTS !!

1 « J'aime »

Bon je ne suis pas comme ça, je te viens en aide :wink:

J’ai quand même hésité, à mettre seulement un j’aime :stuck_out_tongue_winking_eye:

Merci @anthonyfassler1 (du bout des lèvres, sans aucune sincérité)

Reprise des émissions, donc :

L’automate est une classe, pour les raisons expliqués plus haut. Je ne comprend toujours pas pourquoi il est impossible d’utiliser un délai dans une Struct, alors que ça marche dans une class !

Enfin bon, ce n’est pas la peine de s’attarder sur ce genre de détails. D’ici 15 jours, Apple doit nous donner la version suivante de SwiftUI, qui devrais remettre pas mal de choses à plat.

import SwiftUI

enum EtatAutomate: String {
    case indetermine    = "Indeterminé"
    case parametrage    = "Paramétrage"
    case tourJoueur     = "Tour Joueur"
    case tourIA         = "Tour IA"
    case joueurGagnant  = "Joueur Gagnant"
    case IAGagnant      = "Joueur Perdant"
    case pasDeGagnant   = "Pas de Gagnant"
    case finDujeu       = "Fin du Jeu"
    case reset          = "Reset"
}

// struct Automate {
class Automate : ObservableObject {
  @Published var damier = Damier(nbLignes: 3, nbColonnes: 3)
  @Published var interfaceActive = false
  @Published var opaciteDamier = 1.0
  private var ia = IA()
  
  var etatCourant = EtatAutomate.indetermine
  
  init() {
    activer(etat: .parametrage)
  }
}

extension Automate {
  func activer(etat: EtatAutomate) {
      switch etat {
          case .indetermine:
              break
            case .parametrage:
              opaciteDamier = 0.1
              interfaceActive = false
              break
          case .tourJoueur:
            opaciteDamier = 1.0
            interfaceActive = true
              break
          case .tourIA:
            opaciteDamier = 1.0
            interfaceActive = false
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
              self.ia.jouerTour(damier: &self.damier)
              self.activer(etat: .tourJoueur)
        }
          case .joueurGagnant:
            opaciteDamier = 1.0
            interfaceActive = false
            break
          case .IAGagnant:
            opaciteDamier = 1.0
            interfaceActive = false
            break
          case .reset:
  //            interfaceActive = false
  //            compteurIA = 0
  //            compteurJoueur = 0
              break
          case .pasDeGagnant:
            opaciteDamier = 1.0
            interfaceActive = false
            break
          case .finDujeu:
              interfaceActive = false
              break
    }
          etatCourant = etat
    }
}

L’IA est une struct. Elle n’utilise pas de Binding, mais l’opérateur inout qui fait la même chose.

import SwiftUI

struct IA {

  func jouerTour(damier : inout Damier) {
     if let index = damier.rechercheCaseVide() {
        damier.changerCase(index: index, contenu: .ia)
     }
}

}

Normalement les variables envoyées comme paramètres d’une fonction sont juste des copies des valeurs initiales, ne pouvant être modifiés.

Si une variable (un tableau ici) est déclarée en inout, la fonction travaille sur une copie, comme d’habitude. Mais quand le système sort de la fonction, les données originales sont effacés et remplacés par la copie modifiée !

Le fonctionnement est le suivant :

  • création d’une copie locale du Damier
  • recherche d’une case vide (c’est l’algorithme utilisé dans la démo de GitHub)
  • marquage de la Case avec l’attribut .ia dans la copie
  • Effacement du Damier original et remplacement par la copie de travail. Le damier étant stocké dans une variable @Published de la classe, SwiftUI avertis le reste du système, qu’il y a eu une modification du modéle, et qu’il faut redessiner le contenu de l’écran.

Voila, je crois que je n’ai rien oublié. A noter qu’il n’y a aucun @Binding, nul part dans le code. J’ai pas mal galèré avec les différents opérateurs de Binding ce WE, surtout les initialisations et franchement ça me gave. C’est très bien pour les choses simples, mais c’est horrible dans les cas particuliers. J’espère que le prochain SwiftUI vas simplifier tout ça.

Des questions ?

1 « J'aime »

@Draken quel beau message d’amour ! :heart_eyes::joy:

Moi Dragon, toi humain … Toi devoir vénérer :dragon: Toi devoir mettre 10 :heart: !!

1 « J'aime »

Fonctionne pas, site accepte que 1 !! :man_shrugging:

:raised_hands::raised_hands::raised_hands::raised_hands:

Tu ne peux pas utiliser une struct, car @Published est contraint par des attributs de class, donc fonctionne uniquement sur des objets et non des valeurs. (Struct = valeur, Class = Objet).

Attention, ce n’est pas exactement la même chose, @Binding observe une variable et s’il y a modification, elle la renvoie aux deux endroits. Par exemple, dans 2 écrans différents.

inout sert à rentre un paramètre d’une fonction variable, car tous les paramètres des fonctions sont des constantes.

Oui, mais ce cas particulier, c’est presque la même chose, puisque la variable (le tableau en fait) modifiée par inout est une @Publisher, pouvant propager les modifications ailleurs.

EDIT : A noter que la vidéo de Maxime sur @Publisher est obsolète, compte tenu de toutes les modifications apportées par Apple depuis la bêta de l’an dernier. Plus besoin de surveiller les modifications des variables, et d’envoyer une notification au système. @Publisher gère tout cela automatiquement, en une seule ligne.

@Draken je n’ai pas suivi le code jusque là, mais effectivement tu peux arriver au même résultat.

J’ai dit ça pour préciser ce que ça faisait réellement, car ce n’est pas réellement un Binding à lui tout seul.

Bonjour

merci pour tout ce travail
Je suis actuellement parti pour quelques jours
Et ai seulement mon iPad avec moi
Ce serai fastidieux de le faire avec playgrounds
Je regarde tout ça à mon retour

Bonjour de retour

j’ai l’impression qu’il manque un bout de damierView

pas trop compris le inout, le fileprivate, le static dans la func créer
je ne connaissait pas la fonction Set
quand mets tu un _ pour le nom des variables

pas mal de choses à assimiler
j’espère que dans la prochaine version on pourra utiliser que des structures et des Binding
c’est plus facile à comprendre