Сохраняйте отзывчивость при быстром рисовании прямоугольников, отделяя предпросмотр активного перетаскивания от уже добавленных фигур на холсте. В приложении для Mac назначьте одну поверхность обработчиком ввода, храните временную геометрию локально и добавляйте фигуру в коллекцию только после успешного завершения перетаскивания.

Это руководство предназначено для разработчика, который создаёт простой инструмент рисования прямоугольников щелчком и перетаскиванием в собственном приложении для macOS. Все примеры фигур хранятся в памяти. Цель — не допустить, чтобы перемещение указателя многократно изменяло всю модель рисунка, а затем с помощью профилирования выявить оставшиеся затраты на отрисовку.

Почему рисование замедляется по мере накопления добавленных фигур?

Для начала разделите три операции: получение ввода, обновление состояния и отрисовку геометрии. Во время перетаскивания временная геометрия может часто меняться. Если эти изменения также заменяют коллекцию добавленных фигур, заново строят каждый контур или делают недействительной широкую иерархию представлений, объём работы при каждом перемещении может расти вместе с рисунком.

Apple объясняет, что SwiftUI отслеживает зависимости и обновляет представления при изменении этих зависимостей. И медленные обновления отдельных представлений, и многочисленные ненужные обновления могут привести к пропуску сроков показа кадров. Отделение часто меняющегося состояния предпросмотра от добавленного содержимого — архитектурное применение описанной модели. Оптимизация производительности SwiftUI с помощью Instruments

Canvas обеспечивает рисование в непосредственном режиме с помощью графического контекста. Apple указывает, что этот компонент может повысить производительность при создании сложных динамических рисунков, но не обеспечивает взаимодействие и доступность для каждого нарисованного элемента. Обрабатывайте ввод на специально выбранной поверхности рисования; если позднее приложению потребуется управлять отдельными фигурами, добавьте для них поддержку выбора и доступности. Canvas

Таким образом, границы ответственности должны быть явными: поверхность ввода обрабатывает жест, временное состояние хранит предпросмотр, а родительское представление хранит добавленные прямоугольники. Разделение этих обязанностей сокращает число лишних зависимостей. Оно не гарантирует, что система никогда не перерисует холст с добавленными фигурами.

Отделите предпросмотр от добавленного рисунка

Выберите такую схему владения до того, как решать, какие оптимизации отрисовки применять:

Ответственность Владелец Когда меняется
Текущий прямоугольник при перетаскивании Поверхность обработки жеста Во время активного перетаскивания
Коллекция добавленных прямоугольников Родительское представление рисунка Один раз после успешного перетаскивания
Отрисовка добавленных прямоугольников Фоновый холст Когда его входные данные требуют обновления
Распознавание указателя Поверхность обработки жеста На протяжении всей последовательности жеста

Для временной геометрии удобно использовать @GestureState: SwiftUI сбрасывает это состояние, когда жест становится неактивным. Добавляйте фигуру через onEnded; его обработчик получает итоговое значение жеста. Согласно документации Apple, этот обработчик вызывается только при успешном завершении жеста, а состояние жеста очищается независимо от того, как жест завершился. GestureState, onEnded(_:)

Чтобы задать итоговую геометрию прямоугольника, нужны только начальная и текущая точки. Сохранять все промежуточные положения указателя не требуется. Apple описывает эти положения отдельно от прогнозируемых конечных точек; при добавлении прямоугольника используйте фактическое конечное положение. DragGesture.Value

Создайте законченный инструмент рисования прямоугольников с хранением данных в памяти

Следующие два блока кода вместе составляют один полный файл Swift. В проекте macOS SwiftUI App с целевой версией macOS 12 или новее замените оба блока сгенерированный файл точки входа приложения и удалите все остальные объявления точки входа. Canvas доступен начиная с macOS 12. Этот пример был проверен, но здесь его не удалось скомпилировать, поскольку в среде нет набора инструментов Swift. Canvas

Первый блок определяет приложение, коллекцию добавленных фигур и средство их отрисовки. Родительское представление получает только завершённые прямоугольники и не обращается к состоянию активного перетаскивания.

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

Добавьте следующий блок в тот же файл. Прозрачная поверхность покрывает область рисования, задаёт явную прямоугольную форму для проверки попаданий и обрабатывает единственный жест рисования. Apple указывает, что contentShape задаёт форму для проверки попаданий, а allowsHitTesting(false) исключает представление из проверки попаданий. 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 отменяет минимальное расстояние, необходимое для распознавания жеста. Локальная система координат позволяет совместить положения жеста с холстом предпросмотра такого же размера. Это документированные параметры настройки жеста, а не гарантия частоты событий или задержки отображения. DragGesture

Вспомогательная функция для прямоугольника нормализует все четыре направления перетаскивания. Например, при перетаскивании из (90, 80) в (30, 20) начало прямоугольника будет равно (30, 20), а размер — (60, 60). Для предпросмотра и добавления используется один и тот же расчёт, поэтому отпускание указателя не меняет формулу геометрии.

Этот минимальный инструмент добавляет прямоугольники даже нулевой ширины и высоты. Поэтому щелчок может увеличить счётчик, не создав видимого прямоугольника. Если продукт требует минимального размера, явно добавьте это правило при сохранении и предусмотрите подходящую обратную связь для пользователя. В примере сохраняется каждый успешно завершённый жест, а маленькие фигуры не отфильтровываются незаметно.

Видимая область обрезается, но сохранённая геометрия остаётся неизменной. Поэтому прямоугольник, выходящий за пределы доски, может содержать координаты за её границами. Прежде чем добавлять ограничение координат, решите, должно ли будущее изменение размера показывать эту геометрию.

Прежде чем добавлять кэш, выясните, откуда берутся оставшиеся затраты

В примере мутации модели добавленных фигур исключены из пути обновления при перетаскивании. Однако средство их отрисовки по-прежнему проходит по коллекции при каждом выполнении. Различайте сокращение распространения изменений состояния и сокращение объёма работы по отрисовке: отдельное представление не является документированным постоянным кэшем изображения.

Проведите контролируемое сравнение, чтобы выяснить, какие затраты остались. Возьмите, например, 0, 500 и 5 000 прямоугольников в памяти. Сохраняйте одинаковыми размеры прямоугольников, размер окна, масштаб дисплея и конфигурацию сборки. Эти числа задают тестовые входные данные, а не пределы ёмкости и не результаты измерений.

Отдельно зафиксируйте три интервала: перетаскивание без изменений коллекции, отпускание указателя с сохранением фигуры и изменение размера окна. Замедление во время непрерывного перетаскивания указывает на другой вид работы, чем пауза, возникающая только при сохранении. Поведение при изменении размера может выявить перестроение геометрии или кэша, которое не проявляется при обычном рисовании.

В сессии Apple по профилированию SwiftUI описаны инструменты Long View Body Updates, Long Representable Updates и Cause & Effect Graph, а также Time Profiler. Используйте граф зависимостей, чтобы выяснить, почему представления добавленных фигур обновляются во время перетаскивания; с помощью сэмплов ЦП найдите дорогостоящее построение геометрии или отрисовку. Apple представила этот сценарий работы с инструментами в Instruments 26. Оптимизация производительности SwiftUI с помощью Instruments

Если возникают более общие задержки, начните с Time Profiler, как рекомендует сессия Apple WWDC26 об отзывчивости, и различайте загруженное выполнение и время ожидания. Учтите это различие, прежде чем переносить работу между исполнителями или заменять стек отрисовки. Профилируйте, исправляйте и проверяйте: повышайте отзывчивость приложений с помощью Instruments

Следующие рекомендации — инженерный вывод из описанных инструментов профилирования и приведённой выше схемы владения:

Наблюдаемое поведение Что проверить в первую очередь Возможное изменение
Состояние рисунка в родительском представлении меняется при перемещении указателя Обработчики обновления жеста и общее состояние Хранить состояние предпросмотра внутри поверхности обработки жеста
При перемещении указателя в сэмплах преобладает отрисовка добавленных фигур Повторный обход и создание контуров Предварительно вычислять геометрию или добавить кэш добавленного содержимого
Паузы при отпускании указателя становятся длиннее по мере роста коллекции Изменение массива и полная перестройка рисунка Сократить число сохраняемых снимков и перестраивать только затронутое производное содержимое
Прямоугольники рисуются отзывчиво, а длинные штрихи от руки — медленно Размер активного контура и его перестроение Отдельно профилировать растущий активный штрих
Паузы возникают в основном при изменении размера окна Отрисовка с учётом размера и сброс кэша Задать явное поведение производного содержимого при изменении размера

Кэш создаёт дополнительную границу владения: геометрия прямоугольников остаётся первоисточником, а кэш — заменяемым производным содержимым. При проектировании сброса кэша учитывайте размер рисунка, масштаб дисплея и значимые параметры стиля. Для рисунков, где фигуры только добавляются, можно рассмотреть инкрементальное обновление кэша; для редактирования и удаления нужна чёткая стратегия перестроения или сброса кэша по отдельным областям. Прежде чем оставлять дополнительную сложность, проверьте результат на тех же наборах входных данных.

Проверьте копирование массива при сохранении фигуры

Массивы Swift обладают семантикой значений и используют копирование при записи. Копии могут совместно использовать хранилище, пока для изменения не потребуется отдельное хранилище; при этом массивы, содержащие экземпляры классов, продолжают совместно использовать ссылки на эти экземпляры. Не следует утверждать, что передача массива представлению или добавление элемента в массив всегда копирует каждый элемент. Array

В этом инструменте не создавайте новый полный снимок рисунка при каждом обновлении указателя. Если профилирование выявит копирование при сохранении фигуры, проверьте, какие снимки остаются в памяти и как используется модель рисунка. Замена типов значений общими изменяемыми объектами только ради устранения копирования меняет семантику владения и требует отдельного проектного решения.

Рассматривайте асинхронное отображение как вариант, который нужно проверить измерениями

Apple определяет rendersAsynchronously как возможность асинхронно отображать содержимое холста в родительском представлении. В этом описании нет обещания ускорить обработку жестов, перенести обработчик жестов на фоновый исполнитель или устранить обход коллекции. Оцените эту возможность отдельно после того, как определите границы ввода и состояния. rendersAsynchronously

Когда поверхностью рисования должен управлять AppKit?

Используйте собственную поверхность AppKit, если задача требует непосредственной обработки методов responder или явной инвалидации областей рисования. Apple описывает NSView как инфраструктуру для рисования и обработки событий и поддерживает создание подклассов для собственного поведения. Это поддерживаемая альтернатива со своими обязанностями по отрисовке. NSView

В реализации на AppKit сохраните ту же последовательность: инициализируйте временную геометрию в mouseDown(with:), обновляйте её в mouseDragged(with:) и добавляйте фигуру в mouseUp(with:). Преобразуйте event.locationInWindow в систему координат принимающего представления, прежде чем вычислять геометрию. В архивном руководстве Apple по событиям мыши описаны эта последовательность и преобразование координат; текущая справка по NSView документирует поддерживаемые методы responder. Обработка событий мыши, NSView

Запрашивайте отображение, устанавливая needsDisplay = true; Apple указывает, что после этого представление перерисовывается при следующем проходе цикла событий. В дизайне с обновлением отдельных областей помечайте для перерисовки объединение старых и новых границ предпросмотра, расширив его с учётом толщины линии. Обновление отдельных областей сокращает обход только в том случае, если реализация рисования также пропускает нетронутое содержимое. needsDisplay, Обработка событий мыши

Встраивайте поверхность с помощью NSViewRepresentable: создавайте её в makeNSView, согласовывайте внешнее состояние в updateNSView и используйте координатор для передачи изменений обратно в SwiftUI. При обновлениях обновляйте маршрутизацию обработчиков и не сбрасывайте активный черновик только из-за того, что SwiftUI передал другое значение representable. Apple также указывает, что компоновкой представления управляет SwiftUI; не перезаписывайте напрямую его управляемые frame или bounds. NSViewRepresentable

Назначьте обработку ввода для рисования одному владельцу. Если последовательностью рисования управляет AppKit, не добавляйте и ту же фигуру через перекрывающийся жест перетаскивания SwiftUI. Для прямого подкласса NSView Apple предписывает не вызывать реализацию суперкласса для перечисленных событий мыши, если их обрабатывает подкласс. NSView

Сделайте первое улучшение небольшим и измеримым: локальное состояние предпросмотра, одна граница сохранения и средство отрисовки добавленных фигур, входные данные которого не меняются во время перемещения указателя. Затем сравните рисунки из растущих синтетических наборов в Instruments. Полученная трассировка покажет, относится ли следующее полезное изменение к управлению зависимостями, владению коллекцией, подготовке геометрии или отрисовке.

Источники