17 нояб. 2025 г.·6 мин

Оптимизация производительности SwiftUI для длинных списков: практические решения

Настройка производительности SwiftUI для длинных списков: практические решения для лишних перерисовок, стабильной идентификации строк, пагинации, загрузки изображений и плавной прокрутки на старых iPhone.

Оптимизация производительности SwiftUI для длинных списков: практические решения

Как выглядят «медленные списки» в реальных SwiftUI‑приложениях

«Медленный список» в SwiftUI обычно не баг. Это момент, когда интерфейс не успевает за пальцем. Вы замечаете это при прокрутке: список заикается, падают кадры, и всё кажется тяжёлым.

Типичные признаки:

  • Прокрутка подтормаживает, особенно на старых устройствах
  • Строки мерцают или кратко показывают неправильное содержимое
  • Нажатия задерживаются, или свайп‑действия срабатывают с опозданием
  • Телефон нагревается и батарея расходуется быстрее, чем обычно
  • Память растёт по мере прокрутки

Длинные списки кажутся медленными даже если каждая строка «небольшая», потому что стоимость — не только в отрисовке пикселей. SwiftUI всё ещё должен понять, что за строка перед ним, вычислить layout, разрешить шрифты и изображения, выполнить код форматирования и сделать diff при изменениях данных. Если какая‑то из этих операций выполняется слишком часто, список становится горячей точкой.

Полезно разделять две идеи. В SwiftUI «перерисовкой» часто называют перерасчёт body представления. Это обычно дешёвая часть. Дорогим бывает то, что запускает перерасчёт: тяжёлый layout, декодирование изображений, измерение текста или перестройка многих строк из‑за того, что SwiftUI посчитал их идентичность изменившейся.

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

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

Главные причины: идентичность, работа на строку и штормы обновлений

Когда SwiftUI‑список кажется медленным, редко виновато просто «слишком много строк». Дело в лишней работе, которая происходит во время прокрутки: перестройка строк, пересчёт layout или многократная перезагрузка изображений.

Большинство корневых причин укладываются в три группы:

  • Нестабильная идентичность: у строк нет постоянного id, или используется \.self для значений, которые могут меняться. SwiftUI не может сопоставить старые строки с новыми, и перестраивает больше, чем нужно.
  • Слишком много работы на строку: форматирование дат, фильтрация, изменение размера изображений или сетевые/дисковые операции внутри представления строки.
  • Штормы обновлений: одно изменение (ввод текста, тиковый таймер, обновления прогресса) триггерит частые обновления состояния, и список обновляется вновь и вновь.

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

Почему List и LazyVStack ощущаются по‑разному

List — это больше, чем scroll view. Он спроектирован под таблицы/коллекции и системные оптимизации. Чаще всего он эффективнее использует память, но может быть чувствителен к идентичности и частым обновлениям.

ScrollView + LazyVStack даёт больше контроля над лэйаутом и внешним видом, но при этом легче случайно вызвать лишние вычисления layout или триггерить дорогостоящие обновления. На старых устройствах эта лишняя работа будет видна раньше.

Прежде чем переписывать UI, измерьте его. Небольшие правки вроде стабильных id, вынесения работы из строк и уменьшения трения с состоянием часто решают проблему без смены контейнера.

Исправьте идентичность строк, чтобы SwiftUI мог эффективно делать diff

Когда длинный список дергается, виновата часто идентичность. SwiftUI решает, какие строки можно переиспользовать, сравнивая id. Если эти id меняются, SwiftUI считает строки новыми, выбрасывает старые и перестраивает больше, чем нужно. Это может проявляться как случайные перерисовки, потеря позиции прокрутки или запуск анимаций без причины.

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

Распространённая ошибка — генерировать идентичность внутри view:

ForEach(items) { item in
  Row(item: item)
    .id(UUID())
}

Это заставляет каждый рендер создавать новый id, и каждая строка становится «другой» при каждом обновлении.

Предпочитайте id, которые уже есть в модели: первичный ключ БД, серверный ID или стабильный slug. Если такого нет — создавайте его один раз при создании модели, а не внутри view.

struct Item: Identifiable {
  let id: Int
  let title: String
}

List(items) { item in
  Row(item: item)
}

Будьте осторожны с индексами. ForEach(items.indices, id: \.self) привязывает идентичность к позиции. При вставке, удалении или сортировке строки «двигаются», и SwiftUI может перепутать view с данными. Используйте индексы только для действительно статичных массивов.

Если вы используете id: \.self, убедитесь, что значение, которое хешируется, стабильно во времени. Если хэш меняется при обновлении поля, идентичность строки тоже меняется. Простое правило для Equatable и Hashable: базируйте их на одном стабильном id, а не на редактируемых свойствах вроде name или isSelected.

Проверки на адекватность:

  • ID берутся из источника данных (не UUID() во view)
  • ID не меняются при изменении содержимого строки
  • Идентичность не зависит от позиции в массиве, если список может переупорядочиваться

Уменьшите количество перерисовок, сделав строки дешевле

Длинный список часто кажется медленным, потому что каждая строка делает слишком много работы при каждом перерасчёте body. Цель — сделать каждую строку дешёвой для перестроения.

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

Вынесите тяжёлую работу из body

Если что‑то медленно, не делайте этого внутри body строки снова и снова. Предварительно вычисляйте при получении данных, кешируйте в view model или используйте memoization в небольшом хелпере.

Расходники по строке, которые быстро накапливаются:

  • Создание DateFormatter или NumberFormatter для каждой строки
  • Тяжёлое строковое форматирование в body (join, regex, парсинг markdown)
  • Построение производных массивов с .map или .filter в body
  • Чтение больших blob и их преобразование (например, декодирование JSON) во view
  • Слишком сложный лэйаут с множеством вложенных стэков и условных ветвлений

Простой пример: держите форматтеры статичными и передавайте в строку заранее отформатированные строки.

enum Formatters {
    static let shortDate: DateFormatter = {
        let f = DateFormatter()
        f.dateStyle = .medium
        f.timeStyle = .none
        return f
    }()
}

struct OrderRow: View {
    let title: String
    let dateText: String

    var body: some View {
        HStack {
            Text(title)
            Spacer()
            Text(dateText).foregroundStyle(.secondary)
        }
    }
}

Разделяйте строки и используйте Equatable, когда это подходит

Если меняется только небольшая часть (например, бейдж с количеством), изолируйте её в сабвью, чтобы остальная часть строки оставалась стабильной.

Для UI, зависящего от значений, пометка сабвью как Equatable (или обёртка через EquatableView) может помочь SwiftUI пропускать работу, если входы не изменились. Держите equatable‑входы маленькими и конкретными — не передавайте туда всю модель.

Контролируйте обновления состояния, которые триггерят полное обновление списка

Выбирайте, как доставлять и хостить
Разворачивайте в AppMaster Cloud, AWS, Azure, Google Cloud или экспортируйте исходники для самостоятельного хостинга.
Изучить AppMaster

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

Одна распространённая причина — слишком частое воссоздание модели. Если родительское представление пересоздаётся и вы использовали @ObservedObject для view model, которую view само создаёт, SwiftUI может пересоздать её, сбросить подписки и вызвать новые публикации. Если view владеет моделью, используйте @StateObject, чтобы она создавалась один раз и оставалась стабильной. @ObservedObject используйте для объектов, которые инжектятся извне.

Тихий убийца производительности — слишком частые публикации. Таймеры, Combine‑пайплайны и обновления прогресса могут срабатывать много раз в секунду. Если published‑свойство влияет на список (или находится в общем ObservableObject, используемом экраном), каждый тик может инвалидировать список.

Пример: у вас поле поиска, которое обновляет query при каждом вводе, а потом фильтрует 5000 элементов. Если фильтрация происходит немедленно, список постоянно делает diff в процессе ввода. Примените дебаунс к запросу и обновляйте отфильтрованный массив после небольшой паузы.

Шаблоны, которые обычно помогают:

  • Держите быстро меняющиеся значения вне объекта, управляющего списком (используйте меньшие объекты или локальный @State)
  • Дебаунсьте поиск и фильтрацию, чтобы список обновлялся после паузы в вводе
  • Избегайте высокочастотных публикаций таймера; обновляйте реже или только при реальном изменении значения
  • Держите состояние строки локальным (@State) если оно действительно принадлежит этой строке, а не одному общему объекту
  • Разделяйте большие модели: один ObservableObject для данных списка, другой — для экранного UI

Идея проста: сделайте время прокрутки «тихим». Если ничего важного не изменилось, список не должен просить систему сделать работу.

Выбирайте правильный контейнер: List vs LazyVStack

Выбор контейнера влияет на объём работы, которую делает система.

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

ScrollView с LazyVStack хорош, когда нужен кастомный лэйаут: карточки, смешанный контент, специальные хедеры или фид в стиле соцсетей. «Lazy» значит, что строки строятся по мере появления на экране, но это не даёт все те же гарантий, что есть в List. При очень больших наборах это может означать больше памяти и более заметные подтормаживания на старых устройствах.

Простое правило:

  • Используйте List для классических таблиц: настройки, почта, заказы, админки
  • Используйте ScrollView + LazyVStack для кастомных лэйаутов и смешанного контента
  • Если у вас тысячи элементов и нужен только табличный вид — начните с List
  • Если вам нужен пиксельно‑точный контроль, пробуйте LazyVStack, но измеряйте память и падения FPS

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

Конкретный пример: экран «Заказы» с 5000 строк часто остаётся плавным в List, потому что строки переиспользуются. Если вы переключитесь на LazyVStack и начнёте строить карточки с большими тенями и несколькими оверлеями, вы можете получить дёрганую прокрутку, хотя код выглядит аккуратно.

Пагинация, которая остаётся плавной и не вызывает всплесков памяти

Быстро создавать быстрые админки
Создавайте внутренние инструменты и админки, которые остаются отзывчивыми даже при тысячах записей.
Попробовать AppMaster

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

Начинайте с чёткого контракта пагинации: фиксированный размер страницы (например, 30–60 элементов), флаг «конец результатов» и строка загрузки, которая появляется только во время fetch‑а.

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

Вот простой паттерн:

@State private var items: [Item] = []
@State private var isLoading = false
@State private var reachedEnd = false

func loadNextPageIfNeeded(currentIndex: Int) {
    guard !isLoading, !reachedEnd else { return }
    let threshold = max(items.count - 5, 0)
    guard currentIndex >= threshold else { return }

    isLoading = true
    Task {
        let page = try await api.fetchPage(after: items.last?.id)
        await MainActor.run {
            let newUnique = page.filter { p in !items.contains(where: { $0.id == p.id }) }
            items.append(contentsOf: newUnique)
            reachedEnd = page.isEmpty
            isLoading = false
        }
    }
}

Это помогает избежать дублирующихся строк (перекрывающиеся результаты API), гонок от множества onAppear и загрузки слишком большого объёма сразу.

Если у списка есть pull to refresh, аккуратно сбрасывайте состояние пагинации (очищайте items, сбрасывайте reachedEnd, отменяйте текущие задачи, если возможно). Если вы контролируете бэкенд — стабильные ID и курсорная пагинация делают UI заметно плавнее.

Изображения, текст и layout: держите рендеринг строки лёгким

Создавайте быстрее, чем ручная оптимизация списков
Смоделируйте данные и пагинацию один раз, а затем генерируйте iOS, веб и backend код вместе.
Попробовать AppMaster

Длинные списки редко замедляются из‑за контейнера. Чаще виноватая — строка. Изображения — частый виновник: декодирование, ресайз и отрисовка могут не поспевать за скоростью скролла, особенно на старых устройствах.

Если вы загружаете удалённые изображения, убедитесь, что тяжёлая работа не выполняется в основном потоке во время прокрутки. Также избегайте скачивания полноразмерных активов для миниатюры 44–80 pt.

Пример: «Сообщения» с аватарами. Если каждая строка скачивает изображение 2000×2000, масштабирует его и применяет blur или shadow, список будет тормозить, даже если модель проста.

Делайте работу с изображениями предсказуемой

Важные практики:

  • Используйте серверные или предгенерированные миниатюры, близкие к отображаемому размеру
  • Декодируйте и масштабируйте вне главного потока, где возможно
  • Кэшируйте миниатюры, чтобы быстрая прокрутка не перезапрашивала и не пердекодировала их
  • Используйте плейсхолдер того же размера, чтобы избежать мерцаний и перерасчётов layout
  • Избегайте тяжёлых модификаторов на изображениях в строках (сложные тени, маски, размытия)

Стабилизируйте layout, чтобы избежать треша

SwiftUI может тратить больше времени на измерения, чем на отрисовку, если высота строки постоянно меняется. Старайтесь держать строки предсказуемыми: фиксированные фреймы для миниатюр, постоянные лимиты строк и стабильные отступы. Если текст может расширяться, ограничьте его (например, 1–2 строки), чтобы одно обновление не заставляло пересчитывать всё.

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

Как измерять: проверки в Instruments, которые выявляют реальные узкие места

Работа с производительностью — если полагаться только на «кажется, дергается», — довольно гадательная. Instruments показывает, что выполняется в главном потоке, какие объекты выделяются при быстрой прокрутке и что вызывает падение кадров.

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

Три вида в Instruments, которые дают пользу

Используйте их вместе:

  • Time Profiler: ищите пики на главном потоке во время прокрутки. Layout, измерение текста, парсинг JSON и декодирование изображений здесь обычно объясняют зависание.
  • Allocations: следите за всплесками временных объектов при быстрой прокрутке. Это часто указывает на многократное форматирование, создание атрибутных строк или перестройку per‑row моделей.
  • Core Animation: подтверждает сброшенные кадры и долгие времена рендера. Это помогает отделить давление рендера от медленной работы с данными.

Когда находите пик, заходим в дерево вызовов и спрашиваем: случается ли это раз за экран или раз на строку при прокрутке? Второе ломает плавность.

Добавляйте signpost‑ы для событий прокрутки и пагинации

Многие приложения делают лишнюю работу при скролле (загрузки изображений, пагинация, фильтрация). Signpost‑ы помогают увидеть эти моменты на таймлайне.

import os
let log = OSLog(subsystem: "com.yourapp", category: "list")
os_signpost(.begin, log: log, name: "LoadMore")
// fetch next page
os_signpost(.end, log: log, name: "LoadMore")

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

Распространённые ошибки, которые тихо убивают производительность списков

Получите корректную идентичность с первого дня
Проектируйте стабильные идентификаторы и таблицы PostgreSQL визуально до того, как SwiftUI начнёт рендерить строки.
Создать проект

Некоторые проблемы очевидны (большие изображения, огромные наборы данных). Другие проявляются лишь при росте данных, особенно на старых устройствах.

1) Нестабильные ID строк

Классическая ошибка — создавать ID внутри view, например id: \.self для ссылочных типов или UUID() в теле строки. SwiftUI использует идентичность для diff‑а. Если id меняется, строка считается новой, её перестраивают, и может быть выброшен кэш layout.

Используйте стабильный id из модели (первичный ключ БД, серверный ID или сохранённый UUID, созданный один раз при создании элемента). Если его нет — добавьте.

2) Тяжёлая работа внутри onAppear

onAppear выполняется чаще, чем многие думают, потому что строки входят и выходят из экрана при прокрутке. Если каждая строка в нём запускает декодирование изображения, парсинг JSON или запрос в базу — вы получите многократные пики.

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

3) Связывание всего списка через биндинги для редактирования строк

Когда каждая строка получает @Binding в большой массив, небольшое редактирование выглядит как большое изменение. Это может заставить многие строки переоцениться, а иногда и весь список обновиться.

Предпочитайте передавать неизменяемые значения в строку и возвращать изменения через компактное действие (например, «toggle favorite для id»). Держите per‑row состояние локальным, если оно действительно принадлежит строке.

4) Слишком много анимаций при прокрутке

Анимации дороги в списке, потому что могут вызвать дополнительные проходы layout. Применение animation(.default, value:) высоко (на весь список) или анимация каждой мелкой переменной может сделать прокрутку «липкой».

Простые правила:

  • Ограничивайте анимации области, которая реально меняется
  • Избегайте анимаций во время быстрой прокрутки (особенно для выделения/выбора)
  • Остерегайтесь неявных анимаций на часто меняющихся значениях
  • Предпочитайте простые переходы вместо сложных комбинированных эффектов

Реальный пример: чат, где каждая строка запускает сетевой fetch в onAppear, использует UUID() как id и анимирует статус «seen». Это создаёт постоянный churn строк. Исправление идентичности, кэширование работы и ограничение анимаций часто делает тот же UI мгновенно плавным.

Быстрый чек‑лист, простой пример и дальнейшие шаги

Если вы успеете сделать только несколько вещей, начните с этого:

  • Используйте стабильный уникальный id для каждой строки (не индекс массива, не свежесозданный UUID)
  • Сделайте работу строки максимально маленькой: избегайте тяжёлого форматирования, больших деревьев view и дорогих вычисляемых свойств в body
  • Контролируйте публикации: не позволяйте быстро меняющемуся состоянию (таймеры, ввод, прогресс) инвалидировать весь список
  • Загружайте постранично и предвыгружайте, чтобы память оставалась плоской
  • Измеряйте до и после в Instruments, чтобы не гадать

Представьте себе инбокс поддержки с 20 000 переписок. Каждая строка показывает тему, превью последнего сообщения, метку времени, бейдж непрочитанных и аватар. Пользователи могут искать, и новые сообщения приходят во время прокрутки. Медленная версия обычно делает несколько вещей одновременно: она перестраивает строки при каждом вводе в поиск, слишком часто измеряет текст и преждевременно подгружает слишком много изображений.

Практический план, который не требует переделки кода:

  • Базовая проверка: запишите короткую прокрутку и сессию поиска в Instruments (Time Profiler + Core Animation).
  • Исправьте идентичность: убедитесь, что у модели есть реальный id из сервера/БД, и ForEach использует его стабильно.
  • Добавьте пагинацию: начните с самых свежих 50–100 элементов, подгружайте по мере приближения к концу.
  • Оптимизируйте изображения: используйте меньшие миниатюры, кешируйте и избегайте декодирования в главном потоке.
  • Повторно измерьте: подтвердите меньше проходов layout, меньше обновлений view и более ровные времена кадров на старых устройствах.

Если вы строите полный продукт (iOS‑приложение плюс бэкенд и веб‑админка), полезно проектировать модель данных и контракт пагинации заранее. Платформы вроде AppMaster (appmaster.io) ориентированы на такой full‑stack рабочий процесс: вы можете визуально определить данные и бизнес‑логику и затем сгенерировать реальный исходный код для деплоя или self‑host.

Вопросы и ответы

Какой самый быстрый способ, если мой SwiftUI-список тормозит при прокрутке?

Начните с исправления идентичности строк. Используйте стабильный id из вашей модели и избегайте генерации идентификаторов во view: изменение id заставляет SwiftUI считать строки новыми и перестраивать их значительно чаще, чем нужно.

SwiftUI медленный из‑за того, что он «перерисовывает» слишком часто?

Пересчёт body обычно дешёв: дорогой work — это то, что он запускает. Тяжёлый layout, измерение текста, декодирование изображений и перестройка множества строк из‑за нестабильной идентичности — вот что обычно вызывает потерю кадров.

Как выбрать стабильный `id` для `ForEach` и `List`?

Не используйте UUID() внутри строки и не полагайтесь на индекс массива, если данные могут вставляться, удаляться или сортироваться. Лучше всего — server/database ID или UUID, созданный и сохранённый в модели при её создании, чтобы идентификатор оставался неизменным между обновлениями.

Может ли `id: .self` ухудшить производительность списка?

Может. Особенно если хэш-значение изменяется при редактировании полей: SwiftUI будет считать элемент другим. Если нужен Hashable, базируйте его на одном стабильном идентификаторе, а не на изменяемых свойствах вроде name или isSelected.

Чего следует избегать внутри `body` строки?

Убирайте тяжёлую работу из body. Переформатируйте даты и числа заранее, не создавайте новый форматтер для каждой строки, и не стройте большие производные массивы с map/filter в представлении — вычисляйте это в модели или view model и передавайте в строку готовые значения для показа.

Почему у меня так часто срабатывает `onAppear` в длинном списке?

onAppear запускается чаще, чем ожидают, потому что строки появляются и исчезают при прокрутке. Если в нём делать тяжёлую работу (декодирование изображений, чтение из БД, парсинг), вы получите постоянные пики нагрузки; ограничьте onAppear дешевыми действиями, например триггером пагинации рядом с концом.

Что вызывает «штормы обновлений», из‑за которых прокрутка залипает?

Любое быстро меняющееся published‑свойство, используемое списком, может неоднократно инвалидировать его, даже если сами данные строк не менялись. Держите таймеры, ввод текста и прогресс вне основного объекта списка, применяйте дебаунс для поиска и раскладывайте большие ObservableObject на меньшие по мере необходимости.

Когда стоит использовать `List`, а когда `LazyVStack` для больших наборов данных?

Используйте List, если интерфейс похож на классическую таблицу: строки с текстом, изображениями, свайп‑действиями, выбором и разделителями — системные оптимизации помогут. Берите ScrollView + LazyVStack для кастомного лэйаута, но тестируйте на память и падения частоты кадров, потому что вы легче можете случайно вызвать лишние расчёты layout.

Какой простой подход к пагинации остаётся плавным?

Не ждите останова на последней строке: начинайте загрузку заранее, когда пользователь достигает порога в последних нескольких элементах, и защищайте загрузку от дублей. Поддерживайте разумный размер страницы, флаг isLoading и reachedEnd, и убирайте дубли по стабильным идентификаторам, чтобы избежать повторных diff‑операций.

Как измерить, что реально замедляет мой SwiftUI‑список?

Опишите базовый сценарий на реальном устройстве и используйте Instruments: Time Profiler покажет, что блокирует главный поток, Allocations выявит временные объекты, возникающие при быстрой прокрутке, а Core Animation подтвердит сброшенные кадры. Так вы поймёте, рендеринг это или тяжёлая работа с данными.

Легко начать
Создай что-то невероятное

Экспериментируйте с AppMaster с бесплатной подпиской.
Как только вы будете готовы, вы сможете выбрать подходящий платный план.

Попробовать AppMaster