Tableau d'objets dans un objet

Bonjour,

J’essaie de créer une application pour gérer des tournois. J’ai donc créé plusieurs struct:
-Tournament
-Team
-Player
qui me permettent de créer des objets.
J’envisage ensuite de créer un « GameManager » pour gérer les matchs.

Ainsi dans ma struct « Tournament », j’y insère un tableau de [Team], dans lequel j’y insère également un tableau de [Player]. Ainsi mon objet Tournament sera composé de Team elles-mêmes composées de Player.
Ainsi, je pense que chaque tournoi créé aura les équipes et les joueurs qui lui sont associés.
Mais tout cela devenant un peu compliqué à gérer, ma question est la suivante:
Ma logique de conception est-elle correcte ou y-a-t-il une manière plus simple d’envisager les choses ?

En vous remerciant de votre aide et de vos conseils toujours précieux,
Cordialement,

Benoit

1 « J'aime »

Ça me parait bien compliqué, ton systéme.

Moi j’aurais fait :

  • Un tableau pour décrire les joueurs
  • Un tableau pour décrire les équipes (une équipe contenant des références vers les joueurs et non les joueurs eux-mêmes)
  • Un tableau pour décrire le tournoi (contenant des références vers les équipes)

Et j’aurais plutôt utilisé des dictionnaires (liste d’informations liés à des clés d’identification) que des tableaux classiques.

T’imagine la complexité avec ton imbrication d’objets, pour modifier un joueur, s’il se casse la jambe avant le concours, pour être remplacé par une autre personne ?

1 « J'aime »

Merci Draken pour ta réponse, mais j’avoue que je suis un peu perdu.
J’ai regardé de nouveau les cours sur les tableaux et notamment les tableaux clés:valeurs, mais je ne vois pas trop comment décrire les joueurs puisque pour une clé, je ne peux mettre qu’un type de valeur.
Mes structures sont les suivantes:
‘‘‘
struct Player : Identifiable {
let id = UUID()
let name:String
let shirtNumber:Int
let licenceNumber:String

}

struct Team : Identifiable {
let id = UUID()
let teamTown:String
let teamName:String
let teamNumber:String
let shirtColor:String
var score:Int = 0
var point:Int = 0

let playerList:[Player]
}
‘‘‘
Comment créer les dictionnaires avec toutes ces variables et comment faire référence depuis la structure Team vers les joueurs ?

Si quelqu’un peut m’aiguiller ?
En vous remerciant.

Benoit

Bonjour,

Spontanément, je serais parti comme Behache. Ça me semble plus propre de gérer des objets que des tableaux.
Pour la complexité, je pense qu’elle plus liée à l’interface utilisateur pour rendre simple tous les cas tordus qui peuvent se produire.
Parce que ajouter un joueur qui en remplace un autre, ça se déclare sans trop de difficultés dans les deux cas; avec les objets de Behache, le code sera sans doute plus lisible qu’avec des dictionnaires.
Mais bon, je suis loin d’être un expert…

Bonne soirée,
Nicolas

Pour mieux t’aider, il faut peut-être que tu nous dises qu’est-ce qui devient « compliqué à gérer »; ce sera plus facile de voir si la complexité est lié à ton modèle ou à autre chose ?

Je suis trop fatigué pour te répondre, là. Mais ce n’est pas compliqué. Je te donne un exemple de code, demain après-midi.

Merci pour votre aide, vous êtes sympas, c’est cool de se sentir accompagné quand on débute.

En fait, avec mon système d’imbrication Tournament[Team[Player]], je ne vois pas comment récupérer le contenu d’une variable (ou d’une constante) qui se trouve dans Player par exemple.

Bonjour

var teams: [Team] = tournament.teams

Une boucle forEach pour récupérer pour chaque team de teams un tableau de Player ( en utilisant team.players pour chaque team).

A définir sans doute dans une variable players qui est de type « computed » et qui donnera la liste des joueurs à chaque fois que tu en as besoin

A suivre en fonction de ton besoin
Nicolas

1 « J'aime »

Ah, merci merci pour cet éclairage.
Je suis relancé, je vais essayer tout ça.

Mille mercis

Benoît

Hello,

Pour poursuivre dans la logique de @ristretto je dirais que le mieux ce serait plutôt une fonction au lieu d’une variable.
Disons que tu est dans un tournois avec ta liste de team et que tu souhaites afficher la liste des joueurs au tap sur une team :
func players(for team: Team) -> [Player] { return team.playerList }

Aussi tu pourrais enlever les préfixes ‹ team › de tes variables.
let teamTown:String
let teamName:String
let teamNumber:String
Tu appelleras tes variables avec team.town, team.name donc pas besoin de repetition tu sauras identifier que ca vient de Team d’une manière ou d’une autre.

Merci Sota,
Cette proposition colle tout à fait à ce que je veux faire. Je vais tester.

Merci beaucoup

1 « J'aime »

Profitant d’un peu de calme ce WE, j’ai bossé sur ton problème, tapant un peu de code.

Première remarque : Player n’a pas besoin d’avoir un id de type UUID(). Les structures de données utilisées par SwiftUI dans l’affichage ont besoin d’un identifiant unique, mais il existe déjà : c’est le numéro de licence, normalement unique dans une structure sportive.

Pour mes tests, j’ai créé cette structure :

struct Joueur {
  let nom:String
  let licence:String
}

Affichage :

struct JoueurView:View {
  let joueur:Joueur
  var body: some View {
    HStack {
      Text(joueur.nom)
      Spacer()
      Text(joueur.licence)
    }
  }
}

Au début, je voulais utiliser un dictionnaire pour stocker les joueurs, mais j’y ai renoncé pour simplifier le code. On ne peut pas utiliser directement un dictionnaire dans un Foreach ou un List, ll faut passer par une petite transformation dont la syntaxe est un peu rébarbative pour un novice.

L’utilisation d’un tableau engendre une petite perte de performances, dans l’affichage des Equipes. Nous verrons plus bas pourquoi.


Plutôt que de stocker les joueurs et les équipes dans des variables @State de la vue principale, je préfère les définir dans une classe observable, regroupant toutes les données importantes de l’application. C’est plus propre.

Techniquement, une variable @Publisher d’une classe @Observable se comporte exactement comme une @State, sauf qu’elle n’a pas besoin d’être créé dans le même fichier.

class ModeleDatas : ObservableObject {
  @Published var fichesJoueurs =  [Joueur]()
  @Published var listeEquipes = [Equipe]()
}

La fonction JoueursListeView() permet d’afficher sur l’écran une liste de joueurs stockée dans un tableau [Joueur].

struct JoueursListeView:View {
  let liste:[Joueur]
  
  var body: some View {

    List(liste, id: \.licence) {
      joueur in
        JoueurView(joueur: joueur)
    }
  }
}

Le paramètre id: .licence indique à List que l’identifiant unique de chaque structure est stocké dans la propriété .licence. Cela permet d’éviter d’utiliser une structure Identifiable avec un id contenant un UUID.

List(liste, id: \.licence) {

Mine de rien, les UUID sont gourmands en mémoire, environ une centaine d’octets chacun. Ce sont de longues chaînes de caractères. Exemple d’un UUID (tiré de la doc Apple) :

E621E1F8-C36C-495A-93FC-0C247A3E6E5F

Autant éviter de les utiliser, si les données possèdent déjà une propriété unique.


J’ai codé les équipes comme ça :

struct Equipe:Identifiable {
  let id = UUID()
  let nom:String
  let couleur:String
  var score = 0
  var licenceJoueurs = [String]()
}

J’ai utilisé un UUID pour identifier les équipes. En considérant que les noms sont probablement uniques, j’aurais pu les utiliser comme identifiant, mais je ne voulais pas me casser la tête à ce stade.

Les joueurs sont stockés dans un tableau contenant uniquement leurs numéros de licence.

Pour ajouter un joueur à une équipe, il suffit de faire :

equipe.licenceJoueurs.append(numeroLicence)

Et c’est tout …


C’est plus compliqué pour lire l’information, car il faut extraire les données du joueur à partir de son numéro de licence.

Voici comment j’ai fait pour afficher une équipe sur l’écran :

struct EquipeView:View {
  let equipe:Equipe
  @Binding var listeGlobaleJoueurs:[Joueur]
  
  var body: some View {
    VStack {
      Text("Equipe : " + equipe.nom)
      Text("Couleur : " + equipe.couleur)
      Text("Score : " + String(equipe.score))
      ForEach(equipe.licenceJoueurs, id:\.self) {
        licence in
        // Recherche Joueur avec cette licence
        if let joueur = listeGlobaleJoueurs.first(where: { $0.licence == licence}) {
          JoueurView(joueur: joueur)
        }
      }
    }
  }
}

EquipeView a besoin de deux paramètres : l’équipe et le tableau contenant les joueurs.

Si j’avais utilisé un dictionnaire, ce serait très simple de récupérer un joueur à partir de son numéro de licence.

joueur = dico[numeroLicence]

Mais ce n’est pas possible, puisque c’est un tableau. On peut faire la même chose en demandant à l’application de fouiller le tableau, à la recherche d’un joueur ayant le bon numéro de licence.

   if let joueur = listeGlobaleJoueurs.first(where: { $0.licence == licence}) {
       JoueurView(joueur: joueur)
   }

Les tableaux de Swift ont une fonction first() permettant de rechercher la première occurence d’un objet, correspondant à un critère de recherche.

La recherche d’un objet dans un dictionnaire est trés rapide, parce que les clés sont triés et classés dans un arbre binaire.

C’est plus lent avec une fonction de recherche dans un tableau, l’application devant lire chaque case, jusqu’à obtenir le bon résultat. Ce n’est pas significatif avec un petit nombre d’éléments. Si par contre, le tableau contient des milliers d’objets, il est impératif d’utiliser un dictionnaire pour accélérer la recherche, sous peine de ralentissement de l’application.


Des questions ? Il y a encore d’autres choses à dire, mais pour aller plus loin, je dois m’assurer que tu comprennes bien ces bases.

3 « J'aime »

Bonjour Draken,

Merci pour ton retour qui tient plus du cours particulier que de la simple réponse. J’apprécie vraiment.
Il me semble que je comprends tout ce que tu expliques. Cela me parait clair.
J’ai cependant une petite question. Si j’ai plusieurs équipes, comment le programme arrive-t-il a retrouver la liste de joueurs associée à l’équipe. Est-ce que cela est géré au travers de la « List » qui a un identifiant unique pour chaque équipe ?

Bon… cette fois-ci il faut que je commence à nettoyer tout mon code pour repartir sur les bases que tu viens de m’expliquer.
Encore merci et bonne continuation.

Bonsoir,

Une possibilité:

struct Equipe:Identifiable {
  let id = UUID()
  let nom:String
  let couleur:String
  var score = 0
  var joueurs = [Joueur]()
}

ce qui te permet d’ajouter un joueur

team1.append(joueurTartempion)

et de connaître tout naturellement la liste des joueurs

Cordialement
Nicolas

Chaque équipe possède la liste des numéros de licence de ses joueurs, stockés dans le tableau licenceJoueurs[].

Ce sera plus parlant avec un exemple complet. Je vais te faire une mini-application manipulant plusieurs équipes.

C’est génial, merci beaucoup.
Je suis certain que ça répondra à l’ensemble de mes questions.
Merci.

Cela peut prendre un certain temps, selon mes disponibilités. Je vais le faire par étapes.

On recommence par le début :

La struct FicheJoueur permet de stocker les informations définissant un joueur. Je préfère ce nom à celle de Joueur, qui est un objet physique dans le monde réel. Intuitivement on comprend qu’une fiche contient des données sur un autre objet. Le bon choix d’un nom pour un objet, un type d’objet ou une variable facilite la compréhension du code.

struct FicheJoueur {
  let nom:String
  let licence:String
}

Affichage d’une fiche :

struct FicheJoueurView:View {
  let fiche:FicheJoueur
  var body: some View {
    HStack {
      Text(fiche.nom)
      Spacer()
      Text(fiche.licence)
    }
  }
}

Affichage d’une liste de fiches, dans un composant List :

struct FicheJoueursListeView:View {
  let liste:[FicheJoueur]
  
  var body: some View {

    List(liste, id: \.licence) {
      ficheJoueur in
        FicheJoueurView(fiche: ficheJoueur)
    }
  }
}

A part le changement de nom, j’ai déjà expliqué tout ça dans un post précédent.

Les données importantes de ma mini-application sont stockées dans la classe d’objets

class ModeleDatas : ObservableObject {
  @Published var fichesJoueurs = [FicheJoueur]()
}

Mise en oeuvre :

struct ContentView: View {

  @ObservedObject var modeleDatas = ModeleDatas()
    
  var body: some View {
    VStack {
      VStack {
        
        VStack {
          Text("Liste des Joueurs")
            .font(.title)
            .bold()
          FicheJoueursListeView(liste: modeleDatas.fichesJoueurs) 
.frame(height: 350)
            .padding(.horizontal, 20)
        }
      }
      Spacer()      
    }
  }
}

Il ne se passe pas grand chose à l’exécution, la liste des fiches joueurs étant vide.

Pour vérifier les lignes de code, on a souvent besoin d’échantillons de tests, juste pour voir si ça marche.

Une technique que j’utilise souvent est de créer une classe pour générer des jeux d’échantillon. Exemple :

class GenerateurEchantillonsTests {
  static func creerlisteFichesJoueurs() -> [FicheJoueur] {
    var liste = [FicheJoueur]()
    
    liste.append(FicheJoueur(nom: "Sophie Mercier", licence: "L0001"))
    liste.append(FicheJoueur(nom: "Paul Dirac", licence: "L0002"))
    liste.append(FicheJoueur(nom: "Jean Dulac", licence: "L0003"))
    liste.append(FicheJoueur(nom: "Robert Dulac", licence: "L0004"))
    liste.append(FicheJoueur(nom: "Nathalie Newman", licence: "L0005"))
    liste.append(FicheJoueur(nom: "Eric Lopze", licence: "L0010"))
    liste.append(FicheJoueur(nom: "Robert Dulac", licence: "L0012"))
    liste.append(FicheJoueur(nom: "Murielle Michou", licence: "L0013"))
    liste.append(FicheJoueur(nom: "Paul Dulac", licence: "L0014"))
    liste.append(FicheJoueur(nom: "Michel Hautgenoux", licence: "L0015"))
    liste.append(FicheJoueur(nom: "Agnés Torez", licence: "L0017"))
    liste.append(FicheJoueur(nom: "Robert Torez", licence: "L0022"))
    
    return liste
  }
}

La fonction creerListeFichesJoueurs() est une fonction static, c’est-à-dire qu’elle ne nécessite pas de créer un objet de classe GenerateurEchantillonsTests pour l’utiliser.

On peut l’appeler partout dans le code, à condition de préciser le nom de la classe GenerateurEchantillonsTests.

Pour avoir mes fausses fiches disponibles au début de l’application, j’ai inséré un appel au générateur d’échantillons dans la classe ModeleDatas :

class ModeleDatas : ObservableObject {
  @Published var fichesJoueurs =
     GenerateurEchantillonsTests.listeFichesJoueurs()
}

Normalement le ModeleDatas ne contient rien au lancement de l’application, mais là, il se remplit avec les fausses fiches.

Problème : comment détecter la sélection d’une fiche joueur ? List ne contient pas d’instructions pour gérer ce cas.

Voici une version de la fonction affichant une List de fiches joueurs, avec l’ajout d’une gesture et d’une closure d’action :

struct FicheJoueursListeView:View {
  let liste:[FicheJoueur]
  var action: (_ licence:String) -> Void
  
  var body: some View {

    List(liste, id: \.licence) {
      ficheJoueur in
        FicheJoueurView(fiche: ficheJoueur)
        
          .onTapGesture(count: 1, perform: {
            self.action(ficheJoueur.licence)
          })
    }
  }
}

L’association gesture + closure d’action est très pratique. Concrètement cela permet d’avertir l’application qu’il vient de se produire un événement sur un objet particulier de la liste. Ici, chaque fois que l’utilisateur fait un Tap sur une fiche joueur, l’application est au courant, et peut réagir.

struct ContentView: View {

  @ObservedObject var modeleDatas = ModeleDatas()
  
  var body: some View {
    VStack {
      VStack {
        
        VStack {
          Text("Liste des Joueurs")
              .font(.title)
              .bold()
          FicheJoueursListeView(
            liste: modeleDatas.fichesJoueurs,
            // Réaction de l'application quand
            // l'utilisateur fait un "tap" sur une fiche joueur
          action: { licence in
              print ("Numéro de licence choisi : ", licence)
          }
            
          ) .frame(height: 350)
            .padding(.horizontal, 20)
        }
      }
      Spacer()
            
    }
  }
}

La réaction est limité, l’application affichant juste le numéro de licence dans la fenêtre de début d’XCode. Mais c’est une base pour construire des interactions plus évoluées (comme l’ajout du numéro de licence dans une équipe).

La suite au prochain numéro …

EDIT : Je viens de me dire que ce serait peut-être plus lisible pour toi, en stockant le numéro de licence sélectionné dans une variable @State, pour l’afficher sur l’écran. Voici le code modifié :

struct ContentView: View {

  @ObservedObject var modeleDatas = ModeleDatas()
  @State var numeroLicenceSelection : String?
  
  var body: some View {
    VStack {
      VStack {
        
        VStack {
          Text("Liste des Joueurs")
            .font(.title)
            .bold()
          FicheJoueursListeView(
            liste: modeleDatas.fichesJoueurs,
            // Réaction de l'application quand
            // l'utilisateur fait un "tap" sur une fiche joueur
          action: { licence in
              // Mémorisation sélection licence
              numeroLicenceSelection = licence
            }
            
          ) .frame(height: 350)
            .padding(.horizontal, 20)
        }
      }
      Spacer()
      
// Affichage sélection (si elle existe)      
if let licenceSelection = numeroLicenceSelection {
        Text("Licence selection : \(licenceSelection)")
      } else {
        Text("Aucune sélection")
      }
      Spacer()
    }
  }
}

1 « J'aime »

Merci merci @Draken,

J’ai bien lu ton code et tout cela me parle.
Je vais essayer de refaire la même chose par moi-même pour vérifier que j’ai bien tout assimilé.
Un grand merci pour ta patience et ta générosité.
@ bientôt