Combine DragGesture, RotateGesture e MagnifyGesture com simultaneously(with:) e, em seguida, renderize seus valores por meio de um único pipeline de transformações. Para uma tela Canvas do Mac, mantenha a superfície de entrada fixa e armazene separadamente a alteração temporária de cada gesto e o resultado confirmado.

Este tutorial cria um pequeno visualizador de formas dentro do seu próprio app para macOS: arraste com o clique pressionado para deslocar, faça um gesto de pinça para ampliar e use um gesto de rotação no trackpad para girar a forma. Ele usa apenas um retângulo e um círculo na memória. A composição permite que gestos reconhecidos se sobreponham; quais entradas realmente se sobrepõem depende dos eventos enviados pelo dispositivo.

O que significa simultâneo no Mac?

A simultaneously(with:) da Apple combina dois gestos em outro gesto, permitindo combinar o resultado com um terceiro. Isso fornece a composição necessária para pan, rotação e zoom sem exigir um toque longo nem outra ação preliminar. A documentação da Apple sobre gestos simultâneos descreve essa combinação.

Escolha o significado de pan antes de escrever o código. Aqui, significa arrastar mantendo pressionado o botão principal do mouse, inclusive arrastar clicando no trackpad. Deslizar dois dedos pelo trackpad normalmente produz entrada de rolagem. O guia arquivado da Apple sobre eventos distingue explicitamente esse fluxo de rolagem dos gestos de ampliação e rotação; a documentação atual do AppKit continua oferecendo a rolagem por meio de scrollWheel(with:). Adicionar um gesto de arrastar não converte a rolagem em arrasto. Como lidar com eventos do trackpad, scrollWheel(with:)

Use esta tabela de decisão para escolher a camada de entrada da sua tela Canvas:

Interação desejada Ponto de partida Limite a preservar
Pan com clique e arrasto, zoom com pinça, rotação pelo trackpad O exemplo em SwiftUI abaixo A composição permite sobreposição entre fluxos de gestos reconhecidos.
Rolar com dois dedos para deslocar Uma tela Canvas do AppKit incorporada ao SwiftUI A rolagem continua sendo um fluxo de eventos de rolagem.
Uma transformação de três componentes garantida por um único padrão de dedos Uma interação personalizada especificada separadamente A composição simultânea, por si só, não garante que o hardware ou o macOS emitirá todos os componentes.

O exemplo requer macOS 14 ou posterior, versão na qual MagnifyGesture e RotateGesture estão disponíveis. Ele usa os inicializadores básicos, em vez das APIs mais recentes de configuração do tipo de entrada. MagnifyGesture, RotateGesture

Como organizar o estado das transformações?

Dê a cada componente um valor confirmado e um valor do gesto em andamento. A translação e a rotação são combinadas por adição; a escala, por multiplicação. As equações a seguir definem o comportamento do exemplo, e não uma exigência do SwiftUI:

translação exibida = translação confirmada + translação do arrasto em andamento
rotação exibida    = rotação confirmada + rotação em andamento
escala exibida     = clamp(escala confirmada × ampliação em andamento)

Use @GestureState para os valores em andamento. A Apple documenta que ele é redefinido quando um gesto fica inativo. Confirme cada componente por meio de seu próprio callback onEnded, que só é executado após a conclusão bem-sucedida. Assim, o cancelamento tem um significado claro: descartar a prévia inacabada daquele componente e manter o valor anteriormente confirmado. GestureState, onEnded(_:)

Mantenha esses callbacks nos gestos individuais, antes de compô-los. Assim, cada callback recebe seu próprio valor tipado e altera somente a propriedade sob sua responsabilidade. Não há um instantâneo compartilhado da transformação inicial que um gesto iniciado depois possa sobrescrever.

A translação do arrasto exige atenção especial: seu valor representa o movimento total desde o início do gesto. Somar esse total ao deslocamento confirmado a cada atualização contabilizaria repetidamente o movimento anterior. Atribua o total à prévia em andamento e, ao concluir, some o total final uma única vez. DragGesture.Value.translation

Crie o exemplo completo em SwiftUI

Crie um app SwiftUI para macOS com destino de implantação macOS 14 ou posterior. Substitua o ponto de entrada do app e a visualização de conteúdo gerados pelo arquivo único a seguir; mantenha exatamente uma declaração @main no destino. Não são necessários recursos, pacotes nem dados externos.

A tela Canvas cinza é responsável pelo gesto. A forma filha recebe as transformações visuais, portanto, mover ou girar a forma não transforma a superfície de entrada do gesto. O exemplo amplia e gira deliberadamente a forma em torno do centro dela.

import SwiftUI

@main
struct TransformCanvasApp: App {
    var body: some Scene {
        WindowGroup {
            TransformCanvas()
                .frame(minWidth: 400, minHeight: 300)
        }
        .defaultSize(width: 720, height: 500)
    }
}

struct TransformCanvas: View {
    @State private var offset = CGSize.zero
    @State private var angle = Angle.zero
    @State private var scale: CGFloat = 1

    @GestureState private var liveOffset = CGSize.zero
    @GestureState private var liveAngle = Angle.zero
    @GestureState private var liveScale: CGFloat = 1

    private func boundedScale(_ value: CGFloat) -> CGFloat {
        min(max(value, 0.25), 4)
    }

    private var pan: some Gesture {
        DragGesture(minimumDistance: 3, coordinateSpace: .local)
            .updating($liveOffset) { value, state, _ in
                state = value.translation
            }
            .onEnded { value in
                offset = CGSize(
                    width: offset.width + value.translation.width,
                    height: offset.height + value.translation.height
                )
            }
    }

    private var rotation: some Gesture {
        RotateGesture()
            .updating($liveAngle) { value, state, _ in
                state = value.rotation
            }
            .onEnded { value in
                angle = angle + value.rotation
            }
    }

    private var zoom: some Gesture {
        MagnifyGesture()
            .updating($liveScale) { value, state, _ in
                state = value.magnification
            }
            .onEnded { value in
                scale = boundedScale(scale * value.magnification)
            }
    }

    private var manipulation: some Gesture {
        pan.simultaneously(with: rotation)
            .simultaneously(with: zoom)
    }

    var body: some View {
        ZStack {
            Color.gray.opacity(0.12)

            RoundedRectangle(cornerRadius: 18)
                .fill(Color.blue)
                .overlay(alignment: .topLeading) {
                    Circle()
                        .fill(Color.white)
                        .frame(width: 24, height: 24)
                        .padding(16)
                }
                .frame(width: 180, height: 120)
                .scaleEffect(
                    boundedScale(scale * liveScale),
                    anchor: .center
                )
                .rotationEffect(angle + liveAngle, anchor: .center)
                .offset(CGSize(
                    width: offset.width + liveOffset.width,
                    height: offset.height + liveOffset.height
                ))
                .allowsHitTesting(false)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .contentShape(Rectangle())
        .clipped()
        .gesture(manipulation)
    }
}

A Apple documenta os tipos de gesto subjacentes de arrasto, ampliação e rotação, além de como anexar gestos às visualizações. O exemplo combina essas APIs com uma política explícita de estado e coordenadas. O SwiftUI não estava disponível no ambiente de pesquisa, então o programa foi conferido com base na documentação, mas não foi compilado nem executado aqui. DragGesture, MagnifyGesture, RotateGesture

Por que aplicar escala, depois rotação e, por fim, deslocamento?

O mapeamento pretendido é ampliar a forma em torno do centro dela, girá-la em torno desse mesmo centro e, por fim, transladá-la nas coordenadas da tela Canvas. Aplicar o deslocamento por último mantém o pan fora da escala e da rotação da forma. Mover o ponteiro na horizontal deve deslocar a forma horizontalmente, mesmo depois de ela ter girado.

Essa ordem é uma escolha de projeto derivada do pipeline de transformações do exemplo. A Apple documenta que scaleEffect escala a saída renderizada em torno de uma âncora, rotationEffect gira a saída renderizada e offset desloca o conteúdo exibido. Esses efeitos visuais não transformam a tela Canvas fixa em um contêiner de layout transformado. scaleEffect(_:anchor:), rotationEffect(_:anchor:), offset(_:)

Os limites de 0.25 e 4 são configurações ilustrativas do app. O mesmo clamp é aplicado durante a prévia e a confirmação, então, ao soltar o gesto, a escala exibida é preservada. min e max do Swift selecionam os valores mínimo e máximo usados para expressar esses limites. min(::), max(::)

Confira a interação com expectativas concretas

Comece com cada componente separadamente e depois tente sobrepô-los usando os dispositivos disponíveis. O círculo branco torna a rotação visível mesmo quando o retângulo é pequeno.

  1. Arraste clicando na tela Canvas, solte e arraste novamente. O segundo arrasto deve começar na posição confirmada pelo primeiro.
  2. Faça uma pinça para ampliar a forma, solte e faça outra pinça. Cada gesto deve multiplicar a escala mantida.
  3. Gire, solte e gire novamente. O ângulo deve se acumular.
  4. Depois de ampliar e girar, arraste horizontalmente. A forma ainda deve se deslocar ao longo do eixo horizontal da tela Canvas.
  5. Quando os dispositivos enviarem entradas sobrepostas, encerre um componente enquanto mantém outro em andamento. O componente restante deve continuar atualizando sua própria propriedade.
  6. Interrompa um gesto ativo, por exemplo, alternando de app. Qualquer gesto que fique inativo sem concluir com sucesso deve perder a prévia em andamento, em vez de deixar um estado temporário preso.

Como exemplo de conferência aritmética, suponha que a escala mantida seja 1.5 e uma pinça informe 1.2. O resultado exibido será 1.8. Se o ângulo mantido for 30 graus e a rotação em andamento for 15 graus, o ângulo exibido será 45 graus. Uma translação de arrasto de 40 pontos continua sendo um deslocamento de 40 pontos nas coordenadas da tela Canvas, independentemente dessa escala e desse ângulo.

Este exemplo confirma os componentes de maneira independente. Se a rotação for concluída com sucesso e um arrasto ainda ativo for cancelado depois, a rotação concluída será mantida, enquanto o arrasto inacabado será revertido. Um editor que exija uma operação atômica em todos os componentes precisa de um modelo adicional de sessão de interação, com uma política explícita de confirmação e reversão.

A forma também pode ser arrastada para fora da área visível da tela Canvas, pois a translação não tem limites de propósito. A tela Canvas continua aceitando arrastos, permitindo mover a forma de volta na direção oposta. Um visualizador completo pode adicionar um comando separado para redefinir a posição ou restringir a translação de acordo com os limites do próprio conteúdo.

Quando usar uma tela Canvas do AppKit?

Escolha uma camada de entrada do AppKit quando a tarefa exigir especificamente pan com rolagem de dois dedos ou controle direto do impulso de rolagem. Incorpore uma visualização AppKit própria por meio de NSViewRepresentable, que a Apple fornece para criar, atualizar e desmontar visualizações AppKit dentro do SwiftUI. NSViewRepresentable

Mantenha um único responsável por cada fluxo de entrada. Nessa tela Canvas do AppKit, trate a rolagem por meio de scrollWheel(with:) e use reconhecedores para ampliação e rotação. Encaminhe as alterações resultantes para o mesmo modelo de translação, ângulo e escala. Evite anexar outro manipulador SwiftUI que também aplique a mesma entrada a esse modelo.

Para permitir o funcionamento simultâneo dos reconhecedores do AppKit, implemente gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) e retorne true somente para os pares de reconhecedores previstos que pertencem à sua tela Canvas. A Apple documenta que o método delegado permite o reconhecimento simultâneo; ele não cria um fluxo de entrada ausente. Método delegado de reconhecimento simultâneo

Preserve as convenções dos valores ao fazer a ponte. O reconhecedor de ampliação do AppKit informa um valor que deve ser somado a 1 para obter um fator de escala, enquanto o reconhecedor de rotação expõe radianos. Não copie as atribuições de valores do SwiftUI sem alterações. NSMagnificationGestureRecognizer.magnification, NSRotationGestureRecognizer.rotation

Para a rolagem, a Apple indica scrollingDeltaX como a propriedade preferencial para o delta horizontal e observa que talvez seja necessário ajustar deltas não precisos. A documentação sobre impulso descreve eventos de rolagem que continuam após o fim do gesto físico. Decida se a tela Canvas aceita essa continuação e encaminhe cada evento aceito uma única vez. scrollingDeltaX, momentumPhase

A ponte deve alterar a transformação do conteúdo da tela Canvas, enquanto o SwiftUI continua responsável pelo layout da visualização representada. A Apple alerta explicitamente contra definir diretamente o frame ou os limites da visualização representada. Use o coordenador do representable para encaminhar ao SwiftUI as alterações de delegado e de target-action. NSViewRepresentable

Para o visualizador com clique e arrasto, a implementação em SwiftUI fornece um ponto de partida completo: uma superfície de entrada fixa, três componentes de gesto gerenciados de forma independente e uma ordem deliberada das transformações. Preserve esses limites ao expandi-la e escolha o caminho do AppKit quando a tarefa voltada ao usuário exigir pan por rolagem.

Fontes