Реализация Coach Marks (указателей) для обучения пользователя в мобильном приложении
Coach marks — это мгновенные контекстные объяснения: затемнённый экран, подсвеченный элемент, стрелка и одна фраза «Нажмите здесь, чтобы добавить задачу». Отличаются от walk-through тем, что не требуют последовательного выполнения действий — просто указывают и объясняют. Технически проще walk-through, но свои проблемы есть.
Реализация на iOS
Самый надёжный подход — UIView-оверлей поверх window с маскированием через CAShapeLayer. Создаём синглтон CoachMarkManager или встраиваем в UIViewController-наследник.
Получение frame целевого элемента:
let globalFrame = targetView.convert(targetView.bounds, to: UIApplication.shared.keyWindow)
keyWindow устарел с iOS 13 — правильно: UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) как UIWindowScene, затем .windows.first(where: { $0.isKeyWindow }).
Маска overlay: UIBezierPath(rect: overlayView.bounds).appending(UIBezierPath(ovalIn: globalFrame.insetBy(dx: -8, dy: -8))) с fillRule = .evenOdd. Анимируем появление через CABasicAnimation на opacity.
Библиотеки. Instructions (ephread/Instructions) — зрелая iOS библиотека с CoachMarkController, поддержкой кастомных bubble-view и accessibility. EasyTipView для простых tooltip без overlay. Если дизайн стандартный — библиотека сэкономит день. Если кастомный overlay с нестандартными формами highlight — пишем сам.
В SwiftUI — anchorPreference(key:value:) + overlayPreferenceValue позволяют разместить overlay относительно произвольного view без знания его координат заранее. Это правильный SwiftUI-way, но требует погружения в preference system. Альтернатива — GeometryReader + .coordinateSpace(name:) для получения глобальных координат.
Android и Compose
На Android — библиотека TapTargetView (KeepSafe) для Material-styled coach marks. Работает с обычными View. Для Compose — Spotlight (TakuSemba) или кастомная реализация через Popup + Canvas.
В Compose кастомная реализация:
@Composable
fun CoachMarkOverlay(targetRect: Rect, text: String) {
Canvas(modifier = Modifier.fillMaxSize()) {
drawRect(color = Color.Black.copy(alpha = 0.7f))
drawCircle(
color = Color.Transparent,
radius = targetRect.size.minDimension / 2 + 12f,
center = targetRect.center,
blendMode = BlendMode.Clear
)
}
// Tooltip позиционируем через offset от targetRect
}
BlendMode.Clear требует graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } на родительском контейнере — без этого Clear не работает корректно.
Последовательность и очерёдность coach marks
Если coach marks несколько — показываем по одному. Очередь [CoachMarkConfig] в CoachMarkManager. После dismiss каждого — небольшая задержка 300 мс перед следующим (пользователю нужно секунда осознать увиденное).
Условия показа: после конкретного действия пользователя, после N-го запуска приложения, после обновления до версии X. Логика в CoachMarkScheduler — отдельный объект с UserDefaults-персистентностью. Никаких условий в контроллерах.
Доступность
Coach mark должен быть виден VoiceOver/TalkBack. Overlay view — accessibilityViewIsModal = true на iOS (все элементы под overlay исчезнут из accessibility tree). Текст подсказки — accessibilityLabel на bubble view. Dismiss-кнопка — accessibilityLabel = "Закрыть подсказку". При VoiceOver фокус устанавливаем на bubble автоматически через UIAccessibility.post(notification: .screenChanged, argument: bubbleView).
Срок: 1–3 дня. Один coach mark с простым highlight — 1 день. Система с очередью, конфигурацией из JSON, кастомными формами highlight и полной accessibility — 3 дня.







