Implementing Coach Marks (Pointers) for User Education in Mobile Apps
Coach marks — instant contextual explanations: darkened screen, highlighted element, arrow and one phrase "Tap here to add task". Differ from walkthrough by not requiring sequential action execution — just point and explain. Technically simpler than walkthrough, but has own challenges.
iOS Implementation
Most reliable approach — UIView overlay above window with masking via CAShapeLayer. Create singleton CoachMarkManager or embed in UIViewController subclass.
Getting target element frame:
let globalFrame = targetView.convert(targetView.bounds, to: UIApplication.shared.keyWindow)
keyWindow deprecated since iOS 13 — correct: UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as UIWindowScene, then .windows.first(where: { $0.isKeyWindow }).
Overlay mask: UIBezierPath(rect: overlayView.bounds).appending(UIBezierPath(ovalIn: globalFrame.insetBy(dx: -8, dy: -8))) with fillRule = .evenOdd. Animate appearance via CABasicAnimation on opacity.
Libraries. Instructions (ephread/Instructions) — mature iOS library with CoachMarkController, custom bubble-view support and accessibility. EasyTipView for simple tooltips without overlay. If standard design — library saves a day. If custom overlay with non-standard highlight shapes — build yourself.
In SwiftUI — anchorPreference(key:value:) + overlayPreferenceValue let place overlay relative to arbitrary view without knowing coordinates beforehand. This is correct SwiftUI-way, but requires diving into preference system. Alternative — GeometryReader + .coordinateSpace(name:) for global coordinates.
Android and Compose
On Android — library TapTargetView (KeepSafe) for Material-styled coach marks. Works with regular Views. For Compose — Spotlight (TakuSemba) or custom via Popup + Canvas.
Custom Compose implementation:
@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
)
}
// Position tooltip via offset from targetRect
}
BlendMode.Clear requires graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } on parent container — without it Clear doesn't work properly.
Coach Marks Sequence and Order
If multiple coach marks — show one by one. Queue [CoachMarkConfig] in CoachMarkManager. After dismissing each — small 300ms delay before next (user needs second to process what they saw).
Display conditions: after specific user action, after N-th app launch, after update to version X. Logic in CoachMarkScheduler — separate object with UserDefaults persistence. No conditions in controllers.
Accessibility
Coach mark visible to VoiceOver/TalkBack. Overlay view — accessibilityViewIsModal = true on iOS (all elements under overlay disappear from accessibility tree). Hint text — accessibilityLabel on bubble view. Dismiss button — accessibilityLabel = "Close hint". With VoiceOver set focus on bubble automatically via UIAccessibility.post(notification: .screenChanged, argument: bubbleView).
Timeline: 1–3 days. Single coach mark with simple highlight — 1 day. System with queue, JSON config, custom highlight shapes and full accessibility — 3 days.







