Implementing element comments (Annotations) in a mobile app
We develop annotation systems for mobile apps — this is not just a chat over content, but binding a comment to a specific point on an image, document, or list item.
We have 5+ years of experience in this niche, and have delivered more than 20 projects with annotations for iOS, Android, and Flutter. We will share key technical solutions that ensure stability and performance.
How to normalize coordinates for different screens?
The key problem is that a user taps an image on an iPhone SE with one screen size, while another user views the same document on an iPad Pro in landscape. The pin coordinate must point to the same place.
Solution: we store not absolute pixels but relative coordinates — x and y as a fraction of the container's width and height (from 0.0 to 1.0). During rendering, we multiply by the current container size.
On iOS, this is CGPoint(x: pin.relativeX * containerWidth, y: pin.relativeY * containerHeight). On Flutter — similarly via Positioned inside a Stack with calculated left and top.
For documents with zoom, it is more difficult: you need to account for contentOffset and zoomScale on UIScrollView. We store the coordinate in content space, and during display convert it to screen coordinates via UIScrollView.convert(_:to:).
This guarantees that pins do not “drift” during scaling. A typical bug: calculation in viewport coordinates rather than content. We fix it by adding scrollViewDidZoom to force-update positions.
How to implement real-time annotation synchronization?
Each pin is an object with fields: id, contentId, relativeX, relativeY, authorId, createdAt, text, resolved. The last one is important: the ability to mark a comment as resolved is a standard feature for review tools.
For synchronization between users, we use WebSocket (Socket.io or native URLSessionWebSocketTask). A new pin appears immediately for everyone viewing the same document. Optimistic update: we add the pin to the local state immediately, send the request, and roll back on error.
For offline scenarios: Core Data or SQLite with a pendingSync flag. When the connection is restored — batch synchronization via REST.
Comparison of approaches to storing coordinates
| Approach | Description | Advantages | Disadvantages |
|---|---|---|---|
| Absolute pixels | Fixed x,y in pixels | Simplicity of implementation | Do not work on different screens |
| Relative coordinates | x,y as a fraction of width/height | Scalability across screens | Require normalization |
| Content space coordinates | Internal document coordinate system | Correct during zoom and scroll | More complex conversion |
Relative coordinates with content space are the optimal choice. Pin clustering reduces UI load by 10 times compared to rendering all markers with 50+ points.
Pin component UI
A pin on the screen is a UIView (or View in SwiftUI / widget in Flutter) with absolute positioning. A few details from practice:
- Pins must not extend beyond the container boundaries. When
relativeX > 0.95, we align the tooltip to the left edge; when< 0.05, to the right edge. Similarly vertically. Simple logic, but without it the tooltip goes off-screen. - If there are many pins (50+), it is not worth rendering them all at once. We use clustering: at a small scale, we group nearby pins into a cluster with a number. We expand them on zoom. On iOS —
MKClusterAnnotationas a pattern (even if we are not working with a map). On Flutter — manual clustering viaquadtreeor theflutter_map_marker_clusterlibrary.
Common mistakes during implementation
- Storing coordinates in viewport pixels — pins “drift” during zoom.
- Ignoring ScrollView offset and zoomScale — the pin position does not match after scrolling.
- Lack of optimistic UI — the user waits for the server response.
- Rendering all pins at once with 100+ — FPS drop.
- Deep threads (more than 2 levels) — inconvenient on a mobile device.
Comment thread
There can be several replies to one pin — a thread is needed. We implement it via parentId: root comments have parentId: null, and replies reference the parent. We do not go deeper than one level of nesting in the mobile UI — it is inconvenient.
The thread component opens as a bottom sheet (iOS: UISheetPresentationController with .medium and .large detents; Flutter: DraggableScrollableSheet). This does not cover the entire screen and does not lose the pin context.
What is included in the work
- Pin component with normalized coordinates and zoom support
- Comment add and edit form
- Reply thread in a bottom sheet
- “Resolved” status with visual distinction
- REST API integration + optional WebSocket synchronization
- Pin clustering for large numbers
- Support for images, PDF, arbitrary View containers
Timeline and guarantees
Basic implementation (pins on an image, without thread and synchronization): 2 days. Full version with threads, real-time synchronization, and clustering: 4–5 days. Cost is calculated individually after analyzing the requirements and existing API.
Contact us to evaluate your project. We guarantee quality and adherence to deadlines. Order turnkey development — get a ready-made solution adapted to your stack.







