Розробка мобільного додатку для оплати парковки
Додаток для оплати парковки—це не просто кнопка «Оплатити». Ключова складність тут—сесійна модель: парковочна сесія починається при в'їзді, закінчується при виїзді або за минулий час. Користувач повинен знати, скільки часу залишилось, та отримати попередження до того, як розпочнуться штрафні санкції. Це вимагає точної роботи push-повідомлень, фонових таймерів та надійної інтеграції з платіжним шлюзом.
Як улаштована парковочна сесія
Центральна сутність—ParkingSession. Має життєвий цикл:
IDLE → ACTIVE → EXPIRING (за 15 хв до кінця) → EXPIRED / EXTENDED
Переходи станів управляються на серверу. Додаток відображає поточний стан через polling або WebSocket. Фоновий таймер на пристрою—лише для UI, не для бізнес-логіки.
Типовий об'єкт сесії:
{
"sessionId": "PSN-20240921-4471",
"zoneCode": "A-12",
"vehiclePlate": "А123ВС77",
"startedAt": "2024-09-21T10:15:00+03:00",
"expiresAt": "2024-09-21T12:15:00+03:00",
"rate": 60,
"currency": "RUB",
"status": "ACTIVE",
"paymentStatus": "PAID"
}
Розпізнавання номерного знака через камеру
Введення номера вручну—поганий UX. Зручніше—розпізнавання через камеру. На iOS використовуємо Vision + VNRecognizeTextRequest:
import Vision
func recognizePlate(from pixelBuffer: CVPixelBuffer) {
let request = VNRecognizeTextRequest { [weak self] request, error in
guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
let candidates = observations.compactMap { $0.topCandidates(1).first?.string }
let plateRegex = /[АВЕКМНОРСТУХ]{1}\d{3}[АВЕКМНОРСТУХ]{2}\d{2,3}/
let plate = candidates.compactMap { $0.firstMatch(of: plateRegex)?.0 }.first
DispatchQueue.main.async {
self?.vehiclePlateField.text = plate.map(String.init) ?? ""
}
}
request.recognitionLevel = .accurate
request.recognitionLanguages = ["ru-RU"]
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer)
try? handler.perform([request])
}
На Android аналогічно через ML Kit Text Recognition:
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
val image = InputImage.fromMediaImage(mediaImage, rotationDegrees)
recognizer.process(image)
.addOnSuccessListener { visionText ->
val plateRegex = Regex("[АВЕКМНОРСТУХ]{1}\\d{3}[АВЕКМНОРСТУХ]{2}\\d{2,3}")
val plate = visionText.textBlocks
.flatMap { it.lines }
.mapNotNull { plateRegex.find(it.text)?.value }
.firstOrNull()
vehiclePlateField.setText(plate)
}
Точність розпізнавання російських номерів на якісних знімках—близько 85–90%. Для підвищення точності додатково застосовуємо OpenALPR через серверний API для складних випадків.
Таймер сесії та push-повідомлення
Таймер зворотного відліку—найчастіше запитуваний UI-елемент. На iOS він живе в ProgressView + Timer, на Android в CountDownTimer. Але фонові повідомлення—через APNs/FCM.
Логіка відправки повідомлень на серверу:
- За 15 хвилин до
expiresAt→ push «Парковка вичерпується через 15 хвилин» - За 5 хвилин → push з кнопками «Продовжити на 1 годину» / «Завершити»
- У момент
expiresAt→ push «Парковочна сесія завершена»
На iOS кнопки у push-повідомленні реалізуються через UNNotificationCategory:
let extendAction = UNNotificationAction(
identifier: "EXTEND_PARKING",
title: "Продовжити на 1 годину",
options: [.foreground]
)
let stopAction = UNNotificationAction(
identifier: "STOP_PARKING",
title: "Завершити",
options: [.destructive]
)
let category = UNNotificationCategory(
identifier: "PARKING_EXPIRING",
actions: [extendAction, stopAction],
intentIdentifiers: []
)
UNUserNotificationCenter.current().setNotificationCategories([category])
Натиснення «Продовжити» з повідомлення—додаток відкривається на екрані продовження та автоматично ініціює платіж збереженою карткою без зайвих кроків.
Інтеграція з парковочним обладнанням
Якщо парковка керується СКУД або шлагбаумом, інтеграція ведеться через серверний API оператора. Розповсюджені протоколи: SOAP/XML (legacy-системи), REST JSON (сучасні). Додаток не спілкується з обладнанням безпосередньо—лише через бекенд.
Для відкриття шлагбаума через QR-код на виїзді використовуємо AVCaptureSession:
let session = AVCaptureSession()
guard let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device) else { return }
session.addInput(input)
let output = AVCaptureMetadataOutput()
session.addOutput(output)
output.setMetadataObjectsDelegate(self, queue: .main)
output.metadataObjectTypes = [.qr]
session.startRunning()
Платіжний flow
Оплата парковки зазвичай реалізується двома сценаріями:
Pre-paid—покупуємо час до в'їзду. Користувач вибирає зону, час, платить. Сервер видає код сесії. На в'їзді оператор сканує QR або читає номер.
Post-paid—оплата при виїзді. Сесія починається автоматично при в'їзді (за номером), сума розраховується при виїзді, додаток пропонує оплатити.
Для обох варіантів використовуємо збережену картку через токен провайдера (CloudPayments, YooKassa, Stripe). Розовий платіж без збереження картки—через платіжний веб-віджет в WKWebView/WebView. Регулярні платежі (підписка на парковочний абонемент)—через recurring payments з токеном.
Стек та архітектура
| Компонент | iOS | Android |
|---|---|---|
| UI | SwiftUI + UIKit (камера) | Jetpack Compose + CameraX |
| Розпізнавання номерів | Vision Framework | ML Kit Text Recognition |
| Карти | MapKit / Google Maps SDK | Google Maps SDK |
| Платежі | Stripe iOS SDK / CloudPayments | Stripe Android SDK / CloudPayments |
| Push | APNs через Firebase | FCM |
| Архітектура | MVVM + Combine | MVVM + StateFlow |
Ориентири по терміни
Базова версія (сесії, таймер, оплата карткою, push): 4–6 тижнів. Додавання розпізнавання номерів, інтеграції з обладнанням, абонементів—ще 2–4 тижні. Вартість розраховується індивідуально після аналізу вимог.







