Реалізація In-App Feedback (скриншот + аннотація + описання) у мобільному додатку
Класичні форми зворотного зв'язку дають слабкий контекст: користувач пише «кнопка не працює», розробник не розуміє, яка кнопка й при яких умовах. In-App Feedback з захопленням скриншота й інструментом аннотації вирішує цю проблему — користувач буквально показує, що не так. Рівень деталізації звітів про баги зростає багатократно.
Захоплення скриншота
iOS — UIGraphicsImageRenderer
func captureScreenshot() -> UIImage? {
let renderer = UIGraphicsImageRenderer(bounds: UIScreen.main.bounds)
return renderer.image { ctx in
UIApplication.shared.windows.first?.layer.render(in: ctx.cgContext)
}
}
Важний нюанс: layer.render не захоплює контент WKWebView і ARSCNView — вони рендерятся через окремий GPU-контекст. Для WebView потрібен WKWebView.takeSnapshot(with:):
webView.takeSnapshot(with: nil) { image, error in
// Вставити в підсумковий скриншот через Core Graphics
}
Android — PixelCopy API
До Android 8.0 використовували View.getDrawingCache(), але він не захоплює SurfaceView і TextureView (відео, карти, камера). З Android 8.0+ рекомендується PixelCopy:
fun captureScreenshot(activity: Activity, callback: (Bitmap?) -> Unit) {
val bitmap = Bitmap.createBitmap(
activity.window.decorView.width,
activity.window.decorView.height,
Bitmap.Config.ARGB_8888
)
PixelCopy.request(activity.window, bitmap, { result ->
callback(if (result == PixelCopy.SUCCESS) bitmap else null)
}, Handler(Looper.getMainLooper()))
}
Для Flutter використовується RenderRepaintBoundary:
Future<ui.Image> captureWidget(GlobalKey key) async {
final boundary = key.currentContext!.findRenderObject()
as RenderRepaintBoundary;
return boundary.toImage(pixelRatio: 3.0);
}
Інструмент аннотації
Після захоплення скриншота користувач повинен виділити проблемну область. Базовий набір інструментів: маркер (рисування від руки), стрілка, прямокутне виділення, текстова мітка. Опціонально — пікселізація (blur) для скриття приватних даних перед відправкою.
Реалізація canvas на iOS
class AnnotationCanvasView: UIView {
private var paths: [UIBezierPath] = []
private var currentPath: UIBezierPath?
var strokeColor: UIColor = .red
var strokeWidth: CGFloat = 3.0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let path = UIBezierPath()
path.move(to: touches.first!.location(in: self))
currentPath = path
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
currentPath?.addLine(to: touches.first!.location(in: self))
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let path = currentPath { paths.append(path) }
currentPath = nil
}
override func draw(_ rect: CGRect) {
for path in paths {
strokeColor.setStroke()
path.lineWidth = strokeWidth
path.stroke()
}
strokeColor.setStroke()
currentPath?.stroke()
}
}
Підсумковий аннотований скриншот — це об'єднання canvas-шару поверх зображення скриншота через UIGraphicsImageRenderer.
Готові бібліотеки
Якщо немає завдання писати canvas з нуля: PSPDFKit Annotations (платний, професійний), Pen (iOS, open source), Annotatable (Flutter). Для більшості продуктових задач кастомний canvas займає 2–3 дні розробки й дає повний контроль над UX.
Збір метаданих
До скриншота автоматично прикріпляємо:
struct FeedbackPayload: Encodable {
let screenshot: Data // JPEG, якість 0.7
let description: String
let appVersion: String
let osVersion: String
let deviceModel: String
let screenName: String // поточний екран (маршрутизатор/NavigationStack)
let userId: String?
let sessionId: String // UUID сесії для кореляції з логами
let timestamp: Date
}
screenName особливо важливий — дає змогу відразу зрозуміти, на якому екрані виникла проблема, без розпитування користувача.
Відправка й зберігання
Скриншот відправляємо як multipart/form-data. Для зберігання на backend — S3 або аналог з pre-signed URL. У систему тикетів (Jira, Linear, Sentry) прикріпляємо посилання на зображення.
Приклад через Sentry:
let attachment = Attachment(
data: screenshotData,
filename: "screenshot.jpg",
contentType: "image/jpeg"
)
SentrySDK.capture(message: feedback.description) { scope in
scope.addAttachment(attachment)
scope.setTag(value: feedback.screenName, key: "screen")
}
Орієнтири по строкам
Кастомна реалізація з canvas-аннотацією, захопленням скриншота й відправкою в Jira/Sentry — 1–1,5 тижня. Інтеграція готової бібліотеки аннотацій з кастомним UI обгорткою — 3–5 днів.







