Module 9 - Note de cours

← Retour au module principal

Menus contextuels en SwiftUI

Pour une démonstration en vidéo, regardez la vidéo suivante :

Qu’est-ce qu’un menu contextuel en SwiftUI ?

Un menu contextuel est un menu d’options qui s’affiche lorsqu’un utilisateur effectue un appui long sur une vue.

🎯 Exemple d’utilisation :

👉 SwiftUI permet d’ajouter un menu contextuel à n’importe quelle vue en utilisant le modificateur .contextMenu {}.

Création d’un menu contextuel simple

Exemple : Changer les couleurs d’un texte via un menu contextuel

import SwiftUI

struct ContentView: View {
    @State private var foregroundColor: Color = .black
    @State private var backgroundColor: Color = .white

    var body: some View {
        Text("Hello, world!")
            .font(.largeTitle)
            .padding()
            .foregroundColor(foregroundColor)
            .background(backgroundColor)
            .contextMenu {
                Button(action: {
                    self.foregroundColor = .black
                    self.backgroundColor = .white
                }) {
                    Label("Couleurs normales", systemImage: "paintbrush")
                }

                Button(action: {
                    self.foregroundColor = .white
                    self.backgroundColor = .black
                }) {
                    Label("Couleurs inversées", systemImage: "paintbrush.fill")
                }
            }
    }
}

contextMenu {} : Ajoute un menu qui s’affiche au clic long sur le Text.

Boutons du menu :

Utilisation de Label("Texte", systemImage: "icone") : Pour ajouter une icône au bouton.

🔎 Comment tester ?

  1. Exécutez l’application.
  2. Faites un appui long sur le texte “Hello, world!”.
  3. Un menu s’affiche avec les options de changement de couleur.

Ajouter des options avancées au menu

On peut ajouter plus d’options et organiser les actions en groupes distincts.

Exemple : Ajouter des tailles de police et une option de réinitialisation

struct ContentView: View {
    @State private var foregroundColor: Color = .black
    @State private var backgroundColor: Color = .white
    @State private var fontSize: CGFloat = 24

    var body: some View {
        Text("Hello, world!")
            .font(.system(size: fontSize))
            .padding()
            .foregroundColor(foregroundColor)
            .background(backgroundColor)
            .contextMenu {
                Section {
                    Button(action: { fontSize = 16 }) {
                        Label("Petite police", systemImage: "textformat.size")
                    }
                    Button(action: { fontSize = 32 }) {
                        Label("Grande police", systemImage: "textformat.size")
                    }
                }
                
                Section {
                    Button(action: {
                        foregroundColor = .black
                        backgroundColor = .white
                        fontSize = 24
                    }) {
                        Label("Réinitialiser", systemImage: "arrow.counterclockwise")
                    }
                }
            }
    }
}

Nouveautés dans cet exemple :

🔎 Comment tester ?

Ajouter un menu contextuel à une liste d’éléments

On peut aussi ajouter un menu contextuel sur chaque élément d’une liste.

Exemple : Menu contextuel sur une liste de contacts

struct ContentView: View {
    let contacts = ["Alice", "Bob", "Charlie", "David"]

    var body: some View {
        List(contacts, id: \.self) { contact in
            Text(contact)
                .contextMenu {
                    Button(action: {
                        print("Envoyer un message à \(contact)")
                    }) {
                        Label("Envoyer un message", systemImage: "message")
                    }
                    
                    Button(action: {
                        print("Appeler \(contact)")
                    }) {
                        Label("Appeler", systemImage: "phone")
                    }
                }
        }
    }
}

Nouveautés dans cet exemple :

🔎 Comment tester ?

  1. Lancez l’application et faites un appui long sur un contact.
  2. Le menu affiche les options d’appel et de message.

Ajouter une image avec un menu contextuel

On peut également ajouter un menu contextuel sur une image, ce qui est utile pour des applications de galerie ou de réseaux sociaux.

Exemple : Menu contextuel pour interagir avec une image

struct ContentView: View {
    var body: some View {
        Image(systemName: "photo")
            .resizable()
            .frame(width: 100, height: 100)
            .foregroundColor(.blue)
            .contextMenu {
                Button(action: {
                    print("Partager la photo")
                }) {
                    Label("Partager", systemImage: "square.and.arrow.up")
                }
                
                Button(action: {
                    print("Supprimer la photo")
                }) {
                    Label("Supprimer", systemImage: "trash")
                }
            }
    }
}

Explication :

Résumé des concepts abordés

Dessin en 2D avec SwiftUI 🎨

Pour une démonstration en vidéo, regardez la vidéo suivante :

SwiftUI propose des outils puissants pour dessiner des formes, personnaliser leur apparence et créer des tracés sur mesure.

🛠 Pourquoi apprendre le dessin 2D en SwiftUI ?

Dessiner des formes de base

SwiftUI propose cinq formes prédéfinies que l’on peut directement utiliser :

Exemple : Afficher un cercle rouge de 200 x 200 pixels

Circle()
    .fill(.red)
    .frame(width: 200, height: 200)

.fill(.red) : Remplit le cercle en rouge.

.frame(width: 200, height: 200) : Définit la taille du cercle.


Exemple : Ajouter une bordure bleue à une capsule

Capsule()
    .stroke(lineWidth: 10)
    .foregroundColor(.blue)
    .frame(width: 200, height: 100)

.stroke(lineWidth: 10) : Dessine uniquement les contours de la forme.

.foregroundColor(.blue) : Définit la couleur de la bordure.


Exemple : Créer un rectangle arrondi avec un contour en pointillés

RoundedRectangle(cornerRadius: 20)
    .stroke(style: StrokeStyle(lineWidth: 8, dash: [10]))
    .foregroundColor(.blue)
    .frame(width: 200, height: 100)

.stroke(style: StrokeStyle(lineWidth: 8, dash: [10])) : dash: [10] crée un effet de pointillés (10 points de trait, 10 points d’espace).

Ajouter une superposition (overlay)

Si l’on souhaite combiner un fond et une bordure, on peut superposer (overlay) une forme sur elle-même.

Exemple : Un ovale rouge avec un contour bleu

Ellipse()
    .fill(.red)
    .overlay(
        Ellipse()
            .stroke(.blue, lineWidth: 10)
    )
    .frame(width: 250, height: 150)

.overlay() superpose une deuxième ellipse au-dessus de la première.

Dessiner un tracé

Un Path permet de créer des formes personnalisées en définissant des points et en reliant ces points avec des lignes ou des courbes.

Exemple : Dessiner un triangle

struct ContentView: View {
    var body: some View {
        Path { path in
            path.move(to: CGPoint(x: 10, y: 0))
            path.addLine(to: CGPoint(x: 10, y: 350))
            path.addLine(to: CGPoint(x: 300, y: 300))
            path.closeSubpath()
        }
        .stroke(Color.blue, lineWidth: 5)
    }
}

move(to:) : Place le point de départ.

addLine(to:) : Ajoute une ligne reliant deux points.

closeSubpath() : Ferme la forme pour créer un triangle.

Créer une forme personnalisée

Les formes prédéfinies sont utiles, mais parfois nous avons besoin de formes plus complexes.

Pour cela, nous créons une structure qui respecte le protocole Shape.

Exemple : Créer une forme personnalisée avec une courbe

struct MyShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()

        path.move(to: CGPoint(x: rect.minX, y: rect.minY))
        path.addQuadCurve(
            to: CGPoint(x: rect.minX, y: rect.maxY),
            control: CGPoint(x: rect.midX, y: rect.midY)
        )
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        path.closeSubpath()

        return path
    }
}

Utilisation de la forme :

MyShape()
    .fill(.red)
    .frame(width: 360, height: 350)

Les formes personnalisées sont réutilisables et supportent les modificateurs comme .fill(), .stroke(), etc.

Ajouter des dégradés de couleur

SwiftUI propose trois types de dégradés :

Exemple : Un cercle avec un dégradé radial

struct ContentView: View {
    let colors = Gradient(colors: [.red, .yellow, .green, .blue, .purple])
    
    var body: some View {
        Circle()
            .fill(RadialGradient(gradient: colors, center: .center, startRadius: 0, endRadius: 300))
            .frame(width: 200, height: 200)
    }
}

Le centre du cercle est rouge et devient violet sur les bords.

Animations et transitions en SwiftUI 🎬✨

Pour une démonstration en vidéo, regardez la vidéo suivante :

SwiftUI facilite l’animation des vues grâce à un système de gestion fluide et performant des animations.

🛠 Pourquoi utiliser des animations ?

Animation implicite (.animation())

Les animations implicites permettent d’animer les modifications d’un état en ajoutant .animation() à un modificateur.

Exemple : Rotation animée d’un bouton

import SwiftUI

struct ContentView: View {
    @State private var rotation: Double = 0

    var body: some View {
        Button(action: {
            self.rotation = (self.rotation < 360 ? self.rotation + 60 : 0)
        }) {
            Text("Cliquez pour tourner")
                .rotationEffect(.degrees(rotation))
                .animation(.linear(duration: 1), value: rotation)
        }
    }
}

À chaque clic, le texte tourne de 60 degrés.

.animation(.linear(duration: 1), value: rotation) ajoute une transition fluide sur 1 seconde.

Animation avec plusieurs propriétés

On peut animer plusieurs propriétés en même temps, comme la rotation et l’échelle.

Exemple : Un bouton qui tourne et grandit progressivement

struct ContentView: View {
    @State private var rotation: Double = 0
    @State private var scale: CGFloat = 1

    var body: some View {
        Button(action: {
            self.rotation = (self.rotation < 360 ? self.rotation + 60 : 0)
            self.scale = (self.scale < 2.8 ? self.scale + 0.3 : 1)
        }) {
            Text("Cliquez pour animer")
                .scaleEffect(scale)
                .rotationEffect(.degrees(rotation))
                .animation(.linear(duration: 1), value: rotation)
        }
    }
}

Le bouton tourne ET grandit à chaque clic.

Utiliser une animation avec effet de rebond (spring())

L’animation spring() ajoute un effet de rebond naturel.

Exemple : Un bouton qui rebondit légèrement

Text("Cliquez pour animer")
    .scaleEffect(scale)
    .rotationEffect(.degrees(rotation))
    .animation(.spring(response: 1, dampingFraction: 0.2, blendDuration: 0), value: rotation)

spring(response: 1, dampingFraction: 0.2) exagère le mouvement et rebondit légèrement avant de s’arrêter.

Répéter une animation

L’animation peut se répéter plusieurs fois avec .repeatCount() ou à l’infini avec .repeatForever().

Exemple : Répéter une animation 10 fois

.animation(.linear(duration: 1).repeatCount(10), value: rotation)

Exemple : Répéter une animation en boucle infinie

.animation(.linear(duration: 1).repeatForever(autoreverses: true), value: rotation)

autoreverses: true alterne entre les états (va-retour fluide).

Animation explicite (withAnimation {})

L’animation explicite permet de contrôler précisément quelles propriétés doivent être animées.

Exemple : Animer une rotation mais pas le changement de taille

Button(action: { 
    withAnimation(.linear(duration: 2)) {
        self.rotation = (self.rotation < 360 ? self.rotation + 60 : 0)
    }
    self.scale = (self.scale < 2.8 ? self.scale + 0.3 : 1)
}) {
    Text("Cliquez pour animer")
        .rotationEffect(.degrees(rotation))
        .scaleEffect(scale)
}

Seule la rotation est animée, le changement d’échelle est instantané.

Animation au chargement d’une vue (onAppear())

On peut animer une vue dès son apparition avec onAppear().

Exemple : Faire tourner une icône dès l’affichage

struct ContentView: View {
    @State private var rotation: Double = 0

    var body: some View {
        Image(systemName: "forward.fill")
            .rotationEffect(.degrees(rotation))
            .onAppear {
                withAnimation(Animation.linear(duration: 5).repeatForever(autoreverses: false)) {
                    rotation = 360
                }
            }
    }
}

L’icône tourne indéfiniment dès que la vue est affichée.

Ajout de transitions animées (.transition())

Les transitions animent l’apparition et la disparition des vues.

Exemple : Faire apparaître un texte avec un effet de fondu

struct ContentView: View {
    @State private var isVisible: Bool = false

    var body: some View {
        VStack {
            Toggle("Afficher le texte", isOn: $isVisible.animation(.easeInOut(duration: 1)))
                .padding()

            if isVisible {
                Text("Hello, world!")
                    .font(.largeTitle)
                    .transition(.opacity)
            }
        }
    }
}

Le texte apparaît et disparaît en fondu lorsqu’on active/désactive le toggle.

Types de transitions disponibles

Transition Effet
.opacity Fondu en entrée/sortie.
.scale Agrandissement/réduction.
.slide Déplacement vers l’intérieur/l’extérieur.
.move(edge: .leading) Arrivée par la gauche.
.asymmetric(insertion: .scale, removal: .slide) Effet différent à l’apparition et à la disparition.

Exemple : Une transition qui combine opacity et slide

.transition(AnyTransition.opacity.combined(with: .slide))

Le texte glisse tout en apparaissant progressivement.

Gestes et interactions tactiles (Gesture Recognizers) en SwiftUI 👆📱

Pour une démonstration en vidéo, regardez la vidéo suivante :

SwiftUI permet de gérer facilement les interactions tactiles grâce aux gestures (reconnaisseurs de gestes). Ces gestes permettent aux utilisateurs d’interagir avec l’interface via :

Ajout d’un simple tap (.onTapGesture)

Le geste le plus basique en SwiftUI est l’appui simple (TapGesture). Il est utile pour déclencher une action en touchant un élément.

Exemple : Changer la couleur d’un cercle avec un tap

struct ContentView: View {
    @State private var isRed = false

    var body: some View {
        Circle()
            .fill(isRed ? .red : .blue)
            .frame(width: 150, height: 150)
            .onTapGesture {
                isRed.toggle()
            }
    }
}

À chaque tap, la couleur alterne entre rouge et bleu.

Gérer un double tap (.onTapGesture(count:))

On peut différencier un tap simple et un double tap en précisant count:.

Exemple : Changer la taille d’un cercle avec un double tap

struct ContentView: View {
    @State private var size: CGFloat = 100

    var body: some View {
        Circle()
            .frame(width: size, height: size)
            .onTapGesture(count: 2) {
                size = (size == 100 ? 200 : 100)
            }
    }
}

Un double tap agrandit ou réduit le cercle.

Gérer un appui long (.onLongPressGesture)

struct ContentView: View {
    @State private var isLiked = false

    var body: some View {
        Image(systemName: isLiked ? "heart.fill" : "heart")
            .resizable()
            .frame(width: 100, height: 100)
            .foregroundColor(.red)
            .onLongPressGesture {
                isLiked.toggle()
            }
    }
}

Un appui long active ou désactive l’icône de “like”.

Détecter un glissement (DragGesture)

Un glissement (DragGesture) est souvent utilisé pour déplacer un élément à l’écran.

Exemple : Faire glisser un carré

struct ContentView: View {
    @State private var offset = CGSize.zero

    var body: some View {
        Rectangle()
            .fill(.blue)
            .frame(width: 100, height: 100)
            .offset(offset)
            .gesture(
                DragGesture()
                    .onChanged { gesture in
                        offset = gesture.translation
                    }
                    .onEnded { _ in
                        offset = .zero
                    }
            )
    }
}

Le carré suit le doigt lors du drag et revient à sa position initiale après le relâchement.

Détecter une rotation (RotationGesture)

Le geste de rotation est souvent utilisé pour faire pivoter un élément.

Exemple : Faire tourner une image avec deux doigts

struct ContentView: View {
    @State private var rotation: Angle = .zero

    var body: some View {
        Image(systemName: "arrow.triangle.2.circlepath")
            .resizable()
            .frame(width: 150, height: 150)
            .rotationEffect(rotation)
            .gesture(
                RotationGesture()
                    .onChanged { angle in
                        rotation = angle
                    }
            )
    }
}

L’image tourne en fonction du mouvement des doigts.

Zoom avec un pincement (MagnificationGesture)

Le geste de pincement (pinch) permet de zoomer ou dézoomer un élément.

Exemple : Agrandir une image avec deux doigts

struct ContentView: View {
    @State private var scale: CGFloat = 1.0

    var body: some View {
        Image(systemName: "photo")
            .resizable()
            .frame(width: 150, height: 150)
            .scaleEffect(scale)
            .gesture(
                MagnificationGesture()
                    .onChanged { value in
                        scale = value
                    }
            )
    }
}

L’image grandit lorsqu’on l’écarte avec deux doigts et se réduit lorsqu’on pince.

Combiner plusieurs gestes (SimultaneousGesture)

On peut combiner plusieurs gestes, par exemple un glissement + une rotation.

Exemple : Glisser ET tourner un carré

struct ContentView: View {
    @State private var offset = CGSize.zero
    @State private var rotation: Angle = .zero

    var body: some View {
        Rectangle()
            .fill(.green)
            .frame(width: 100, height: 100)
            .offset(offset)
            .rotationEffect(rotation)
            .gesture(
                SimultaneousGesture(
                    DragGesture()
                        .onChanged { gesture in
                            offset = gesture.translation
                        },
                    RotationGesture()
                        .onChanged { angle in
                            rotation = angle
                        }
                )
            )
    }
}

Le carré peut être déplacé ET tourné en même temps.

Différencier plusieurs gestes (ExclusiveGesture)

ExclusiveGesture permet de choisir quel geste a priorité si plusieurs sont détectés.

Exemple : Un simple tap et un appui long sur le même élément

struct ContentView: View {
    @State private var message = "Tap ou Long Press"

    var body: some View {
        Text(message)
            .padding()
            .gesture(
                ExclusiveGesture(
                    TapGesture()
                        .onEnded { _ in message = "Tap détecté !" },
                    LongPressGesture()
                        .onEnded { _ in message = "Appui long détecté !" }
                )
            )
    }
}

Si l’utilisateur appuie longtemps, le tap est ignoré.

Résumé des gestes en SwiftUI

Geste Fonction
.onTapGesture() Détecte un tap.
.onTapGesture(count: 2) Détecte un double tap.
.onLongPressGesture() Détecte un appui long.
DragGesture() Détecte un glissement.
RotationGesture() Détecte une rotation avec deux doigts.
MagnificationGesture() Détecte un pincement pour zoomer.
SimultaneousGesture() Permet d’effectuer deux gestes en même temps.
ExclusiveGesture() Donne la priorité à un seul geste parmi plusieurs.
style="margin-left: 30px;"