Ми розробляємо візуальний редактор діалогових дерев для мобільних додатків. Це один із найскладніших технічно UI-компонентів: нескінченний canvas, gesture-система з кількома одночасними розпізнавачами, рендеринг тисяч елементів, undo/redo, серіалізація графа. За 5+ років на ринку ми реалізували десятки подібних проєктів для чат-ботів у банкінгу, e-commerce та підтримці. Наші інженери володіють як нативними стеками (Swift, Kotlin), так і крос-платформними рішеннями (Flutter, React Native). Гарантуємо коректну роботу на всіх цільових пристроях та відповідність гайдлайнам App Store і Google Play.
Як реалізувати нескінченне полотно?
Основа редактора — нескінченний масштабований простір для розміщення нод. Два принципово різних підходи — порівняємо їх у таблиці.
| Критерій | UIScrollView як viewport | Кастомний canvas з трансформацією |
|---|---|---|
| Простота реалізації | Висока, нативний scroll | Середня, потрібна ручна обробка жестів |
| Масштабування тексту | Проблема: шрифти масштабуються | Фікс через зворотну трансформацію |
| Гнучкість zoom-to-fit | Обмежена | Повна, через матрицю |
| Продуктивність (100+ нод) | Середня | Висока (один CALayer) |
На iOS UIScrollView з contentSize 5000×5000 pt — простий старт. Ноди — UIView всередині contentView. Масштабування через minimumZoomScale/maximumZoomScale. Мінус: при zoom текст стає нечитабельним. Фікс — зворотна трансформація label'ів при scrollViewDidZoom. Кастомний canvas через CATransform3D дає більше контролю: zoom-to-fit, snap-to-grid. Конфлікт жестів вирішується через UIGestureRecognizerDelegate: pan на ноді — drag, pan на порожньому місці — панорамування.
У Jetpack Compose — Modifier.graphicsLayer з pointerInput для detectTransformGestures.
Чому важливе коректне керування жестами?
Редактор одночасно використовує pan, pinch, long press та tap. Без чіткої пріоритизації дії користувача інтерпретуватимуться невірно. На iOS ми використовуємо UIGestureRecognizerDelegate з shouldRecognizeSimultaneouslyWith для pan і pinch, а hit-test на початку жесту визначає, чи перетягується нода або полотно. На Android — GestureDetector з різними слухачами. Такий підхід виключає хибні спрацювання при інтенсивній роботі.
Рендеринг ребер (стрілок)
Криві Безьє для з'єднань між нодами — візуально краще прямі лінії. Кубічна крива від output handle до input handle:
let path = UIBezierPath() path.move(to: startPoint) let controlPoint1 = CGPoint(x: startPoint.x + (endPoint.x - startPoint.x) * 0.5, y: startPoint.y) let controlPoint2 = CGPoint(x: startPoint.x + (endPoint.x - startPoint.x) * 0.5, y: endPoint.y) path.addCurve(to: endPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) edgeLayer.path = path.cgPath Кожне ребро — CAShapeLayer з strokeColor та стрілковим наконечником. При переміщенні ноди — перераховуємо всі CAShapeLayer.path для вхідних та вихідних ребер. CATransaction.begin(); CATransaction.setDisableActions(true) для миттєвого оновлення. При великій кількості ребер (50+) — рендеринг через один спільний CALayer з draw-методом: один CGContext.addPath() для всіх ребер швидше ніж 50 окремих шарів.
Інтерактивне створення зв'язків
Користувач тягне від output handle однієї ноди до input handle іншої — створюється нове ребро. Реалізація:
-
UILongPressGestureRecognizer(duration 0.0) на handle view — початок drag - При
.began— створюємоtemporaryEdgeLayer, початок зафіксовано на handle - При
.changed— оновлюємо кінецьtemporaryEdgeLayerна позицію пальця - Hit-test при кожному
.changed:canvas.hitTest(location, with: nil)— якщо потрапляємо на input handle — підсвічуємо її - При
.ended— якщо під пальцем input handle — створюємо постійне ребро, інакше видаляємо temporaryEdgeLayer
Запобігання невалідним зв'язкам: не можна з'єднати ноду з собою, не можна створити дублюючий зв'язок, не можна створити цикл (якщо граф має бути DAG). Перевірка циклу — DFS з target ноди. O(V + E) — швидко для типових діалогових дерев.
Редагування нод
Торкання на ноді — відкривається редактор вмісту. Для діалогового вузла типу «Message»: UITextView з rich text підтримкою для тексту повідомлення, перемикач типу (текст/медіа/кнопки), список кнопок з можливістю додати/видалити.
Bottom sheet (iOS 15+ UISheetPresentationController з .medium та .large detents) — користувач бачить граф і редактор одночасно в medium режимі. Keyboard avoidance: при появі клавіатури нижній padding UIScrollView всередині sheet збільшується на keyboardFrame.height - sheet.frame.height + sheetMaxHeight. Обробляється через UIKeyboardWillShowNotification.
Undo/Redo
Command Pattern — кожна дія (переміщення ноди, створення/видалення ребра, редагування вмісту) реалізується як об'єкт команди з методами execute() та undo().
protocol GraphCommand { func execute() func undo() } struct MoveNodeCommand: GraphCommand { let node: GraphNode let fromPosition: CGPoint let toPosition: CGPoint func execute() { node.position = toPosition } func undo() { node.position = fromPosition } } UndoManager — вбудований в iOS UIKit, працює з registerUndo(withTarget:handler:). Або власний CommandStack з undoStack: [GraphCommand] та redoStack: [GraphCommand]. Групування команд: переміщення кількох виділених нод — одна операція undo.
Виділення та multiple selection
Ласо-виділення: pan на порожньому просторі при утриманні натиснення — малює прямокутник виділення через CAShapeLayer з fillColor = UIColor.blue.withAlphaComponent(0.1) та strokeColor = UIColor.blue. При завершенні жесту — перевіряємо всі ноди на перетин. Виділені ноди — синя обводка через layer.borderColor. Pan на виділенні переміщує всі виділені ноди одночасно. Delete — видалення всіх виділених нод і інцидентних ребер.
Серіалізація та синхронізація
Граф → JSON з повним описом нод і ребер. Автозбереження кожні 30 секунд або при кожній зміні з debounce 2 секунди. На iOS — Combine.debounce(for: .seconds(2), scheduler: RunLoop.main) на publisher змін графа, потім URLSession.shared.dataTask для відправки на сервер.
Conflict resolution при спільному редагуванні: Last-Write-Wins на рівні ноди з timestamp сервера. updatedAt: Date на кожній ноді — при merge беремо версію з пізнішим timestamp.
Що входить в роботу?
- Дослідження та проектування структури вузлів під ваш чат-бот
- Реалізація кастомного canvas з нескінченним полотном та масштабуванням
- Система undo/redo та multiple selection
- Інтеграція з вашим бекендом через REST/GraphQL
- Підтримка всіх типів вузлів (текст, медіа, кнопки, умови)
- Тестування на цільових пристроях та публікація в сторах
- Документація та навчання вашої команди
Термін: від 1 тижня до 3 місяців. Вартість розраховується індивідуально після аналізу вимог. Зв'яжіться з нами — оцінимо ваш проєкт і запропонуємо оптимальне рішення.







