Как я решал задачу получения доступа к данным банковских приложений на iOS

Вы пользуетесь кешбэком, может даже выбираете категории повышенного кешбэка каждый месяц? Когда у вас несколько карт разных банков с кешбэком, становится непросто запомнить, какую из них лучше использовать для конкретной покупки в этом месяце.

Можно было бы эти категории записывать на листик или куда-то в заметки, но я решил сделать iOS приложение, в котором можно добавлять выбранные категории кешбэка, а они уже будут выводиться в виде виджета.
Заполнять данные о категориях вручную мне показалось неудобно, поэтому я начал думать, как упростить пользователю получение и ввод этих данных из банковских приложений. О том, как я решил эту задачу, расскажу в этой публикации.

Задача и варианты решения

Определимся с целью — максимально упростить пользователю ввод данных о выбранных категориях кешбека.
Есть вариант — подключаться по API к банку и забирать оттуда данные о выбранных категориях кешбэка, однако:

  • пользователи вряд ли захотят давать доступ приложению к своему банку

  • безопасная интеграция с большим количеством банков и поддержка таких интеграций — ресурсоёмкая задача

  • банки могут не иметь публичного API или не будут готовы давать к нему доступ

Apple недавно представила FinanceKit, который дает доступ к транзакциям, но сейчас это решение нам не подойдет, в первую очередь из-за того, что оно имеет очень ограниченную поддержку, к тому же я не нашел там данных о кешбеке.

Получить доступ к данным банковских приложений на iOS? Сомнительно. К тому же приложения эти как минимум для банков РФ сейчас часто меняются, какие-то банки просто через Safari открывают веб-версию, короче говоря, не вариант.

Нужно было что-то более элегантное, простое и универсальное. Я смотрел на экраны со списками категорий кешбеков и обратил внимание, что они все очень похожи друг на друга … еще я подумал, что друг на друга похожи кассовые чеки, которые ряд приложений для учета финансов умеют распознавать, упрощая ввод данных о покупках …

удивительное сходство способов выбора кешбэка натолкнуло меня на мысль...

удивительное сходство способов выбора кешбэка натолкнуло меня на мысль…

Если такие экраны имеют схожую структуру, значит их можно пробовать распарсить. Пользователю останется только сделать скриншот нужного экрана и «отдать» его приложению, остальное уже дело техники…

Распознаем категории кешбэка на iOS по скриншоту

Итак, дано изображение (скриншот экрана банковского приложения), нужно вытащить из него строки определенного вида — нас интересуют те, в которых есть число с процентами. С этой задачей может справиться какой-нибудь OCR или модель нейросети, можно было бы взять какое-то API или даже зашить модель в приложение, но хочется сделать просто, безопасно, дешево и приватно, а тогда давайте посмотрим, может что-то у нас есть на iOS.

Есть Vision Framework (доступен с iOS 11.0), про него Apple пишет

Apply computer vision algorithms to perform a variety of tasks on input images and videos. The Vision framework combines machine learning technologies and Swift«s concurrency features to perform computer vision tasks in your app.

Use the Vision framework to analyze images for a variety of purposes:

Recognizing text in 18 different languages

ок, значит собираем прототип и смотрим, как он справится с задачей

Собираем прототип на SwiftUI + Vision

Берем SwiftUI, Vision, а также PhotosUI — чтобы получить доступ к галерее.
Кстати, удобно, что можно сразу указать, что нас интересуют только скриншоты через параметр для PhotosPicker

import SwiftUI
import Vision
import PhotosUI

struct ContentView: View {

    @State private var imageItem: PhotosPickerItem?

    var body: some View {
        PhotosPicker(selection: $imageItem, matching: .screenshots) {
            Text("Chose screenshot to detect categories")
        }
        .onChange(of: imageItem) {
            detectCashbackFromImage()
        }
    }
}

Vision поддерживает 18 языков (вы можете указать список и приоритет поддерживаемых языков через свойство recognitionLanguages) и работает прямо на устройстве. Довольная подробная инструкция по распознаванию текста есть в официальной документации Apple. Работать с текстом Vision может в двух режимах fast и accurate, первый похож на OCR и будет распознавать каждый символ в отдельности, а второй использует нейронную сеть для поиска целых строк, я воспользуюсь именно им.

d60afa4258ccbdc1b68ed105cf93215e.png

Давай напишем функцию, которая должна отработать при выборе скриншота, а также заведем отдельный класс для детектора.

private func detectCashbackFromImage() {
        
        Task {
            if let data = try? await imageItem?.loadTransferable(type: Data.self),
               let image = UIImage(data: data) {
                
                CashbackDetector().recogniseCashbackCategories(from: image) { categories in
                    if categories.isEmpty {
                        /// handle case with no categories found
                    } else {
                        /// handle case when categories found
                    }
                }
            } else {
                print("Failed to load image from gallery")
            }
        }
    }

Функция recogniseCashbackCategories будет отвечать за то, чтобы распознать текст на изображении, а processDecetectedTexts за то, чтобы получить из него кешбэк с процентами.
В ней мы также применим регулярное выражение, чтобы найти строку, содержащую число с процентами, не забываем, что проценты могут записаны по-разному: 5%, 0.5%, 0,5%

class CashbackDetector {
    
    func recogniseCashbackCategories(from image: UIImage, completion: @escaping ([CashbackCategory]) -> Void) {

        guard let cgImage = image.cgImage else {
            return
        }

        let reuqestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
        let request = VNRecognizeTextRequest { (req, error ) in
            guard let observations = req.results as? [VNRecognizedTextObservation] else {
                print("nothing found")
                return
            }
            
            var detectedTexts: [(String, CGRect)] = []
            
            for observation in observations {
                guard let topCandidate = observation.topCandidates(1).first else { continue }
                detectedTexts.append( (topCandidate.string, observation.boundingBox) )
            }
            
            let categories = self.processDecetectedTexts(detectedTexts, in: image)
            
            completion(categories)
        }
        
        request.recognitionLevel = .accurate
        request.usesLanguageCorrection = true
        DispatchQueue.global(qos: .userInitiated).async {
            do {
                try reuqestHandler.perform([request])
            } catch {
                print("recognition handler error \(error.localizedDescription)")
            }
        }
    }
    
    private func processDecetectedTexts(_ texts: [(String, CGRect)], in image: UIImage) -> [CashbackCategory] {
        
        var detectedCategories: [CashbackCategory] = []
        
        for (text, _) in texts {
            
            if let percentRange = text.range(of: #"\d+[.,]?\d*%"#, options: .regularExpression) {
                let percentage = String(text[percentRange])
                let lines = text.components(separatedBy: "\n")      
                let name = lines[0]
                        .replacingOccurrences(of: percentage, with: "")
                        .trimmingCharacters(in: .whitespaces)
                
                /// create new caterory based on detected name and percentage...
                /// add those to detectedCategories[]
            }
        }
        
        return detectedCategories
    }
}

Тестируем получившийся Proof of Concept, на тестовых скриншотах отрабатывает довольно неплохо!

Что делать с иконкой и цветом?

В виджете я показываю не только текст с названием категории кешбека и процент по ней, но также и иконку с цветом. Цвет для категории можно попробовать определить из области, находящейся рядом с распознанным текстом boundingBox, но я пока пошел более простым путем — составил набор наиболее часто встречающихся текстовых категорий, для которых уже предопределил иконку и цвет, для всех остальных выбираю цвет и иконку случайным образом, пользователь может поменять на то, что ему больше нравится.

Собираем в приложение

Для приватности — все данные о выбранных категориях хранятся только на устройстве, использую для этого SwiftData, со SwiftUI очень удобно использовать. Также без сетевых запросов виджет будет более экономный в плане потребления энергии. Иконки беру из встроенной библиотеки SF Symbols. Весит приложение 3–4 Мб.

приложение Cashbacker - виджеты с выбранными категориями повышенного кешбека

приложение Cashbacker — виджеты с выбранными категориями повышенного кешбека

Если хотите проверить в приложении, вот и сам Кешбекер в App Store (iOS 17+), если приложение вам понравится, пожалуйста, оставьте отзыв на 5 звезд в сторе и расскажите друзьям и знакомым.
Хотите помочь с тестированием или зарепортить баг? Пожалуйста, вступайте в группу TestFlight и сообщите оттуда.
В своем Твиттер-е буду рассказывать новости при приложение, подписывайтесь, если интересно.

Всем творческих успехов и интересных задач!

© Habrahabr.ru