Pour une démonstration en vidéo, regardez la vidéo suivante :
Un menu contextuel est un menu d’options qui s’affiche lorsqu’un utilisateur effectue un appui long sur une vue.
👉 SwiftUI permet d’ajouter un menu contextuel à n’importe quelle vue en utilisant le modificateur .contextMenu {}.
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.
On peut ajouter plus d’options et organiser les actions en groupes distincts.
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")
}
}
}
}
}
On peut aussi ajouter un menu contextuel sur chaque élément d’une liste.
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")
}
}
}
}
}
On peut également ajouter un menu contextuel sur une image, ce qui est utile pour des applications de galerie ou de réseaux sociaux.
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")
}
}
}
}
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.
SwiftUI propose cinq formes prédéfinies que l’on peut directement utiliser :
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.
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.
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).
Si l’on souhaite combiner un fond et une bordure, on peut superposer (overlay) une forme sur elle-même.
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.
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.
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.
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.
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
}
}
MyShape()
.fill(.red)
.frame(width: 360, height: 350)
Les formes personnalisées sont réutilisables et supportent les modificateurs comme .fill(), .stroke(), etc.
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.
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.
Les animations implicites permettent d’animer les modifications d’un état en ajoutant .animation() à un modificateur.
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.
On peut animer plusieurs propriétés en même temps, comme la rotation et l’échelle.
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.
L’animation spring() ajoute un effet de rebond naturel.
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.
L’animation peut se répéter plusieurs fois avec .repeatCount() ou à l’infini avec .repeatForever().
.animation(.linear(duration: 1).repeatCount(10), value: rotation)
.animation(.linear(duration: 1).repeatForever(autoreverses: true), value: rotation)
autoreverses: true alterne entre les états (va-retour fluide).
L’animation explicite permet de contrôler précisément quelles propriétés doivent être animées.
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é.
On peut animer une vue dès son apparition avec onAppear().
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.
Les transitions animent l’apparition et la disparition des vues.
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.
| 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. |
.transition(AnyTransition.opacity.combined(with: .slide))
Le texte glisse tout en apparaissant progressivement.
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 :
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.
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.
On peut différencier un tap simple et un double tap en précisant count:.
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.
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”.
Un glissement (DragGesture) est souvent utilisé pour déplacer un élément à l’écran.
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.
Le geste de rotation est souvent utilisé pour faire pivoter un élément.
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.
Le geste de pincement (pinch) permet de zoomer ou dézoomer un élément.
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.
On peut combiner plusieurs gestes, par exemple un glissement + une rotation.
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.
ExclusiveGesture permet de choisir quel geste a priorité si plusieurs sont détectés.
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é.
| 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. |