Что такое Canvas в SwiftUI и когда его стоит использовать?
Canvas — SwiftUI-вид с immediate-mode растровым контекстом (GraphicsContext), который обходит дерево View и идеально подходит для сотен примитивов, покадровых анимаций и кастомных графиков; доступность и hit-testing нужно реализовывать вручную.
Что такое Canvas
Canvas — это низкоуровневый SwiftUI-вид, появившийся в iOS 15 / macOS 12, который предоставляет немедленный (immediate-mode) растровый контекст для рисования через GraphicsContext. В отличие от обычных SwiftUI-видов, Canvas не создаёт дерево дочерних View-значений и не участвует в механизме инвалидации body — всё содержимое описывается одной замыканием-рисовальщиком, которое вызывается при каждом обновлении кадра.
Когда стоит использовать
- Нужно отрисовать сотни или тысячи примитивов (частицы, графики, тайловые карты) — каждый отдельный SwiftUI-вид создаёт узел в render tree, тогда как
Canvasсводится к одному CALayer. - Требуется покадровая анимация (через
TimelineView+Canvas): замыкание переполучает свежийGraphicsContextиDateна каждый тик без пересоздания поддерева. - Рисование зависит от вычислений в реальном времени (аудиовизуализатор, симуляция физики), где декларативный layout SwiftUI был бы избыточным.
- Простые кастомные фигуры с изменяемой геометрией, которые неудобно описывать через
Shape.
Механизм работы
Canvas принимает два замыкания: renderer и необязательный symbols. Блок symbols позволяет встраивать произвольные SwiftUI-виды как именованные символы (ResolvedSymbol), которые затем рисуются через context.draw(_:at:). Сам GraphicsContext — struct-значение, копируется при передаче в withCGContext, что позволяет применять трансформации и фильтры локально без побочных эффектов.
Поскольку Canvas не порождает дочерних View, доступность (Accessibility) нужно добавлять вручную через модификатор .accessibilityElement или блок symbols.
Пример: анимированный аудиовизуализатор
import SwiftUI
struct AudioVisualizerView: View {
let amplitudes: [Float] // массив значений 0.0–1.0
var body: some View {
TimelineView(.animation) { timeline in
Canvas { context, size in
let barWidth: CGFloat = size.width / CGFloat(amplitudes.count)
for (index, amplitude) in amplitudes.enumerated() {
let barHeight = size.height * CGFloat(amplitude)
let rect = CGRect(
x: CGFloat(index) * barWidth,
y: size.height - barHeight,
width: barWidth - 2,
height: barHeight
)
var path = Path()
path.addRoundedRect(
in: rect,
cornerSize: CGSize(width: 3, height: 3)
)
context.fill(
path,
with: .linearGradient(
Gradient(colors: [.cyan, .blue]),
startPoint: CGPoint(x: rect.midX, y: rect.minY),
endPoint: CGPoint(x: rect.midX, y: rect.maxY)
)
)
}
}
.drawingGroup() // растеризует в отдельный Metal-слой
}
.accessibilityLabel("Аудиовизуализатор")
.accessibilityHidden(true)
}
}
// Использование
struct ContentView: View {
@State private var amps: [Float] = (0..<32).map { _ in Float.random(in: 0...1) }
var body: some View {
AudioVisualizerView(amplitudes: amps)
.frame(height: 120)
.task {
// обновляем каждые 50 мс на фоновом акторе
while !Task.isCancelled {
try? await Task.sleep(for: .milliseconds(50))
await MainActor.run {
amps = (0..<32).map { _ in Float.random(in: 0...1) }
}
}
}
}
}
Сравнение с альтернативами
- Shape / Path — встраиваются в SwiftUI-дерево, анимируются через
withAnimation; подходят для статичных или умеренно сложных контуров. - drawingGroup() — кеширует поддерево SwiftUI в растр; комбинируется с
Canvasдля дополнительного GPU-ускорения. - CALayer / Metal / SpriteKit — оправданы при экстремальной нагрузке (60k+ примитивов) или когда нужен прямой доступ к GPU pipeline.
Подводные камни
- Нет автоматической доступности.
Canvasполностью непрозрачен для VoiceOver: любые интерактивные или информативные элементы нужно добавлять черезsymbolsили отдельные.accessibilityElement-оверлеи. - Трансформации применяются к копии контекста. Если изменить
context.transformбезcontext.withCGContext { ... }илиdefer { context.transform = ... }, все последующие примитивы окажутся в неверной системе координат. - symbols не поддерживают жесты.
ResolvedSymbol, встроенный черезsymbols, теряет интерактивность — нажатия на нарисованный символ не срабатывают. Для кликабельных элементов нужен ZStack с отдельным View-слоем. - Производительность на стороне CPU, если нет drawingGroup(). Без явной растеризации
Canvasвыполняет рисование на CPU через Core Graphics; при большом числе примитивов добавьте.drawingGroup()для GPU-ускорения. - TimelineView пересоздаёт контекст каждый кадр. Любые тяжёлые вычисления внутри замыкания
rendererблокируют main thread — выносите физику или FFT вTask { await actor.compute() }. - GraphicsContext не thread-safe. Замыкание всегда вызывается на главном потоке; передавать
GraphicsContextв другой поток — undefined behaviour. - Отсутствие hit-testing.
Canvasне участвует в стандартной цепочке hit-test SwiftUI. Если нужна реакция на тапы в конкретную область, реализуйте геометрическую проверку вручную в.onTapGesture { location in ... }. - Неявное масштабирование при .drawingGroup(). Модификатор использует текущий
contentScaleFactorэкрана, но при смене размера контейнера растровый буфер пересоздаётся — при частых изменениях размера это может вызвать мерцание.
Common mistakes
- Сводить «
Canvasв SwiftUI и когда его стоит использовать» к синтаксису и не объяснять инвалидация body. - Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swiftui-21.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «
Canvasв SwiftUI и когда его стоит использовать» и подтверждает ее корректным примером. - Умеет связать ответ с navigation state, тестированием и отладкой на устройстве.
- Называет ограничения подхода swiftui-21, включая производительность, память и сопровождение.