Associe o gesto de arrastar do elemento pai com simultaneousGesture(_:including:) para permitir que ele participe junto com as interações dos elementos filhos. Mantenha as ações de clique nos botões filhos, defina um limiar de movimento para o arraste e decida explicitamente se a seleção pode ocorrer junto com o arraste.
Este passo a passo cria um painel móvel com três botões de itens fictícios. A tarefa do leitor é específica: clicar em um item o seleciona, enquanto arrastar o painel altera sua posição dentro do mesmo app para Mac. O exemplo permite que seleção e movimento coexistam; uma variação com uma alça separada oferece regiões de interação distintas quando isso for preferível.
Escolha o contrato de interação antes de alterar a prioridade dos gestos
A Apple documenta simultaneousGesture(_:including:) como a adição de um gesto com a mesma prioridade dos gestos existentes. O exemplo mostra um manipulador de toque em um elemento filho e um manipulador de toque no contêiner que o envolve respondendo ambos a um clique. Esse é o mecanismo de composição relevante quando o elemento pai deve participar sem deliberadamente ter precedência sobre o elemento filho. Documentação da Apple sobre gestos simultâneos
No entanto, o reconhecimento simultâneo não expressa, por si só, a regra “arrastar deve cancelar todas as ações dos elementos filhos”. O exemplo da Apple também não estabelece que todos os estilos de botão e todos os contêineres arbitrem um botão e um gesto de arrastar da mesma forma. Trate a participação dos gestos e o comportamento do app como decisões separadas.
Neste painel, o elemento pai controla o deslocamento do painel e o item selecionado. Cada elemento filho contém um botão que solicita a seleção por meio de um fechamento. Uma solicitação de seleção continua sendo aceita mesmo que a mesma interação do ponteiro mova o painel. Se o comportamento exigido proibir a ativação durante o movimento, use a variação com alça separada abaixo, em vez de depender de uma ordem de chamadas não documentada.
Use um Button para a ação do elemento filho. A Apple recomenda explicitamente botões quando uma interação precisa do comportamento de botão, incluindo o comportamento de acessibilidade padrão. Uma forma personalizada pode continuar sendo o rótulo do botão; ela não precisa se tornar um gesto de toque isolado. Documentação de gestos da Apple
Crie um painel móvel completo com botões filhos
Crie um app SwiftUI para macOS no Xcode, defina o destino de implantação como macOS 14 ou posterior e substitua o app e a visualização de conteúdo gerados pelo seguinte arquivo único. Mantenha apenas um ponto de entrada do app no destino. O requisito do macOS 14 contempla o modificador moderno coordinateSpace(_:) usado aqui. Documentação da Apple sobre espaços de coordenadas
Todos os itens e estados existem na memória. O exemplo move o painel inteiro, incluindo seus botões, dentro de uma área fixa. Ele não implementa a transferência de itens nem move a janela do app.
import SwiftUI
@main
struct GesturePanelApp: App {
var body: some Scene {
WindowGroup {
PanelDemo()
}
}
}
private struct DemoItem: Identifiable {
let id: Int
let title: String
let color: Color
}
private struct ItemTarget: View {
let item: DemoItem
let isSelected: Bool
let select: () -> Void
var body: some View {
Button(action: select) {
HStack(spacing: 10) {
Circle()
.fill(item.color)
.frame(width: 18, height: 18)
Text(item.title)
Spacer()
if isSelected {
Text("Selected")
.font(.caption)
}
}
.padding(12)
.frame(width: 220)
.background(
isSelected
? Color.accentColor.opacity(0.18)
: Color.gray.opacity(0.12)
)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
private struct PanelDemo: View {
private let items = [
DemoItem(id: 1, title: "Item A", color: .blue),
DemoItem(id: 2, title: "Item B", color: .orange),
DemoItem(id: 3, title: "Item C", color: .purple)
]
@State private var selectedID: Int? = nil
@State private var committedOffset: CGSize = .zero
@GestureState private var liveTranslation: CGSize = .zero
private var pan: some Gesture {
DragGesture(
minimumDistance: 8,
coordinateSpace: .named("stage")
)
.updating($liveTranslation) { value, translation, _ in
translation = value.translation
}
.onEnded { value in
committedOffset = CGSize(
width: committedOffset.width + value.translation.width,
height: committedOffset.height + value.translation.height
)
}
}
var body: some View {
VStack(spacing: 12) {
Text("Click an item or drag the panel")
ZStack {
Color.gray.opacity(0.06)
VStack(spacing: 10) {
Text("Drag panel")
.font(.headline)
.frame(width: 244, height: 32)
.contentShape(Rectangle())
ForEach(items) { item in
ItemTarget(
item: item,
isSelected: selectedID == item.id,
select: { selectedID = item.id }
)
}
}
.padding(16)
.background(Color.gray.opacity(0.15))
.contentShape(Rectangle())
.simultaneousGesture(pan, including: .all)
.offset(
x: committedOffset.width + liveTranslation.width,
y: committedOffset.height + liveTranslation.height
)
}
.frame(width: 640, height: 420)
.coordinateSpace(.named("stage"))
Button("Reset panel") {
committedOffset = .zero
}
}
.padding(20)
}
}
Este programa foi inspecionado com base na documentação de API consultada, mas não foi compilado nem testado interativamente aqui: este ambiente não tem compilador Swift nem ambiente de execução da interface do macOS. Compile-o no Xcode e faça as verificações abaixo nas versões do macOS que você oferece suporte.
Por que o gesto de arrastar tem um limiar diferente de zero
DragGesture expõe uma distância mínima de movimento antes que o gesto seja reconhecido. O valor ilustrativo 8 dá aos cliques comuns alguma tolerância de movimento antes de o elemento pai começar a arrastar. É uma escolha do app, não um limiar prescrito pela Apple nem uma política completa para distinguir clique de arraste. Um arraste com distância zero elimina essa distinção específica. Documentação da Apple sobre DragGesture
Por que o movimento temporário e o confirmado são separados
Durante o reconhecimento, updating(_:body:) grava a translação atual em liveTranslation. A Apple documenta que o estado do gesto é redefinido quando o gesto termina ou é cancelado. O exemplo confirma o deslocamento final apenas em onEnded, de modo que o cancelamento descarta a pré-visualização não confirmada em vez de deixar uma translação temporária obsoleta. Documentação da Apple sobre updating
A posição exibida é o deslocamento confirmado mais a translação atual. A Apple define translation como o deslocamento total desde o início do arraste, portanto adicioná-lo repetidamente ao deslocamento confirmado a cada alteração faria o movimento ser contado em excesso. Por exemplo, um deslocamento horizontal confirmado de 40 e uma translação atual de 12 produzem um deslocamento exibido de 52; ao concluir o arraste, 52 é confirmado uma vez. Documentação da Apple sobre translation
O espaço de coordenadas nomeado pertence à área fixa que envolve o painel móvel. Isso faz com que o cálculo do movimento se refira a uma região de referência explícita. Os botões filhos não mantêm cópias próprias da posição do painel.
Por que a seleção permanece no elemento pai
O elemento pai armazena selectedID e fornece a cada elemento filho seu estado de seleção atual e um fechamento de seleção. Isso segue a orientação da Apple de manter o estado na visualização mais alta que precisa dele e, em seguida, passar valores ou vinculações pela hierarquia. Documentação da Apple sobre State
O fechamento captura o item atual e solicita uma atualização do estado do elemento pai. Swift permite que fechamentos capturem valores do contexto ao redor; não é necessário copiar nem encaminhar um evento do AppKit para implementar esse retorno de chamada. A linguagem de programação Swift: Closures
Entenda as regiões de acerto e as máscaras de gesto
contentShape(Rectangle()) define a forma usada nos testes de acerto. No rótulo do botão, ela vem depois do preenchimento e do quadro, deixando explícita a região retangular pretendida para o rótulo. No elemento pai, define a forma de interação do painel. Ela não define a prioridade dos reconhecedores nem exige que dois gestos cooperem. Documentação da Apple sobre content shape
A máscara fornecida a um modificador de gesto determina quais gestos permanecem habilitados. É fácil interpretar essas opções incorretamente: subviews significa que o gesto adicionado ao elemento pai está desabilitado, não que ele coopere automaticamente com os elementos descendentes. Documentação da Apple sobre GestureMask
| Máscara | Efeito documentado | Adequação aqui |
|---|---|---|
.all |
Habilita o gesto adicionado e outros gestos na visualização e em suas subvisualizações | Ponto de partida para participação compartilhada |
.gesture |
Habilita o gesto adicionado e desabilita os gestos na hierarquia de subvisualizações | Entra em conflito com a preservação dos gestos dos elementos filhos |
.subviews |
Habilita os gestos das subvisualizações e desabilita o gesto adicionado | Desabilita o gesto de arrastar do elemento pai |
.none |
Desabilita os gestos na hierarquia de subvisualizações, incluindo o gesto adicionado | Desabilita a interação pretendida |
Evite tratar highPriorityGesture como uma solução genérica para a ausência de chamadas de retorno do gesto de arrastar do elemento pai. Sua finalidade documentada é dar precedência maior ao gesto associado. O exemplo da Apple demonstra que o elemento pai tem precedência sobre um toque no elemento filho. Escolha essa opção quando essa precedência for o comportamento pretendido. Documentação da Apple sobre gestos de alta prioridade
Use uma alça de arraste separada quando a ativação precisar ser distinta
Para um painel cujos botões não devem compartilhar uma região de arraste, mantenha o mesmo estado e cálculo de movimento, mas associe pan apenas ao cabeçalho. Remova .simultaneousGesture(pan, including: .all) do painel, que é o elemento pai.
Substitua a expressão do cabeçalho existente por este trecho dentro do programa completo acima. Ele usa a propriedade pan já definida; nenhum outro pré-requisito muda.
Text("Drag panel")
.font(.headline)
.frame(width: 244, height: 32)
.contentShape(Rectangle())
.gesture(pan)
Agora, o cabeçalho e os botões dos itens ocupam regiões distintas, e os botões já não têm o gesto de arrastar do painel associado a um ancestral. Começar sobre um item é uma interação com o botão; começar no cabeçalho é o meio de mover o painel. Esse design é útil quando a participação compartilhada tornaria o resultado ambíguo.
Mantenha o botão nativo quando o elemento filho inicia uma ação. A Apple descreve Button como uma ação mais um rótulo e permite visualizações personalizadas como rótulos, conforme usado nos itens coloridos fictícios. Substituí-lo por um manipulador de ponteiro feito à mão exigiria recriar um comportamento que o controle já fornece. Documentação da Apple sobre Button
Preserve a propriedade dos eventos do AppKit nas fronteiras entre frameworks
Se o painel incluir uma visualização do AppKit, diagnostique seus testes de acerto separadamente da composição de gestos do SwiftUI. O método hitTest(_:) do AppKit retorna o descendente correspondente mais profundo e ajuda a janela a determinar o destinatário de um evento de pressionamento do mouse. Seu ponto de entrada é expresso nas coordenadas da supervisualização. Documentação da Apple sobre testes de acerto do AppKit
Retornar um elemento pai nos testes de acerto altera o alvo inicial; não equivale a permitir que um elemento filho e um elemento pai sejam reconhecidos simultaneamente. Da mesma forma, retornar nil no teste de acerto de uma sobreposição a remove da consideração — isso não torna a sobreposição uma observadora passiva de toda a interação do elemento filho.
A documentação do AppKit afirma que a implementação padrão de mouseDown(with:) passa a mensagem ao próximo respondedor. Isso é comportamento da cadeia de respondedores, não uma promessa de que cada ancestral recebe uma cópia independente depois que um controle trata o evento. Conecte a ativação do controle e o movimento do painel por meio de retornos de chamada explícitos ou reconhecedores que você controla. Documentação da Apple sobre mouseDown
Quando você controla dois reconhecedores de gestos do AppKit e precisa de reconhecimento simultâneo, o AppKit fornece gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:). A Apple documenta que retornar true permite que o par seja reconhecido em conjunto; retornar false não garante a exclusão se o delegado do outro reconhecedor permitir o reconhecimento simultâneo. Limite essa política ao par de reconhecedores pretendido. Documentação da Apple sobre o delegado do reconhecedor
Verifique a interação com uma pequena matriz de comportamentos
Teste exatamente o estilo de botão, a hierarquia e as versões do macOS que seu app oferece suporte. As verificações a seguir ajudam a distinguir problemas de reconhecimento de problemas na aritmética do movimento e esclarecem se a superfície compartilhada corresponde ao contrato pretendido.
| Ação | O que verificar |
|---|---|
| Clique em cada item sem arrastar | Seu estado de seleção muda; o painel fica parado |
| Arraste o cabeçalho além do limiar | O painel acompanha o ponteiro e mantém sua posição final |
| Arraste a partir de um item na versão compartilhada | O movimento do elemento pai funciona; determine se a ativação do botão também ocorre e confirme que isso é aceitável |
| Faça dois arrastes consecutivos | O segundo começa na posição confirmada pelo primeiro, sem acumular cada translação intermediária |
| Interrompa um arraste ativo | A translação temporária é limpa; nenhum movimento inacabado é confirmado |
| Experimente a variação com alça | Os itens são ativados de forma independente; o movimento começa no cabeçalho |
| Ative os botões dos itens pela navegação com teclado | A seleção funciona sem exigir um arraste do ponteiro |
Se uma área em branco do painel puder ser arrastada corretamente, mas as regiões dos itens não, inspecione os controles filhos e a composição de gestos. Se os cliques não funcionarem, inspecione as máscaras de gesto, a precedência e as regiões de acerto. Se o painel acelerar ou saltar, inspecione a acumulação das translações e o espaço de coordenadas escolhido antes de adicionar mais reconhecedores.
O exemplo deixa deliberadamente o movimento sem limites, então o painel pode ultrapassar a área visível; o botão Reset panel restaura sua posição. Para um espaço de trabalho finalizado com limites, adicione uma restrição de posição explícita com base nas dimensões da área e do painel. Mantenha a mesma estrutura de controle: os elementos filhos solicitam ações, o elemento pai controla o movimento e o contrato de interação determina se as regiões de reconhecimento se sobrepõem.
Fontes
- simultaneousGesture(_:including:) — Apple
- Gestures — Apple
- coordinateSpace(_:) — Apple
- DragGesture — Apple
- updating(_:body:) — Apple
- translation — Apple
- State — Apple
- Closures — The Swift Programming Language, projeto Swift
- contentShape(_:eoFill:) — Apple
- GestureMask — Apple
- highPriorityGesture(_:including:) — Apple
- Button — Apple
- hitTest(_:) — Apple
- mouseDown(with:) — Apple
- gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) — Apple