Quando um arrasto na tela termina, calcule a geometria final do item no sistema de coordenadas do alvo. Primeiro, teste a contenção completa; em seguida, a sobreposição com área positiva. Este tutorial implementa essa regra para itens retangulares no seu próprio app para Mac e explica o equivalente no AppKit.
Defina o que conta como sobreposição e contenção
Considere uma tela simples de desktop com um retângulo azul móvel e um alvo cinza fixo. A tarefa do leitor é informar se o item solto cabe inteiramente dentro do alvo, se o sobrepõe parcialmente ou se não compartilha nenhuma área com ele. Tudo no exemplo é sintético e armazenado na memória.
A contenção é direcional: o alvo deve conter o item. A Apple documenta que CGRectContainsRect(target, item) retorna verdadeiro quando a união dos retângulos é igual ao alvo. Isso inclui um item que corresponde exatamente ao alvo. Verificar a contenção primeiro evita que um item totalmente contido seja informado como sobreposição parcial. Documentação de CGRectContainsRect
Neste exemplo, a sobreposição exige uma região compartilhada com largura positiva e altura positiva. Apenas tocar uma borda ou um canto conta como Sem sobreposição de área. Calcule a interseção e examine suas dimensões, em vez de deixar implícito o comportamento do contato. A Apple documenta que CGRectIntersection retorna o retângulo compartilhado ou um retângulo nulo quando não há interseção. Documentação de CGRectIntersection
| Resultado | Regra usada neste tutorial | Exemplo |
|---|---|---|
Contido |
O alvo contém o item inteiro, inclusive quando as bordas coincidem | Um item cabe dentro do alvo ou corresponde exatamente a ele |
Sobreposição |
A contenção falha, mas a região compartilhada tem largura e altura positivas | O item ultrapassa a borda do alvo |
Sem sobreposição de área |
Não há interseção com área positiva | O item está separado ou apenas toca uma borda |
Geometria inválida |
Um dos retângulos tem coordenadas não finitas ou dimensões não positivas | Um item com largura zero |
Estas são regras do aplicativo derivadas das operações geométricas. Se a sua interface exigir uma margem interna no alvo, defina essa margem separadamente e teste em relação a um retângulo de aceitação menor. Mantenha o alvo visível e a regra de aceitação compreensíveis para quem arrasta o item.
Use um único sistema de coordenadas e a tradução final do arrasto
O retângulo que você desenha e o retângulo que classifica devem descrever a mesma posição. Neste exemplo, tanto o item quanto o alvo usam coordenadas da tela. A tela recebe um sistema de coordenadas nomeado, e o DragGesture do item usa esse mesmo nome. O modificador atual coordinateSpace(_:) da Apple oferece suporte a esse padrão no macOS 14 e posterior. Documentação de sistemas de coordenadas nomeados
Armazene o retângulo confirmado do item separadamente da sua tradução temporária durante o arrasto. Durante o gesto, desenhe o retângulo confirmado deslocado pela tradução. Ao concluir com sucesso, calcule novamente esse retângulo final a partir do valor recebido no callback, classifique-o e confirme a posição uma única vez.
A Apple define translation como o deslocamento total desde o início do arrasto até o evento atual. Portanto, somar repetidamente a tradução inteira a uma posição já atualizada acumularia movimento em excesso. Aplique-a à posição confirmada que não foi alterada. Documentação da tradução do arrasto
O ponto de soltura, por si só, não permite determinar se o item inteiro cabe dentro de um alvo. A pessoa pode pegar o retângulo perto de um dos cantos, deixando grande parte dele para fora mesmo quando o ponteiro está dentro. Aplicar a tradução também preserva o deslocamento original da pegada sem mover o centro do item até o ponteiro.
Crie um exemplo completo de retângulos em SwiftUI
Crie no Xcode um app SwiftUI para macOS com destino de implantação macOS 14 ou posterior. Substitua o ponto de entrada do app gerado pelos dois blocos abaixo, colocados em sequência em um único arquivo Swift. Remova o ponto de entrada original para que o destino contenha apenas uma declaração @main. Juntos, os dois blocos formam o programa completo.
O primeiro bloco define o classificador geométrico. Ele rejeita explicitamente dimensões inválidas, em vez de convertê-las silenciosamente em outro retângulo. Uma enumeração Swift mantém distintos os quatro resultados possíveis; seus valores brutos de string fornecem as mensagens exibidas. Documentação de enumerações Swift
import SwiftUI
import CoreGraphics
enum Placement: String {
case contained = "Contained"
case overlap = "Overlap"
case separate = "No area overlap"
case invalid = "Invalid geometry"
}
func isUsable(_ rect: CGRect) -> Bool {
let values = [rect.origin.x, rect.origin.y,
rect.width, rect.height,
rect.maxX, rect.maxY]
return values.allSatisfy { $0.isFinite }
&& rect.width > 0 && rect.height > 0
}
func classify(item: CGRect, target: CGRect) -> Placement {
guard isUsable(item), isUsable(target) else {
return .invalid
}
if CGRectContainsRect(target, item) {
return .contained
}
let shared = CGRectIntersection(item, target)
if !shared.isNull && shared.width > 0 && shared.height > 0 {
return .overlap
}
return .separate
}
O segundo bloco fornece o app e a tela. Anexe o gesto ao retângulo móvel, conforme descrito na documentação DragGesture da Apple. O tamanho fixo da tela deixa explícita a geometria deste exemplo. Documentação de DragGesture
@main
struct RectanglePlacementApp: App {
var body: some Scene {
WindowGroup {
PlacementCanvas()
}
}
}
struct PlacementCanvas: View {
private let target = CGRect(x: 200, y: 100,
width: 200, height: 160)
@State private var item = CGRect(x: 40, y: 120,
width: 80, height: 60)
@State private var message = "Drag the blue item, then release."
@GestureState private var translation: CGSize = .zero
private var preview: CGRect {
item.offsetBy(dx: translation.width,
dy: translation.height)
}
private var move: some Gesture {
DragGesture(minimumDistance: 1,
coordinateSpace: .named("placementCanvas"))
.updating($translation) { value, state, _ in
state = value.translation
}
.onEnded { value in
let finalItem = item.offsetBy(
dx: value.translation.width,
dy: value.translation.height
)
let result = classify(item: finalItem, target: target)
message = result.rawValue
if result != .invalid {
item = finalItem
}
}
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(message)
ZStack(alignment: .topLeading) {
Color.white
Rectangle()
.fill(Color.gray.opacity(0.25))
.frame(width: target.width, height: target.height)
.position(x: target.midX, y: target.midY)
Rectangle()
.fill(Color.blue)
.frame(width: item.width, height: item.height)
.position(x: preview.midX, y: preview.midY)
.gesture(move)
}
.frame(width: 640, height: 380)
.coordinateSpace(.named("placementCanvas"))
Button("Reset") {
item = CGRect(x: 40, y: 120, width: 80, height: 60)
message = "Drag the blue item, then release."
}
}
.padding()
}
}
Arraste o item azul sobre o alvo cinza e solte-o. A mensagem registra o último posicionamento concluído. Os itens com sobreposição parcial e os itens soltos em posições separadas permanecem onde foram soltos; a contenção não altera automaticamente a relação com o elemento pai nem encaixa o item em outro contêiner. O botão Redefinir restaura o retângulo inicial.
Intencionalmente, não há atualização persistente da posição no callback de atualização do gesto. @GestureState mantém apenas a tradução de pré-visualização e é redefinido quando o gesto fica inativo. A Apple especifica que onEnded é executado somente após a conclusão bem-sucedida e recomenda o estado do gesto para valores que devem ser redefinidos independentemente de como o gesto termina. Assim, um gesto interrompido pode descartar a pré-visualização e preservar o retângulo confirmado e o resultado anterior. Documentação de GestureState, documentação de onEnded
Esta implementação trata os retângulos preenchidos como a geometria. Adicionar uma sombra ou um contorno decorativo não amplia o retângulo de aceitação. Se você adicionar redimensionamento, zoom ou rotação, atualize o modelo geométrico e a conversão de coordenadas antes de confiar no mesmo classificador.
Confira os casos de limite antes de adicionar mais itens
Use os seguintes casos sintéticos com o alvo do exemplo: origem (200, 100), largura 200, altura 160. As coordenadas descrevem retângulos de itens no mesmo sistema de coordenadas da tela.
| Origem do item | Tamanho do item | Resultado esperado | Motivo |
|---|---|---|---|
(220, 120) |
80 × 60 |
Contido |
Todas as bordas estão dentro do alvo |
(360, 120) |
80 × 60 |
Sobreposição |
O item ultrapassa a borda direita do alvo |
(400, 120) |
80 × 60 |
Sem sobreposição de área |
Apenas as bordas verticais se tocam |
(401, 120) |
80 × 60 |
Sem sobreposição de área |
Há um espaço entre os retângulos |
(200, 100) |
200 × 160 |
Contido |
Retângulos iguais satisfazem a condição de contenção |
(180, 80) |
240 × 200 |
Sobreposição |
O item engloba o alvo, invertendo a relação de contenção |
Os cálculos da tabela foram conferidos com uma operação determinística independente. O programa Swift foi inspecionado, mas não compilado no ambiente de pesquisa, que não fornecia um compilador Swift. Compile-o no destino do app para macOS e teste posições de soltura perto de cada limite.
Para vários itens na memória, preserve a identidade do item selecionado no início do arrasto. Ao concluir, compare sua geometria final com os alvos elegíveis e exclua o próprio item. Se vários alvos forem válidos, defina uma regra de seleção estável, como uma ordem explícita dos alvos. A classificação geométrica, por si só, não determina a qual alvo o item deve pertencer.
Aplique a mesma regra geométrica em uma tela AppKit
Em uma subclasse de visualização AppKit personalizada existente, mantenha o classificador independente da entrega de eventos. mouseUp(with:) informa a soltura do botão esquerdo do mouse; use-o para finalizar apenas um arrasto que a sua tela já controla. Uma soltura do mouse não relacionada não deve acionar o posicionamento. Documentação de mouse-up do AppKit
Uma sequência prática de eventos é:
- Em
mouseDown(with:), selecione um item sintético usando a regra de teste de acerto já existente na tela. Salve sua identidade, o retângulo inicial e a posição inicial do ponteiro. - Converta a posição de cada evento de mouse relevante para coordenadas da tela. Para uma visualização anexada à janela, a expressão de conversão é
convert(event.locationInWindow, from: nil). - Em
mouseDragged(with:), derive uma pré-visualização a partir do retângulo salvo e do deslocamento do ponteiro. - Em
mouseUp(with:), recalcule o deslocamento usando o evento de soltura, classifique o retângulo final, confirme-o uma única vez e limpe o arrasto ativo.
A Apple documenta locationInWindow como a posição de um evento de mouse em coordenadas da janela. A sobrecarga de conversão de pontos de NSView.convert(_:from:) converte a partir de coordenadas da janela quando o argumento de visualização é nil. Isso evita comparar um ponto da janela com um retângulo expresso nas coordenadas locais da tela. Documentação da localização do evento, Documentação de conversão de pontos
Mantenha o tratamento de eventos no componente que já controla a interação. A Apple diferencia uma subclasse direta de NSView, que trata um evento, de uma subclasse que precisa deixar seu elemento pai tratar eventos não consumidos. Siga esse comportamento documentado da cadeia de resposta; adicionar lógica de posicionamento não justifica interceptar a sequência de outra visualização nem processar a mesma soltura por dois manipuladores independentes. Orientações de tratamento de eventos de NSView
Saiba quando retângulos não respondem a perguntas sobre formas
O exemplo fornece uma resposta exata para retângulos preenchidos alinhados aos eixos. Para círculos, contornos côncavos, vazados ou formas rotacionadas, os retângulos delimitadores fornecem apenas um teste geométrico preliminar. A área compartilhada deles pode incluir espaços que nenhuma das formas visíveis preenche.
Um teste para saber se um ponto está dentro de um caminho também não permite estabelecer a contenção completa de outra forma. O NSBezierPath.contains(_:) do AppKit aceita um único ponto no sistema de coordenadas do caminho, e a Apple informa que ele não leva em conta a largura do traçado. Portanto, testar o centro de um item responde apenas se esse centro está dentro do caminho. Documentação de contenção de pontos do NSBezierPath
Para caminhos preenchidos no macOS 14 e posterior, o SwiftUI documenta operações booleanas compatíveis: Path.intersection(_:eoFill:) produz a região preenchida comum, e Path.subtracting(_:eoFill:) produz a parte da primeira região preenchida que fica fora da segunda. Essas operações oferecem um meio baseado em caminhos para investigar a sobreposição e a parte de um item que fica fora do alvo. Alinhe ambos os caminhos no mesmo sistema de coordenadas e use a regra de preenchimento correspondente à renderização. Documentação de interseção de Path, Documentação de subtração de Path
Essas operações tratam de regiões preenchidas e pressupõem que subcaminhos abertos sejam fechados. Uma linha traçada ou uma regra do aplicativo sobre o contato entre limites exige uma definição geométrica própria. Mantenha o classificador retangular para itens retangulares; amplie deliberadamente a camada geométrica quando a tela começar a desenhar outras formas.
Um resultado confiável ao soltar decorre de três escolhas concretas: preservar a geometria inicial do arrasto, calcular o item final no sistema de coordenadas do alvo e aplicar uma política explícita de contenção e sobreposição. Com essas escolhas definidas, a interface pode informar o resultado de forma consistente, sem acoplar a geometria ao roteamento de eventos ou à propriedade dos itens.
Fontes
- CGRectContainsRect(::) — Apple
- CGRectIntersection(::) — Apple
- coordinateSpace(_:) — Apple
- tradução — Apple
- Enumerações — Swift.org
- DragGesture — Apple
- GestureState — Apple
- onEnded(_:) — Apple
- mouseUp(with:) — Apple
- locationInWindow — Apple
- convert(_:from:) — Apple
- NSView — Apple
- contains(_:) — Apple
- intersection(_:eoFill:) — Apple
- subtracting(_:eoFill:) — Apple