Préservez la réactivité du dessin rapide de rectangles en séparant l’aperçu du glissement actif des formes validées sur le canevas. Dans une application de dessin pour Mac, confiez la saisie à une seule surface de geste, gardez la géométrie temporaire localement et ajoutez-la à la collection de formes uniquement lorsque le glissement se termine avec succès.
Ce guide s’adresse aux développeurs qui créent un outil simple de dessin de rectangles par clic-glissé dans leur propre application macOS. Toutes les formes d’exemple sont conservées en mémoire. L’objectif est d’éviter que les déplacements du pointeur modifient sans cesse l’ensemble du modèle de dessin, puis d’utiliser le profilage pour repérer les éventuels coûts de rendu restants.
Pourquoi le dessin ralentit-il à mesure que les formes validées s’accumulent ?
Commencez par distinguer trois opérations : la réception des entrées, la mise à jour de l’état et le rendu de la géométrie. Un glissement peut modifier fréquemment la géométrie temporaire. Si ces changements remplacent aussi la collection validée, reconstruisent chaque tracé ou invalident une grande partie de la hiérarchie des vues, le travail associé à chaque déplacement peut augmenter au fil du dessin.
Apple explique que SwiftUI suit les dépendances et met à jour les vues lorsque ces dépendances changent. Des mises à jour individuelles de vues trop lentes comme de nombreuses mises à jour inutiles peuvent entraîner le dépassement des échéances d’affichage des images. Séparer l’état d’aperçu qui change fréquemment du contenu validé est une application architecturale de ce modèle documenté. Optimiser les performances de SwiftUI avec Instruments
Canvas permet le dessin en mode immédiat à l’aide d’un contexte graphique. Apple le présente comme une amélioration possible des performances pour les dessins dynamiques complexes, mais il ne fournit ni interaction ni accessibilité pour chaque élément dessiné. Attachez la gestion des entrées à une surface de dessin bien définie ; si l’application doit ensuite permettre d’utiliser chaque forme individuellement, implémentez séparément la sélection et les fonctions d’accessibilité. Canvas
La séparation des responsabilités est donc explicite : la surface de saisie gère le geste, l’état temporaire gère l’aperçu et la vue parente possède les rectangles validés. Cette séparation réduit les dépendances évitables. Elle ne garantit pas que le système ne redessinera jamais le canevas validé.
Séparer l’aperçu du dessin validé
Définissez cette répartition des responsabilités avant de choisir des optimisations de rendu :
| Responsabilité | Propriétaire | Moment de la modification |
|---|---|---|
| Rectangle du glissement en cours | Surface de geste | Pendant le glissement actif |
| Collection de rectangles validés | Vue parente de dessin | Une fois par glissement réussi |
| Dessin des rectangles validés | Canevas d’arrière-plan | Lorsque ses entrées nécessitent une mise à jour |
| Détection du pointeur | Surface de geste | Pendant toute la séquence du geste |
Pour la géométrie temporaire, @GestureState est utile, car SwiftUI la réinitialise lorsque le geste devient inactif. Validez la forme au moyen de onEnded, dont la fonction de rappel reçoit la valeur finale du geste. Apple précise que cette fonction de rappel n’est exécutée que si le geste se termine avec succès ; l’état du geste assure le nettoyage quelle que soit la façon dont il se termine. GestureState, onEnded(_:)
Pour les rectangles, leur géométrie finale ne nécessite que le point de départ et le point courant. Il n’est pas nécessaire de conserver chaque position intermédiaire du pointeur. Apple documente séparément ces positions et les points d’arrivée prédits ; utilisez la position finale réelle pour valider le rectangle. DragGesture.Value
Créer un outil de dessin de rectangles complet en mémoire
Les deux blocs de code suivants constituent un seul fichier source Swift complet. Dans un projet d’application SwiftUI pour macOS ciblant macOS 12 ou une version ultérieure, remplacez le fichier d’entrée généré de l’application par les deux blocs et supprimez toute autre déclaration de point d’entrée de l’application. Canvas est disponible depuis macOS 12. Cet exemple a été examiné, mais n’a pas pu être compilé ici, car l’environnement ne dispose d’aucune chaîne d’outils Swift. Canvas
Le premier bloc définit l’application, la collection validée et son moteur de rendu. La vue parente ne reçoit que les rectangles terminés ; elle ne lit pas l’état du glissement actif.
import SwiftUI
@main
struct RectangleDrawingApp: App {
var body: some Scene {
WindowGroup {
DrawingBoard()
}
}
}
struct DrawingBoard: View {
@State private var rectangles: [CGRect] = []
var body: some View {
VStack(spacing: 8) {
Text("Drag to draw a rectangle")
Text("Committed rectangles: \(rectangles.count)")
ZStack {
Color.white
CommittedRectangles(rectangles: rectangles)
.allowsHitTesting(false)
RectangleGestureSurface { rectangle in
rectangles.append(rectangle)
}
}
.clipped()
.frame(minWidth: 480, minHeight: 320)
}
.padding()
}
}
struct CommittedRectangles: View {
let rectangles: [CGRect]
var body: some View {
Canvas { context, _ in
for rectangle in rectangles {
context.stroke(
Path(rectangle),
with: .color(.blue),
lineWidth: 2
)
}
}
}
}
Ajoutez le bloc suivant au même fichier. La surface transparente couvre la zone de dessin, définit explicitement une forme rectangulaire pour le test de toucher et gère l’unique geste de dessin. Apple indique que contentShape définit la forme utilisée pour le test de toucher et que allowsHitTesting(false) exclut une vue du test de toucher. contentShape(_:eoFill:), allowsHitTesting(_:)
struct RectangleGestureSurface: View {
let commit: (CGRect) -> Void
@GestureState private var preview: CGRect? = nil
var body: some View {
ZStack {
Color.clear
Canvas { context, _ in
if let rectangle = preview {
context.stroke(
Path(rectangle),
with: .color(.orange),
lineWidth: 2
)
}
}
.allowsHitTesting(false)
}
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.updating($preview) { value, state, _ in
state = Self.rectangle(
from: value.startLocation,
to: value.location
)
}
.onEnded { value in
let rectangle = Self.rectangle(
from: value.startLocation,
to: value.location
)
commit(rectangle)
}
)
}
private static func rectangle(
from start: CGPoint,
to end: CGPoint
) -> CGRect {
CGRect(
x: min(start.x, end.x),
y: min(start.y, end.y),
width: abs(end.x - start.x),
height: abs(end.y - start.y)
)
}
}
minimumDistance: 0 supprime la distance minimale requise pour la reconnaissance. L’espace de coordonnées local maintient l’alignement des positions du geste avec le canevas d’aperçu de même taille. Il s’agit d’options de configuration du geste documentées, et non d’une garantie concernant la fréquence des événements ou la latence d’affichage. DragGesture
L’outil de calcul du rectangle normalise les quatre directions de glissement. À titre d’exemple, un glissement de (90, 80) vers (30, 20) produit une origine de (30, 20) et une taille de (60, 60). L’aperçu et la validation utilisent le même calcul ; le relâchement du pointeur ne modifie donc pas la formule géométrique.
Cet outil minimal valide aussi les rectangles de largeur ou de hauteur nulle. Un clic peut donc incrémenter le compteur sans produire de rectangle visible. Si le produit exige une taille minimale, ajoutez explicitement cette règle au moment de la validation et fournissez un retour adapté à l’utilisateur. L’exemple conserve chaque geste terminé avec succès au lieu d’écarter silencieusement les petits résultats.
La zone visible est découpée, mais la géométrie stockée reste inchangée. Un rectangle qui dépasse du tableau peut donc contenir des coordonnées hors du tableau. Avant d’ajouter une limitation des coordonnées, décidez si un redimensionnement ultérieur doit révéler cette géométrie.
Diagnostiquer les coûts restants avant d’ajouter un cache
L’exemple supprime les modifications du modèle validé du chemin de mise à jour du glissement. Le moteur de rendu des formes validées parcourt toujours la collection chaque fois qu’il s’exécute. Distinguez la réduction de la propagation de l’état de la réduction du travail de rendu : une vue séparée n’est pas un cache d’image persistant documenté.
Effectuez une comparaison contrôlée pour trouver le coût restant. Choisissez des collections en mémoire à titre d’exemple, comme 0, 500 et 5 000 rectangles. Gardez constantes les dimensions des rectangles, la taille de la fenêtre, l’échelle d’affichage et la configuration de compilation. Ces nombres sont des données d’essai, pas des limites de capacité ni des résultats mesurés.
Mesurez séparément trois intervalles : le glissement sans changement de la collection, le relâchement qui valide la forme et le redimensionnement de la fenêtre. Un ralentissement continu pendant le glissement suggère un travail différent d’une pause qui survient uniquement lors de la validation. Le comportement au redimensionnement peut révéler une reconstruction de la géométrie ou du cache absente du dessin ordinaire.
La session de profilage SwiftUI d’Apple présente les outils Long View Body Updates, Long Representable Updates et Cause & Effect Graph, ainsi que Time Profiler. Utilisez le graphe des dépendances pour comprendre pourquoi les vues validées sont mises à jour pendant un glissement ; utilisez les échantillons CPU pour repérer les opérations coûteuses de construction de géométrie ou de rendu. Apple a présenté ce flux de travail dans Instruments 26. Optimiser les performances de SwiftUI avec Instruments
Pour les blocages plus généraux, la session WWDC26 d’Apple sur la réactivité commence par Time Profiler et distingue l’exécution active du temps d’attente. Appliquez cette distinction avant de déplacer du travail entre exécuteurs ou de remplacer la pile de rendu. Profiler, corriger et vérifier : améliorer la réactivité des apps avec Instruments
L’aide à la décision suivante est une déduction technique fondée sur ces outils de profilage et sur la répartition des responsabilités ci-dessus :
| Comportement observé | Première piste à examiner | Modification possible |
|---|---|---|
| L’état de dessin parent change pendant le déplacement du pointeur | Rappels de mise à jour du geste et état partagé | Garder l’état de l’aperçu dans la surface de geste |
| Le dessin validé domine les échantillons pendant le déplacement | Parcours répété et création de tracés | Pré calculer la géométrie ou introduire un cache du contenu validé |
| Les pauses au relâchement s’allongent avec la taille de la collection | Modification du tableau et reconstruction complète du dessin | Réduire les instantanés conservés et ne reconstruire que le contenu dérivé concerné |
| Le dessin de rectangles reste réactif, mais les longs tracés à main levée sont lents | Taille du tracé actif et reconstruction | Profiler séparément le tracé actif qui s’allonge |
| Les pauses surviennent surtout pendant le redimensionnement | Rendu dépendant de la taille et invalidation du cache | Définir explicitement le comportement du contenu dérivé lors du redimensionnement |
Un cache ajoute une limite de propriété supplémentaire : la géométrie des rectangles reste la source de vérité, et le cache est un contenu dérivé remplaçable. Intégrez la taille du dessin, l’échelle d’affichage et les styles pertinents à la stratégie d’invalidation. Pour un dessin auquel on ne fait qu’ajouter des formes, les mises à jour incrémentales du cache constituent une piste d’optimisation ; les modifications et les suppressions nécessitent une stratégie claire de reconstruction ou d’invalidation régionale. Vérifiez le résultat avec les mêmes collections d’entrée avant de conserver cette complexité supplémentaire.
Vérifier la copie des tableaux au moment de la validation
Les tableaux Swift ont une sémantique de valeur et utilisent le mécanisme de copie à l’écriture. Les copies peuvent partager le stockage jusqu’à ce qu’une mutation exige un stockage indépendant ; les tableaux contenant des instances de classes partagent toujours les références à ces instances. Il ne faut donc pas affirmer que transmettre un tableau à une vue ou ajouter un élément à un tableau copie systématiquement chaque élément. Array
Pour cet outil, évitez de stocker un nouvel instantané complet du dessin à chaque mise à jour du pointeur. Si le profilage révèle des copies lors de la validation, examinez les instantanés qui restent en mémoire et la manière dont le modèle de dessin est partagé. Remplacer des types valeur par des objets mutables partagés uniquement pour éviter les copies modifie la sémantique de propriété et exige une décision de conception distincte.
Considérer la présentation asynchrone comme une option à mesurer
Apple définit rendersAsynchronously comme une option qui permet au canevas de présenter son contenu de manière asynchrone à sa vue parente. Cette description ne garantit pas une livraison plus rapide des gestes, ne déplace pas le gestionnaire de gestes vers un exécuteur d’arrière-plan et n’élimine pas le parcours de la collection. Évaluez cette option séparément, après avoir défini les limites de la saisie et de l’état. rendersAsynchronously
Quand AppKit doit-il gérer la surface de dessin ?
Utilisez une surface AppKit personnalisée lorsque la tâche exige la gestion directe des méthodes du répondeur ou une invalidation explicite des régions de dessin. Apple décrit NSView comme l’infrastructure de dessin et de gestion des événements, et prend en charge sa sous-classe pour les comportements personnalisés. Il s’agit d’une solution de remplacement prise en charge, avec ses propres responsabilités de rendu. NSView
Dans une implémentation AppKit, conservez la même séquence : initialisez la géométrie temporaire dans mouseDown(with:), mettez-la à jour dans mouseDragged(with:) et validez-la dans mouseUp(with:). Convertissez event.locationInWindow dans le système de coordonnées de la vue destinataire avant de calculer la géométrie. Le guide archivé d’Apple sur les événements de souris décrit cette séquence et la conversion des coordonnées ; la référence actuelle de NSView documente les méthodes de répondeur prises en charge. Gestion des événements de souris, NSView
Demandez le rafraîchissement en définissant needsDisplay = true ; Apple précise que la vue est alors réaffichée au prochain passage de la boucle d’événements. Dans une conception par régions, invalidez l’union des anciennes et nouvelles limites de l’aperçu, en l’élargissant pour inclure le trait. L’invalidation régionale ne réduit le parcours que si l’implémentation du dessin évite aussi de traiter le contenu non concerné. needsDisplay, Gestion des événements de souris
Intégrez la surface avec NSViewRepresentable : créez-la dans makeNSView, synchronisez l’état externe dans updateNSView et utilisez un coordinateur pour communiquer les modifications à SwiftUI. Actualisez le routage des rappels lors des mises à jour et évitez de réinitialiser un brouillon actif simplement parce que SwiftUI fournit une autre valeur représentable. Apple précise également que SwiftUI contrôle la disposition de la vue représentée ; ne remplacez pas directement son cadre ou ses limites gérés par SwiftUI. NSViewRepresentable
Choisissez un seul propriétaire pour la saisie du dessin. Si AppKit gère la séquence de dessin, ne validez pas également cette séquence au moyen d’un geste de glissement SwiftUI qui chevauche la même zone. Pour une sous-classe directe de NSView, Apple demande aux implémentations qui gèrent les événements de souris indiqués de ne pas appeler l’implémentation de la superclasse pour ces événements. NSView
Commencez par une amélioration limitée et observable : un état d’aperçu local, un seul point de validation et un moteur de rendu des formes validées dont les entrées ne changent pas pendant le déplacement. Comparez ensuite des dessins synthétiques de taille croissante dans Instruments. La trace obtenue vous indiquera si la prochaine amélioration utile concerne la gestion des dépendances, la propriété de la collection, la préparation de la géométrie ou le rendu.
Sources
Canvas — Documentation développeur Apple.
GestureState — Documentation développeur Apple.
onEnded(_:) — Documentation développeur Apple.
DragGesture — Documentation développeur Apple.
DragGesture.Value — Documentation développeur Apple.
contentShape(_:eoFill:) — Documentation développeur Apple.
allowsHitTesting(_:) — Documentation développeur Apple.
Array — Documentation développeur Apple.
rendersAsynchronously — Documentation développeur Apple.
Optimiser les performances de SwiftUI avec Instruments — Apple Developer.
Profiler, corriger et vérifier : améliorer la réactivité des apps avec Instruments — Apple Developer.
NSView — Documentation développeur Apple.
needsDisplay — Documentation développeur Apple.
NSViewRepresentable — Documentation développeur Apple.
Gestion des événements de souris — Archives de la documentation Apple.