Реализация In-App Feedback (скриншот + аннотация + описание) в мобильном приложении
Классические 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()
}
}
Итоговый аннотированный скриншот — merge canvas layer поверх screenshot image через 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 дней.







