Mantenha responsivo o desenho rápido de retângulos separando a prévia do arrasto ativo das formas já adicionadas à tela. Em um app de desenho para Mac, deixe uma única superfície de gesto controlar a entrada, mantenha a geometria temporária local e adicione-a à coleção de formas somente quando o arrasto terminar com sucesso.

Este guia se destina a quem está desenvolvendo uma ferramenta simples de desenho de retângulos com clique e arraste em seu próprio app para macOS. Todas as formas de exemplo ficam na memória. O objetivo é impedir que o movimento do ponteiro altere repetidamente todo o modelo do desenho e, em seguida, usar a criação de perfis para identificar quaisquer custos de renderização restantes.

Por que o desenho fica mais lento à medida que as formas adicionadas se acumulam?

Comece distinguindo três operações: receber entrada, atualizar o estado e renderizar a geometria. Um arrasto pode alterar a geometria temporária com frequência. Se essas alterações também substituírem a coleção já adicionada, reconstruírem todos os caminhos ou invalidarem uma hierarquia ampla de visualizações, o trabalho associado a cada movimento pode crescer junto com o desenho.

A Apple explica que o SwiftUI acompanha dependências e atualiza as visualizações quando essas dependências mudam. Tanto atualizações lentas de visualizações individuais quanto muitas atualizações desnecessárias podem fazer com que os prazos dos quadros não sejam cumpridos. Separar o estado da prévia, que muda com frequência, do conteúdo já adicionado é uma aplicação arquitetural desse modelo documentado. Otimize o desempenho do SwiftUI com o Instruments

Canvas oferece desenho em modo imediato por meio de um contexto gráfico. A Apple o aponta como uma possível melhoria de desempenho para desenhos dinâmicos complexos, mas ele não oferece interação nem acessibilidade para cada elemento desenhado. Associe o tratamento de entrada a uma superfície de desenho definida intencionalmente; se o app precisar futuramente de formas operáveis individualmente, implemente também o suporte à seleção e à acessibilidade. Canvas

O limite útil, portanto, é explícito: a superfície de entrada controla o gesto, o estado temporário controla a prévia e a visualização pai controla os retângulos adicionados. Dividir essas responsabilidades reduz dependências evitáveis. Isso não garante que o sistema nunca redesenhe a tela já adicionada.

Separe a prévia do desenho já adicionado

Use este plano de responsabilidades antes de escolher otimizações de renderização:

Responsabilidade Responsável Quando muda
Retângulo do arrasto atual Superfície de gesto Durante o arrasto ativo
Coleção de retângulos adicionados Visualização pai do desenho Uma vez por arrasto concluído com sucesso
Desenho dos retângulos adicionados Canvas de fundo Quando suas entradas exigem uma atualização
Reconhecimento do ponteiro Superfície de gesto Durante toda a sequência do gesto

Para a geometria temporária, @GestureState é útil porque o SwiftUI a reinicia quando o gesto fica inativo. Confirme as alterações por meio de onEnded, cujo callback recebe o valor final do gesto. A Apple especifica que esse callback é executado somente quando o gesto termina com sucesso; o estado do gesto cuida da limpeza independentemente de como ele termina. GestureState, onEnded(_:)

Para retângulos, a geometria final exige apenas os locais inicial e atual. Não é necessário guardar cada posição intermediária do ponteiro. A Apple documenta essas posições separadamente dos pontos finais previstos; use o local final real ao confirmar o retângulo. DragGesture.Value

Crie uma ferramenta de retângulos completa que funciona em memória

Os dois blocos de código a seguir formam um único arquivo Swift completo. Em um projeto App do SwiftUI para macOS com destino macOS 12 ou posterior, substitua o arquivo de entrada do app gerado pelos dois blocos e remova qualquer outra declaração de entrada do app. Canvas está disponível a partir do macOS 12. Este exemplo foi inspecionado, mas não pôde ser compilado aqui porque o ambiente não tem uma cadeia de ferramentas Swift. Canvas

O primeiro bloco define o app, a coleção de formas adicionadas e o renderizador. O pai recebe apenas os retângulos concluídos; ele não lê o estado do arrasto ativo.

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
                )
            }
        }
    }
}

Acrescente o bloco a seguir ao mesmo arquivo. A superfície transparente cobre a área de desenho, define uma forma retangular explícita para o teste de acerto e controla o único gesto de desenho. A Apple documenta contentShape como a forma que define o teste de acerto e allowsHitTesting(false) como a forma de excluir uma visualização do teste de acerto. 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 elimina a exigência de deslocamento mínimo para o reconhecimento. O espaço de coordenadas local mantém as posições do gesto alinhadas com a tela de prévia, que tem o mesmo tamanho. Estas são opções documentadas de configuração de gesto, não uma garantia sobre a frequência dos eventos ou a latência da tela. DragGesture

O auxiliar de retângulo normaliza as quatro direções de arrasto. Como verificação ilustrativa, arrastar de (90, 80) até (30, 20) produz uma origem de (30, 20) e um tamanho de (60, 60). A prévia e a confirmação usam o mesmo cálculo, portanto soltar o ponteiro não altera a fórmula da geometria.

Esta ferramenta mínima também adiciona retângulos com largura ou altura zero. Assim, um clique pode incrementar a contagem sem produzir um retângulo visível. Se o produto exigir um tamanho mínimo, adicione essa regra explicitamente no momento da confirmação e forneça o feedback apropriado ao usuário. O exemplo mantém todos os gestos concluídos com sucesso, sem descartar silenciosamente os resultados pequenos.

A área visível é recortada, mas a geometria armazenada permanece inalterada. Portanto, um retângulo que ultrapasse os limites da prancheta pode conter coordenadas fora dela. Decida se um redimensionamento futuro deve revelar essa geometria antes de adicionar um limite às coordenadas.

Diagnostique o custo restante antes de adicionar um cache

O exemplo remove as mutações do modelo já adicionado do caminho de atualização do arrasto. O renderizador do conteúdo adicionado ainda percorre a coleção sempre que é executado. Diferencie a redução da propagação de estado da redução do trabalho de renderização: uma visualização separada não é um cache persistente de imagens documentado.

Use uma comparação controlada para descobrir qual custo permanece. Escolha quantidades ilustrativas de formas na memória, como 0, 500 e 5.000 retângulos. Mantenha consistentes as dimensões dos retângulos, o tamanho da janela, a escala da tela e a configuração de compilação. Essas quantidades são entradas de teste, não limites de capacidade nem resultados medidos.

Registre três intervalos separadamente: arrastar sem alterar a coleção, soltar para confirmar e redimensionar a janela. Uma lentidão contínua durante o arrasto sugere um tipo de trabalho diferente de uma pausa que ocorre somente na confirmação. O comportamento ao redimensionar pode revelar reconstruções de geometria ou de cache que não ocorrem durante o desenho normal.

A sessão de criação de perfis do SwiftUI da Apple descreve as ferramentas Long View Body Updates, Long Representable Updates e Cause & Effect Graph, além do Time Profiler. Use o grafo de dependências para investigar por que as visualizações do conteúdo adicionado são atualizadas durante um arrasto; use as amostras de CPU para encontrar construções de geometria ou trabalhos de renderização dispendiosos. A Apple apresentou esse fluxo de trabalho com instrumentos no Instruments 26. Otimize o desempenho do SwiftUI com o Instruments

Para travamentos mais amplos, a sessão WWDC26 da Apple sobre responsividade começa pelo Time Profiler e distingue a execução ativa do tempo de espera. Faça essa distinção antes de transferir trabalho entre executores ou substituir a pilha de renderização. Crie perfis, corrija e verifique: melhore a responsividade do app com o Instruments

A tabela de decisão a seguir é uma inferência de engenharia baseada nessas ferramentas de criação de perfis e no modelo de responsabilidades acima:

Comportamento observado Investigue primeiro Alteração possível
O estado do desenho pai muda durante o movimento do ponteiro Callbacks de atualização do gesto e estado compartilhado Mantenha o estado da prévia dentro da superfície de gesto
O desenho adicionado domina as amostras durante o movimento Percurso repetido e criação de caminhos Pré-calcule a geometria ou introduza um cache para o conteúdo adicionado
As pausas ao soltar ficam mais longas à medida que a coleção cresce Mutação do array e reconstrução completa do desenho Reduza as cópias completas retidas e reconstrua somente o conteúdo derivado afetado
O desenho de retângulos está responsivo, mas traços longos à mão livre ficam lentos Tamanho do caminho ativo e reconstrução Crie um perfil do traço ativo crescente separadamente
As pausas ocorrem principalmente durante o redimensionamento Renderização dependente do tamanho e invalidação do cache Defina explicitamente o comportamento do conteúdo derivado ao redimensionar

Um cache adiciona outro limite de responsabilidade: a geometria dos retângulos continua sendo a fonte de verdade, e o cache é conteúdo derivado que pode ser substituído. Inclua o tamanho do desenho, a escala da tela e os estilos relevantes no planejamento da invalidação. Para desenhos em que só há adições, atualizações incrementais do cache são uma otimização possível; edições e remoções exigem uma estratégia clara de reconstrução ou invalidação regional. Verifique o resultado com as mesmas quantidades de formas de entrada antes de manter a complexidade adicional.

Verifique a cópia do array no limite de confirmação

Os arrays do Swift têm semântica de valor e usam armazenamento com cópia na escrita. As cópias podem compartilhar armazenamento até que uma mutação exija armazenamento independente; arrays que contêm instâncias de classes ainda compartilham referências a essas instâncias. Não se deve dizer que passar um array para uma visualização ou acrescentar um elemento a um array sempre copia todos os elementos. Array

Para esta ferramenta, evite armazenar uma nova cópia completa do desenho a cada atualização do ponteiro. Se a criação de perfis identificar cópias durante a confirmação, examine quais cópias ainda estão vivas e como o modelo do desenho é compartilhado. Substituir tipos de valor por objetos mutáveis compartilhados apenas para evitar cópias altera a semântica de propriedade e exige uma decisão de projeto separada.

Considere a apresentação assíncrona como uma opção a avaliar

A Apple define rendersAsynchronously como uma opção que permite ao canvas apresentar seu conteúdo ao pai de forma assíncrona. Essa descrição não promete uma entrega mais rápida de gestos, não transfere o manipulador de gestos para um executor em segundo plano nem elimina o percurso da coleção. Avalie-a separadamente depois de estabelecer os limites da entrada e do estado. rendersAsynchronously

Quando o AppKit deve controlar a superfície de desenho?

Use uma superfície AppKit personalizada quando a tarefa exigir o tratamento direto por métodos do responder ou a invalidação explícita de regiões de desenho. A Apple documenta NSView como a infraestrutura para desenhar e tratar eventos, e permite criar subclasses para comportamentos personalizados. Essa é uma alternativa compatível, com suas próprias responsabilidades de renderização. NSView

Em uma implementação AppKit, preserve a mesma sequência: inicialize a geometria temporária em mouseDown(with:), atualize-a em mouseDragged(with:) e confirme-a em mouseUp(with:). Converta event.locationInWindow para as coordenadas da visualização que recebe os eventos antes de calcular a geometria. O guia arquivado da Apple sobre eventos do mouse documenta essa sequência e a conversão de coordenadas; a referência atual de NSView documenta os métodos do responder compatíveis. Como tratar eventos do mouse, NSView

Solicite a exibição definindo needsDisplay = true; a Apple especifica que a visualização será redesenhada no próximo ciclo do loop de eventos. Em um projeto baseado em regiões, invalide a união dos limites antigos e novos da prévia, expandida para incluir o traço. A invalidação regional só reduz o percurso se a implementação do desenho também evitar o processamento do conteúdo não afetado. needsDisplay, Como tratar eventos do mouse

Incorpore a superfície com NSViewRepresentable: crie-a em makeNSView, reconcilie o estado externo em updateNSView e use um coordenador para comunicar as alterações de volta ao SwiftUI. Atualize o encaminhamento de callbacks durante as atualizações e evite reiniciar um rascunho ativo só porque o SwiftUI forneceu outro valor de representável. A Apple também afirma que o SwiftUI controla o layout da visualização representada; não sobrescreva diretamente o frame ou os limites gerenciados por ele. NSViewRepresentable

Escolha um único responsável pela entrada do desenho. Se o AppKit tratar a sequência de desenho, não confirme essa mesma sequência também por meio de um gesto de arrasto sobreposto do SwiftUI. Para uma subclasse direta de NSView, a Apple instrui as implementações que tratam os eventos de mouse listados a não chamar a implementação da superclasse para esses eventos tratados. NSView

Mantenha a primeira melhoria pequena e observável: estado de prévia local, um único limite de confirmação e um renderizador do conteúdo adicionado cujas entradas não mudam durante o movimento. Em seguida, compare desenhos sintéticos crescentes no Instruments. O traço resultante indica se a próxima mudança útil deve ser feita no gerenciamento de dependências, na propriedade da coleção, na preparação da geometria ou na renderização.

Fontes