Como melhorar a performance de drag-and-drop em apps macOS

O problema
Durante operações de drag-and-drop, o sistema precisa atualizar a interface com frequência: destacar alvos de drop, animar elementos, redesenhar células.
Se suas views customizadas usam draw(_ dirtyRect:) para desenhar backgrounds, bordas ou separadores, cada atualização visual dispara uma re-execução desse método na CPU.
O resultado: uso elevado de CPU e possível stuttering durante o drag.
Por que isso acontece
O método draw() é processado na CPU. Cada chamada a needsDisplay = true força o sistema a re-executar todo o código de desenho. Durante drag-and-drop, isso pode acontecer dezenas de vezes por segundo.
// Cada needsDisplay = true re-executa este código na CPU
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Desenho do background
NSColor.quaternaryLabelColor.withAlphaComponent(0.08).setFill()
NSBezierPath(roundedRect: bounds, xRadius: 8, yRadius: 8).fill()
// Desenho do separador
NSColor.separatorColor.setStroke()
let path = NSBezierPath()
path.move(to: NSPoint(x: 12, y: bounds.height - 12))
path.line(to: NSPoint(x: bounds.width - 12, y: bounds.height - 12))
path.lineWidth = 1
path.stroke()
}
Multiplique isso por 42 células em um calendário mensal e você tem um problema.
A solução: CALayer
Propriedades de CALayer são gerenciadas pela GPU via Core Animation. O sistema faz composição, batching e otimizações automáticas. A CPU fica livre para outras tarefas.
init(...) {
super.init(frame: .zero)
// Habilita layer-backing
wantsLayer = true
// Background via layer (GPU)
layer?.backgroundColor = NSColor.quaternaryLabelColor
.withAlphaComponent(0.08).cgColor
// Corner radius via layer (GPU)
layer?.cornerRadius = 8
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Agora só desenha o que realmente precisa de desenho customizado
}
O que você pode mover para CALayer
+--------------------------------+----------------------------------+
| Antes (CPU) | Depois (GPU) |
+--------------------------------+----------------------------------+
| NSBezierPath.fill() com cor | layer?.backgroundColor |
| NSBezierPath(roundedRect:) | layer?.cornerRadius |
| Desenho de bordas | layer?.borderWidth/borderColor |
| Sombras manuais | layer?.shadow* |
+--------------------------------+----------------------------------+
Para arredondar apenas alguns cantos, use layer?.maskedCorners:
// Arredonda apenas o canto superior esquerdo
layer?.cornerRadius = 8
layer?.maskedCorners = [.layerMinXMaxYCorner]
Quando manter o draw()
Reserve draw() para formas verdadeiramente customizadas que não podem ser expressas como propriedades de layer:
- Gráficos vetoriais complexos
- Gradientes não-lineares
- Paths com curvas bezier customizadas
- Desenhos que dependem de dados dinâmicos
Resultado prático
Em um app de calendário com 42 células (6 semanas x 7 dias), a mudança de draw() para CALayer reduziu visivelmente o uso de CPU durante drag-and-drop. O Instruments confirmou: menos tempo em rendering, mais tempo ocioso.
A regra é simples: se você consegue expressar algo como propriedade de layer, faça isso.
Referências
- CALayer - Apple Developer Documentation
- Core Animation Programming Guide (Archive) - documentação histórica, útil para entender princípios arquiteturais que permanecem válidos
- wantsLayer - NSView
Posts Relacionados
CoreData: debug: WAL checkpoint - O que significa esse log?
Explicação técnica sobre os logs 'CoreData: debug: WAL checkpoint: Database did checkpoint' que aparecem durante operações com CoreData e SwiftData no iOS e macOS.
Drag and Drop no macOS: Como Evitar Drop Zones Competindo Entre Si
Guia completo sobre implementação de drag and drop em macOS com AppKit. Aborda o problema de drop zones competindo, centralização de drop handling, coordenadas non-flipped do NSView, e como mostrar drop indicators entre itens.
Como remover o Liquid Glass no iOS 26 (SwiftUI e UIKit)
Guia prático para remover o fundo circular de vidro dos botões de navegação no iOS 26, com exemplos em SwiftUI e UIKit