Реализация экрана торговли (Trading View) в мобильном приложении биржи
Экран торговли в биржевом приложении — один из самых технически плотных UI в мобильной разработке. Свечной график с сотнями точек данных, стакан котировок с обновлением в реальном времени, форма ордера с мгновенной валидацией, и всё это должно работать на 60 fps при WebSocket-потоке с 10 обновлениями в секунду. Именно здесь производительность отрисовки напрямую влияет на деньги пользователя.
Архитектура данных и real-time обновления
WebSocket и управление потоком данных
Биржевые данные приходят по WebSocket — подписка на пары, получение тиков и обновлений стакана. На iOS — URLSessionWebSocketTask (нативный, с iOS 13) или Starscream библиотека для более гибкого управления reconnect-логикой. На Android — OkHttp WebSocket или Ktor WebSocket client.
Критически важна стратегия throttling входящих данных. WebSocket может присылать 20–50 обновлений в секунду для активных пар. Обновлять UI на каждое — перегрузка main thread. Правильный подход: накапливаем обновления в ConcurrentQueue на background thread, применяем к UI с частотой 60fps (через CADisplayLink на iOS или Choreographer на Android).
// iOS: накопление обновлений
private var pendingOrderBookUpdates: [OrderBookUpdate] = []
private let updateQueue = DispatchQueue(label: "orderbook.updates")
func handleWebSocketMessage(_ update: OrderBookUpdate) {
updateQueue.async { [weak self] in
self?.pendingOrderBookUpdates.append(update)
}
}
// CADisplayLink callback — на main thread, 60fps
@objc func applyPendingUpdates() {
var updates: [OrderBookUpdate] = []
updateQueue.sync { updates = pendingOrderBookUpdates; pendingOrderBookUpdates.removeAll() }
guard !updates.isEmpty else { return }
// Применяем batch updates к UI
applyToOrderBook(updates)
}
Модель данных стакана
Order book — это sorted structure: bids (покупки) отсортированы по убыванию цены, asks (продажи) по возрастанию. SortedArray или TreeMap (Java) / AVLTree (Swift кастомная реализация) для O(log n) вставки и удаления при обновлениях. Обновление стакана — не полная замена, а patch: новый уровень, изменённый volume, удалённый уровень (volume = 0).
Для iOS стакана — NSFetchedResultsController-паттерн без CoreData: хранить bids: [(price: Decimal, size: Decimal)] и asks: [(price: Decimal, size: Decimal)], при изменении — diffing через DeepDiff или Swift's CollectionDifference для минимальных UITableView updates.
Свечной график
Кастомная отрисовка через Metal / Core Graphics
Стандартный UIKit / View слишком медленен для интерактивного свечного графика с 1000+ свечей. Варианты:
Core Graphics / UIKit Drawing. UIView.draw(_:) с UIGraphicsGetCurrentContext(). Каждая свеча — два прямоугольника (тело и тень) через context.fill(). Для 500 свечей в draw(_:) — 8–12 мс на iPhone 12, что вписывается в 16 мс frame budget. Не рекомендуется для 2000+ свечей или анимированных обновлений.
CALayer / CAShapeLayer. Каждая свеча — отдельный CALayer. Не делай так: 500 CALayer на экране — катастрофа для композитора. Один CALayer с кастомным drawing — правильно.
Metal. Для профессиональных trading-приложений с анимированным scalable графиком — MetalKit. MTLBuffer с vertex data для свечей, vertex shader рисует прямоугольники в пространстве экрана. 10 000 свечей — 1–2 мс render time. TradingView (веб) использует canvas с WebGL — тот же принцип.
Готовые библиотеки. Charts (Daniel Gindi) — популярная iOS/macOS библиотека, имеет CandleStickChartView. Не real-time, но для периодических обновлений подходит. LightweightCharts — официальная мобильная обёртка над TradingView Lightweight Charts (WebView-based). Последнее — самый быстрый путь до production-ready графика с техническими индикаторами.
Жесты и навигация по графику
Pinch для масштабирования, pan для перемещения по временной оси. UIPinchGestureRecognizer + UIPanGestureRecognizer одновременно через gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) -> true.
При масштабировании — visibleRange меняется, подгружаем исторические свечи для нового диапазона через REST API. Infinite scroll влево: при достижении leftEdge — запрос исторических данных, добавление в начало датасета без сброса текущей позиции (обновляем contentOffset на ширину добавленных свечей).
Crosshair при long press — UILongPressGestureRecognizer с minimumPressDuration = 0.1. При движении пальца — обновляем позицию crosshair и OHLCV-тултип с данными свечи под курсором. Hit-test по X-координате: candleIndex = Int((touchX - chartOriginX) / (candleWidth + candleSpacing)).
Форма ордера
Валидация и ввод цены/объёма
UITextField с inputView — кастомная цифровая клавиатура вместо системной. На ней — кнопки 25%, 50%, 75%, 100% от доступного баланса для быстрого ввода. Decimal для хранения, не Double.
Мгновенная валидация при вводе: минимальный объём, шаг цены (tick size), доступный баланс. Combine publisher(for: \.text) на UITextField с debounce(0.1) и map { Decimal(string: $0) }.
Предварительный расчёт стоимости ордера в реальном времени: total = price * amount с учётом комиссии. Обновляется при каждом изменении price или amount. NSDecimalNumber.multiplying(by:) для точности.
Buy/Sell переключение
Анимированный переход между buy и sell режимом: background цвет формы плавно меняется (зелёный ↔ красный), текст кнопки, иконки направления. На iOS — UIView.animate(withDuration: 0.2) с изменением backgroundColor. Haptic .impact(.medium) при переключении. В SwiftUI — withAnimation(.easeInOut(duration: 0.2)).
Подтверждение ордера
Bottom sheet с деталями ордера перед отправкой. UISheetPresentationController (iOS 15+) или кастомный bottom sheet. При подтверждении — кнопка переходит в loading state (UIActivityIndicatorView вместо текста), блокируется повторное нажатие. После ответа API — success animation (зелёная галочка с scale animation) или error с конкретным текстом ошибки из API.
Производительность и оптимизация
UITableView для стакана
Order book — UITableView с бесконечным potential height. UITableView.performBatchUpdates() для анимированного добавления/удаления/обновления строк. При 10 обновлениях/сек — batching изменений: не вызываем performBatchUpdates на каждый тик, собираем за 100 мс и применяем разом.
Ячейка стакана — кастомный UITableViewCell с UILabel-ами для цены, объёма и depth bar (визуальная полоса пропорциональная объёму на уровне). Depth bar — UIView с constraint на width, обновляемый через UIView.animate. Не перерисовываем всю ячейку — только меняем constant constraint.
Профилирование
Xcode Instruments > Time Profiler для поиска hot spots в цикле обновлений. Core Animation instrument для frame drops. Целевые метрики: <16 мс на обработку WebSocket batch, <5 мс на обновление UI, 0 dropped frames при скролле стакана.
На Android — Android Profiler в режиме CPU+Memory+Network одновременно. systrace для анализа длинных frames в Choreographer.
Offline и reconnect
При потере соединения: overlay «Нет подключения» поверх графика (не заменяем весь экран), последние известные данные остаются видимыми. При восстановлении: автоматический reconnect WebSocket с exponential backoff (1с, 2с, 4с, 8с, max 30с), REST запрос актуального snapshot стакана и последних свечей, seamless resume без сброса позиции графика.
Срок: 5 дней. За это время: WebSocket интеграция с throttling, свечной график на Core Graphics или LightweightCharts, order book с batch updates, форма ордера с валидацией и подтверждением. Кастомный Metal-рендеринг графика с индикаторами и расширенная аналитика — дополнительно 5–10 дней.







