Parcours
/
Swift
Swift
/
Exercices
/
Système de fenêtrage
Système de fenêtrage

Système de fenêtrage

Exercice d'apprentissage

Introduction

Structures et classes

Les structures et les classes sont deux des principales briques de base de la programmation en Swift. Toutes deux permettent de regrouper des données et des fonctions liées au sein d'unités de fonctionnalité autonomes. Et lorsque tu définis une structure ou une classe, tu définis en fait un nouveau type à utiliser dans Swift, exactement comme les types que tu as déjà utilisés, tels que Int et String.

Il existe de nombreuses similitudes entre les structures et les classes en Swift. Entre autres points communs, les deux peuvent stocker des valeurs dans des propriétés et fournir des fonctionnalités grâce à des méthodes.

Chacune apporte des fonctionnalités supplémentaires, qui sortent du cadre de cet exercice.

Définis des structures et des classes

Les structures comme les classes se définissent à peu près de la même manière. On commence par le mot-clé approprié, suivi du nom du type que l'on définit, puis on place le corps de la structure ou de la classe entre accolades.

Le corps peut être constitué de propriétés stockées, qui se définissent et se comportent comme des constantes ou des variables ordinaires.

struct CharacterStats {
  var health = 0.0
  var speed = 0
  var strength = 0
}

class GameCharacter {
  var stats = CharacterStats()
  var characterClass: String?
  var name: String?
  var active = false
  let id = makeRandomID()
}

Instances

Comme indiqué plus haut, définir une structure ou une classe revient simplement à définir un nouveau type. Ce n'est que le plan qui décrit à quoi ressembleront les valeurs de ce type, mais cela ne crée réellement aucune valeur de ce type avec laquelle travailler.

Pour créer une instance de ce type, il faut écrire le nom du type suivi de parenthèses.

let someStats = CharacterStats()
let someCharacter = GameCharacter()

Cela crée des valeurs de ces types, dont les propriétés sont remplies avec les valeurs par défaut fournies dans la définition. Note que dans les cas optionnels, comme la propriété name de GameCharacter, si aucune valeur n'est fournie, la propriété vaudra nil par défaut, exactement comme lorsqu'on définit des types optionnels ordinaires sans fournir immédiatement de valeur.

Avec les structures, Swift fournit automatiquement ce qu'on appelle un initialiseur membre par membre : on peut y fournir, entre les parenthèses, des valeurs pour les propriétés de la structure, qui remplacent les valeurs par défaut de la définition.

let differentStats = CharacterStats(health: 100.0, speed: 6, strength: 18)

Accède aux propriétés

On peut accéder aux propriétés d'une structure ou d'une classe à l'aide de la notation pointée : on écrit le nom de la valeur, suivi d'un point (.) et du nom de la propriété. Si une propriété d'une structure ou d'une classe possède elle-même des propriétés, cette notation pointée permet aussi d'accéder à ces propriétés imbriquées.

Cette notation sert à la fois à lire la valeur de la propriété et, lorsque c'est autorisé, à la modifier.

someStats.health
// => 0
someCharacter.name
// => nil

someStats.health = 87.3
someStats.health
// => 87.3
someCharacter.name = "Luther"
someCharacter.name
// => "Luther"

someCharacter.id  = "new id"
// Error: Cannot assign to property: 'id' is a 'let' constant

Méthodes

Comme les propriétés, qui stockent des données dans les structures et les classes, tu peux aussi définir des méthodes, qui stockent des fonctions dans une structure ou une classe.

Les méthodes se définissent de la même manière qu'une fonction ordinaire, mais à l'intérieur du corps de la structure ou de la classe. Note que si une fonction modifie la valeur d'une propriété d'une structure, elle doit être précédée du mot-clé mutating. De plus, si une propriété peut être modifiée par une méthode, elle doit être définie avec var plutôt que let, exactement comme des variables ordinaires.

struct CharacterStats {
  var health = 0.0
  var speed = 0
  var strength = 0

  mutating func takeHit(_ damage: Double) {
    health = max(0.0, health - damage)
  }
  func canLift(_ weight: Int) -> Bool {
    weight < strength * 100
  }
}

class GameCharacter {
  var stats = CharacterStats()
  var characterClass: String?
  var name: String?
  var active = false
  let id: String = makeRandomID()

  func takesDamage(_ damage: Double) {
    stats.takeHit(damage)
    if stats.health <= 0 {
      active = false
    }
  }
  func sayName() -> String {
    return "My name is \(name ?? "no one"), my class is \(characterClass ?? "undetermined")"
  }
  func lift(_ weight: Int) -> String {
    if stats.canLift(weight) {
      return "No problem!"
    } else {
      return "Ooof! No way."
    }
  }
}

Ces méthodes s'appellent avec la notation pointée, exactement comme les propriétés.

var myChar = GameCharacter()
myChar.stats = CharacterStats(health: 72.8, speed: 19, strength: 6)
myChar.active = true

myChar.lift(750)
// => "Ooof! No way."
myChar.takesDamage(80)
myChar.active
// => false

Self

Chaque instance d'une structure ou d'une classe possède une propriété implicite nommée self, qui fait référence à l'instance elle-même. L'usage le plus courant de self est de lever l'ambiguïté entre une propriété d'instance et un paramètre qui porte le même nom :

struct Position {
    var x = 0

    mutating func move(x: Int) {
        // Here, 'self.x' refers to the struct's property,
        // while 'x' refers to the parameter passed to the method.
        self.x = x
    }
}

Les types Swift proposent aussi un mot-clé Self avec une majuscule, qui désigne le type de l'instance plutôt que l'instance elle-même.

Instructions

Dans cet exercice, tu vas simuler un système informatique à fenêtres. Tu vas créer des fenêtres que l'on peut déplacer et redimensionner, et qui affichent leur contenu. L'image suivante illustre les valeurs avec lesquelles tu vas travailler ci-dessous.

                  <--------------------- screenSize.width --------------------->

       ^          ╔════════════════════════════════════════════════════════════╗
       |          ║                                                            ║
       |          ║          position.x,_                                      ║
       |          ║          position.y  \                                     ║
       |          ║                       \<----- size.width ----->            ║
       |          ║                 ^      *──────────────────────┐            ║
       |          ║                 |      │        title         │            ║
       |          ║                 |      ├──────────────────────┤            ║
screenSize.height ║                 |      │                      │            ║
       |          ║            size.height │                      │            ║
       |          ║                 |      │       contents       │            ║
       |          ║                 |      │                      │            ║
       |          ║                 |      │                      │            ║
       |          ║                 v      └──────────────────────┘            ║
       |          ║                                                            ║
       |          ║                                                            ║
       v          ╚════════════════════════════════════════════════════════════╝

1. Définis une structure Size

Définis une structure nommée Size avec deux propriétés Int, width et height, qui stockent respectivement la largeur et la hauteur actuelles de la fenêtre. La largeur et la hauteur initiales doivent être de 80 et 60, respectivement. Ajoute une méthode resize(newWidth:newHeight:) qui prend en paramètres une nouvelle largeur et une nouvelle hauteur, et qui modifie les propriétés pour refléter la nouvelle taille.

let size1080x764 = Size(width: 1080, height: 764)
// Returns Size
var size1200x800 = size1080x764
// Returns Size
size1200x800.resize(newWidth: 1200, newHeight: 800)
size1200x800.height
// Returns 800

2. Définis une structure Position

Définis une structure nommée Position avec deux propriétés Int, x et y, qui stockent respectivement la position horizontale et la position verticale actuelles du coin supérieur gauche de la fenêtre. Les valeurs initiales de x et de y doivent chacune être 0. La position (0, 0) correspond au coin supérieur gauche de l'écran : les valeurs de x augmentent quand on va vers la droite, et celles de y augmentent quand on descend.

Ajoute une méthode moveTo(newX:newY:) qui prend en paramètres une nouvelle valeur de x et une nouvelle valeur de y, et qui modifie les propriétés pour refléter la nouvelle position.

var point = Position(x: 10, y: 20)
// Returns Position
point.moveTo(newX: 100, newY: -100)
point.y
// Returns -100

3. Définis une classe Window

Définis une classe de fenêtre avec les propriétés suivantes :

  • title : String, la valeur initiale est "New Window"
  • screenSize : Size, une valeur constante avec width = 800 et height = 600
  • size : Size, la valeur initiale est la valeur par défaut de la structure Size
  • position : Position, la valeur initiale est la valeur par défaut de la structure Position
  • contents : String?, la valeur initiale est nil

Tu dois aussi définir un initialiseur vide pour la classe.

let window = Window()
window.title
// Returns "New Window"

4. Ajoute une méthode pour redimensionner les fenêtres

  • resize(to:) : (Size) -> () - Cette méthode prend une structure Size en entrée et tente de redimensionner la fenêtre à la taille demandée. Cependant, la nouvelle taille ne peut pas dépasser certaines limites.
  • La hauteur ou la largeur minimale autorisée est 1. Toute hauteur ou largeur demandée inférieure à 1 sera ramenée à 1.
  • La hauteur et la largeur maximales dépendent de la position actuelle de la fenêtre, car les bords de la fenêtre ne peuvent pas dépasser ceux de l'écran. Les valeurs supérieures à ces limites seront ramenées à la plus grande taille possible. Par exemple, si la fenêtre est positionnée à x = 400, y = 300 et qu'un redimensionnement à height = 400, width = 300 est demandé, alors la fenêtre sera redimensionnée à height = 300, width = 300, car l'écran n'est pas assez grand dans la direction y pour satisfaire entièrement la demande.

5. Ajoute une méthode pour déplacer les fenêtres

  • move(to:) : (Position) -> () - C'est similaire à resize(to:), sauf que cette méthode ajuste la position de la fenêtre à la valeur demandée, plutôt que sa taille. Comme pour resize, la nouvelle position ne peut pas dépasser certaines limites.
  • La plus petite position est 0 pour x comme pour y.
  • La position maximale dans chaque direction dépend de la taille actuelle de la fenêtre ; les bords ne peuvent pas dépasser ceux de l'écran. Les valeurs supérieures à ces limites seront ramenées à la plus grande taille possible. Par exemple, si la fenêtre est positionnée à x = 250, y = 100 et qu'un déplacement à x = 600, y = 200 est demandé, alors la fenêtre sera déplacée à x = 550, y = 200, car l'écran n'est pas assez grand dans la direction x pour satisfaire entièrement la demande.

6. Ajoute des méthodes pour mettre à jour le texte de la fenêtre et afficher les informations de la fenêtre

  • update(title:) : (String) -> () - Cette méthode affecte à la propriété title la valeur de la string passée en argument.
  • update(text:) : (String?) -> () - Cette méthode affecte à la propriété contents la valeur de la string optionnelle passée en argument.
  • display() : () -> String - Cette méthode renvoie une string décrivant l'état actuel de la fenêtre. Par exemple, si la fenêtre a pour title "My First Window" avec pour position : x = 10, y = 100 ; pour taille : width = 200, height = 150 ; et pour contents : "I 😍 my window", elle doit renvoyer la string : "My First Window\nPosition: (10, 100), Size: (200 x 150)\nI 😍 my window\n" - Si contents vaut nil, la dernière ligne doit être "[This window intentionally left blank]"

7. Crée un initialiseur pour la classe Window

Le système de fenêtres doit disposer d'un initialiseur pour que l'on puisse créer une fenêtre avec des entrées personnalisées. Crée un autre initialiseur pour la classe Window. Cet initialiseur doit prendre les paramètres suivants : title, contents, ainsi que deux paramètres optionnels size et position. Les paramètres size et position doivent prendre par défaut les valeurs par défaut des structures Size et Position.

let window = Window(title: "My First Window", contents: "I 😍 my window")
window.display()
// Returns "My First Window\nPosition: (0, 0), Size: (80 x 60)\nI 😍 my window\n"

let window2 = Window(title: "My Second Window", contents: "I 😍 my window", size: Size(width: 200, height: 150), position: Position(x: 10, y: 100))
window2.display()
// Returns "My Second Window\nPosition: (10, 100), Size: (200 x 150)\nI 😍 my window\n"
Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
Swift Exercism

Prêt à commencer Système de fenêtrage ?

Inscris-toi sur Exercism pour apprendre et maîtriser Swift avec 35 concepts116 exercices, et un vrai mentorat humain, le tout gratuitement.