Pour rendre cliquable l’espace vide à l’intérieur d’une ligne SwiftUI sur Mac, étendez son contenu à toute la largeur disponible et appliquez-lui une forme de contenu rectangulaire. Pour une ligne qui exécute une action, placez les deux modificateurs dans le libellé d’un Button.
Ce tutoriel crée une petite app macOS dont les lignes contiennent des formes et du texte synthétiques. Cliquer sur une forme, son libellé ou l’espace vide à côté exécute la même action associée à la ligne. L’espace situé en dehors de la ligne ne fait pas partie de la zone cible de cette action.
Pourquoi le texte réagit-il alors que l’espace vide ne réagit pas ?
La mise en page et la détection des interactions répondent à deux questions différentes. Un cadre détermine comment une vue occupe l’espace qui lui est proposé ; une forme de contenu définit la zone utilisée pour la détection des interactions. Apple les documente séparément dans son modificateur de cadre flexible et son modificateur de forme de contenu.
Par exemple, une ligne peut sembler large parce que son conteneur remplit une fenêtre, alors que l’interaction est associée uniquement à une courte vue de texte. De même, ajouter de l’espace vide à la mise en page ne précise pas explicitement que toute la ligne doit participer à la détection des interactions. La solution pratique consiste à définir d’abord les limites souhaitées pour la ligne, puis à attribuer une forme d’interaction à cette zone.
Utilisez .frame(maxWidth: .infinity, alignment: .leading) pour permettre au libellé de s’étendre lorsque son parent lui propose une largeur supplémentaire. Appliquez ensuite .contentShape(.interaction, Rectangle()) à ce libellé étendu. Apple définit le type de forme de contenu interaction spécifiquement pour la détection des interactions et l’accessibilité.
Cela ne rend pas la ligne infiniment large. Le cadre reste soumis à la proposition de mise en page de son parent. Un parent étroit produit une ligne étroite, et une forme de contenu ne peut pas créer l’espace de mise en page manquant. Cette combinaison répond à deux exigences : un libellé suffisamment large et une zone explicite pour la détection des interactions.
Créer un exemple complet de bouton pour une ligne
Créez dans Xcode un projet d’app macOS avec SwiftUI et Swift. Définissez macOS 12 ou une version ultérieure comme cible de déploiement pour utiliser la surcharge explicite de forme de contenu .interaction, qu’Apple indique comme disponible à partir de macOS 12. Remplacez la déclaration d’app et la vue de contenu générées par le programme ci-dessous, qui tient dans un seul fichier ; ne conservez qu’une seule déclaration d’app @main. Consultez la documentation d’Apple sur la surcharge de forme de contenu pour connaître sa disponibilité.
L’exemple utilise un Button, car chaque ligne déclenche une action. Apple recommande les boutons pour les éléments d’interface qui exécutent une action et permet explicitement d’utiliser des vues quelconques comme libellés de bouton. Sa documentation sur les gestes de toucher recommande également de personnaliser un bouton lorsqu’un contrôle basé sur un geste aurait un comportement équivalent.
import SwiftUI
@main
struct ClickableRowsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct SampleItem: Identifiable {
let id: Int
let title: String
let color: Color
}
struct ContentView: View {
private let items = [
SampleItem(id: 1, title: "Blue square", color: .blue),
SampleItem(id: 2, title: "Orange square", color: .orange),
SampleItem(id: 3, title: "Purple square", color: .purple)
]
@State private var lastActivated = "None"
@State private var activationCount = 0
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Choose a shape")
.font(.headline)
ScrollView {
VStack(spacing: 8) {
ForEach(items) { item in
Button {
lastActivated = item.title
activationCount += 1
} label: {
HStack(spacing: 12) {
Rectangle()
.fill(item.color)
.frame(width: 24, height: 24)
Text(item.title)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.frame(maxWidth: .infinity,
alignment: .leading)
.contentShape(.interaction, Rectangle())
}
.buttonStyle(.plain)
.background(Color.gray.opacity(0.12))
}
}
}
Text("Last activated: \(lastActivated)")
Text("Activations: \(activationCount)")
}
.padding(16)
.frame(minWidth: 360, minHeight: 280)
}
}
Tous les éléments et l’état d’interaction restent en mémoire. Chaque élément a un identifiant entier fixe, conformément au protocole Identifiable de Swift, qui représente une identité stable. La vue parente possède l’état d’activation, conformément aux recommandations d’Apple qui préconisent de garder l’State local privé dans la vue qui doit le posséder.
Lancez l’app et cliquez sur la zone vide à droite de n’importe quel titre. Le nombre d’activations doit augmenter de un, et le statut doit indiquer le nom de cette ligne. L’arrière-plan gris rend visible la zone prévue pour la ligne pendant cette vérification ; la forme de contenu explicite définit la zone d’interaction.
Ce programme a été vérifié à l’aide de la documentation d’API consultée, mais n’a pas été compilé ni exécuté ici, car aucun environnement Swift n’était disponible. Vérifiez le comportement avec le pointeur et le clavier dans votre cible macOS avant de l’intégrer.
Où placer le cadre, le remplissage et la forme de contenu ?
Dans cet exemple, l’ordre est intentionnel : construisez le contenu du libellé, ajoutez son remplissage interne, étendez le libellé obtenu, puis définissez une forme d’interaction pour cette zone. Toutes ces opérations ont lieu dans la fermeture du libellé du bouton. Apple explique que l’ordre des modificateurs compte.
En plaçant le remplissage à l’intérieur de ce libellé auquel une forme est attribuée, vous incluez la zone rembourrée dans la cible souhaitée. Placer le remplissage à l’extérieur du bouton crée de l’espace autour dans la mise en page ; ne supposez pas que cet espace appartient au libellé interactif du bouton. De même, étendre uniquement un conteneur extérieur ne définit pas la même géométrie du libellé qu’étendre le contenu à l’intérieur du bouton.
Le modificateur .buttonStyle(.plain) garde l’exemple visuellement simple. Apple décrit le style de bouton simple comme dépourvu de décoration à l’état inactif, avec des effets possibles lorsque le bouton est enfoncé, sélectionné ou activé. C’est un choix de style, qui ne remplace pas la définition de la largeur et de la forme de contenu du libellé.
Pour une cible de déploiement antérieure à macOS 12, remplacez .contentShape(.interaction, Rectangle()) par .contentShape(Rectangle()). Apple indique que l’ancienne surcharge définit la forme de détection des interactions et précise qu’elle est disponible à partir de macOS 10.15. Ce remplacement modifie la version requise de l’API de forme de contenu ; vérifiez séparément les autres API utilisées par votre app.
Une ligne doit-elle utiliser un bouton, la sélection d’une liste ou un geste ?
Choisissez l’interaction en fonction de ce que signifie le clic sur la ligne. Rendre son espace vide réactif doit préserver cette signification ainsi que la responsabilité des contrôles présents dans la ligne.
| Comportement pour l’utilisateur | Point de départ approprié | Limite à respecter |
|---|---|---|
| Toute la ligne exécute une action | Un Button avec un libellé étendu auquel une forme est attribuée |
Conservez un seul responsable de l’action pour la ligne |
| La ligne représente la sélection actuelle | Une List avec une liaison de sélection |
Laissez la liste gérer le comportement de sélection |
| Une vue personnalisée nécessite un geste de clic distinct | Une vue avec une forme et onTapGesture |
Associez le geste à la zone voulue |
| La ligne contient plusieurs contrôles indépendants | Des contrôles frères distincts | Attribuez une cible propre à chaque contrôle |
Pour la sélection dans une liste, utilisez le modèle de sélection du conteneur avant d’ajouter un gestionnaire de clic couvrant toute la ligne. La documentation d’Apple sur List décrit une liaison vers l’identifiant facultatif d’un élément pour une sélection unique, et vers un ensemble d’identifiants pour une sélection multiple. Un bouton qui exécute une action et une ligne de liste qui devient sélectionnée répondent à des besoins d’interaction différents.
Un geste reste approprié lorsque vous avez spécifiquement besoin de reconnaître un geste sur une vue personnalisée. Appliquez la même séquence géométrique — remplissage et cadre, puis forme de contenu — avant d’ajouter onTapGesture. Apple indique dans la documentation de son paramètre count le nombre de pressions ou de clics requis. Une forme de contenu définit la zone cible ; elle ne fournit pas d’action à elle seule. Consultez onTapGesture(count:perform:).
Si la ligne contient aussi un bouton ou un interrupteur distinct, gardez-le en dehors du libellé du bouton principal. Une structure pratique consiste en une pile horizontale contenant un bouton principal extensible et un contrôle distinct à droite. L’espace vide du libellé principal appartient à son action ; le contrôle à droite possède sa propre zone. Évitez d’imbriquer des contrôles interactifs dans un bouton qui englobe toute la ligne.
N’ajoutez pas de modificateurs de priorité des gestes simplement pour corriger la détection des clics dans l’espace vide. Apple indique que highPriorityGesture modifie la priorité, tandis que simultaneousGesture permet à plusieurs gestes d’être traités ensemble. Ces opérations modifient les relations entre les gestes et peuvent affecter les interactions des vues descendantes ; elles ne définissent pas la géométrie manquante de la ligne.
Diagnostiquer une ligne qui ne détecte toujours pas les clics
Suivez les étapes ci-dessous sur la version la plus simple de votre ligne. Elles permettent de distinguer un problème de dimensionnement d’un problème de forme de la cible ou de vue superposée.
- Vérifiez les limites réelles du libellé. Ajoutez temporairement
.border(Color.orange)juste après le modificateur de cadre du libellé. Si la bordure s’arrête à côté du texte, examinez la proposition de largeur du parent et toute contrainte de largeur fixe. - Vérifiez le placement des modificateurs. Confirmez que la forme de contenu couvre le libellé étendu, y compris le remplissage que vous voulez rendre cliquable.
- Vérifiez à quel élément appartient l’interaction. Pour une ligne bouton, utilisez l’action du bouton. Supprimez les gestes de toucher redondants sur la ligne pendant que vous isolez le problème.
- Vérifiez les éléments décoratifs superposés. Examinez les superpositions et les vues sœurs placées au-dessus de la ligne. Pour une décoration qui ne doit jamais recevoir d’entrée, envisagez d’appliquer
.allowsHitTesting(false)à cette décoration uniquement. - Rétablissez les autres contrôles un à un. Après avoir rétabli chaque contrôle, vérifiez sa zone, puis testez l’espace vide restant.
Apple indique que allowsHitTesting(_:) détermine si une vue participe aux opérations de détection des interactions. L’appliquer à toute la ligne exclurait celle-ci de ces opérations ; ciblez donc la vue décorative concernée si c’est le comportement voulu.
Effectuez quelques clics à des endroits distincts pour confirmer le résultat. Le tableau décrit le comportement attendu pour le programme fourni ; il ne présente pas de résultats de tests mesurés.
| Emplacement du clic | Résultat attendu |
|---|---|
| Carré coloré ou titre de la ligne | Activer cette ligne une fois |
| Espace vide à droite, à l’intérieur de la ligne grise | Activer cette ligne une fois |
| Remplissage interne au-dessus ou au-dessous du titre | Activer cette ligne une fois |
| Espace entre les lignes | Aucune activation de ligne |
| Remplissage extérieur ou zone vide sous les lignes | Aucune activation de ligne |
Redimensionnez la fenêtre et vérifiez de nouveau l’espace vide. Vérifiez également le focus clavier et l’activation avec les réglages de navigation au clavier de votre Mac. Ces vérifications permettent de confirmer que le composant final se comporte comme le contrôle prévu, au-delà de sa simple réaction au pointeur.
Quand la détection des interactions d’AppKit est-elle pertinente ?
Pour une ligne SwiftUI ordinaire, commencez par les API SwiftUI de mise en page, de forme de contenu et de contrôle. AppKit devient pertinent lorsque votre app inclut des vues natives personnalisées ou une vue intégrée qui modifie la cible des événements.
Apple indique que NSView.hitTest(_:) trouve le descendant le plus éloigné contenant un point. Une fenêtre l’utilise principalement pour déterminer quelle vue reçoit un événement de pression de souris. Le point d’entrée est exprimé dans le système de coordonnées de la supervue, et Apple précise qu’il est rarement nécessaire d’appeler cette méthode directement.
Si vous gérez une vue AppKit personnalisée, examinez ses limites et sa redéfinition de la détection des interactions à cet endroit. Une forme de contenu SwiftUI ne demande pas de contourner la gestion des événements d’une vue native distincte. Effectuez la correction dans la hiérarchie de vues qui reçoit le clic.
Pour la ligne de ce tutoriel, la démarche prise en charge est simple : faites occuper à l’étiquette du bouton la largeur voulue pour la ligne, définissez sa forme d’interaction rectangulaire et laissez aux autres contrôles leurs propres zones. L’espace vide utile devient ainsi cliquable tout en préservant les autres interactions de la fenêtre.
Sources
- contentShape(_:eoFill:) — Apple
- contentShape(::eoFill:) — Apple
- interaction — Apple
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple
- Button — Apple
- plain — Apple
- padding(::) — Apple
- Identifiable — Apple
- State — Apple
- List — Apple
- onTapGesture(count:perform:) — Apple
- highPriorityGesture(_:including:) — Apple
- simultaneousGesture(_:including:) — Apple
- allowsHitTesting(_:) — Apple
- hitTest(_:) — Apple