Rukon
Back to Project Overview
Full Case StudyiOS / Mobile

SpenDrop

SpenDrop: on-device expense capture from Malaysian payment screenshots

SpenDrop
Role
Sole Developer

Product, iOS App & Share Extension, OCR Parser, Data Model, Tests

Affiliation / Client
Personal project
Timeline
September 2026 (not published)
Documentation Level
7 Engineering Sections

Operational Workflow & State Machine

End-to-End Pipeline
  1. 01Share a payment screenshot
  2. 02OCR on the phone
  3. 03Classify amounts and detect provider
  4. 04Check for duplicates
  5. 05Confirm on the review form
  6. 06Save to the shared store
01

Overview

Most everyday payments in Malaysia now end with a confirmation screen: Touch 'n Go, a bank app, DuitNow QR, Apple Pay. SpenDrop turns that screen into an expense. Take a screenshot, share it to SpenDrop, check the form it fills in, and save. Cash spending goes in by hand with Quick Cash.

It is a native iOS app with a Share Extension, written in Swift and SwiftUI with SwiftData for storage and Apple's Vision framework for OCR. Everything runs on the phone: there is no server, no networking code and no third-party package. It runs on the simulator and on the developer's iPhone; it is not on the App Store.

Three iPhone screenshots of SpenDrop: the dashboard with today, week and month totals and provider-tagged rows; the iOS share sheet with SpenDrop in the app row; and the Share Extension review form showing Payment Detected, RM 18.50, paid to McDonald's, category Food, payment Touch 'n Go, 16 September 2026.
From dashboard to share sheet to a filled-in expense. Real app on an iPhone 17 simulator; the shared screenshot is synthetic.
02

Problem

Expense apps ask you to type what your phone already shows you. The payment screen has the amount, the merchant, the provider and a reference number, but copying them by hand is slow enough that most people stop doing it. And a payment confirmation is financial data: sending it to a server to be read is a poor trade for convenience.

03

From screenshot to expense

The Share Extension receives the image from any app's share sheet. Vision reads the text on the phone, on an image downsampled to 1280 pixels to stay inside the extension's memory limit, and the lines are sorted into rows. A rule-based parser then works out what the screen is: which provider, which merchant, which category, the date and time, and the reference number. The extension shows the result as a review form; nothing is saved until the user confirms.

The app and the extension are two separate processes, so they share one SwiftData store through an App Group. An expense saved from the share sheet is on the dashboard the next time the app opens, and the duplicate check in the extension can see everything the app has saved.

Architecture diagram: the Share Extension and the app both feed a shared on-device engine of Vision OCR, amount candidate classification, provider and merchant detection, a duplicate detector and a review screen, which saves to an App Group SwiftData store read by the dashboard, analytics and PayBook.
Two targets compile the same engine and share one store. No server and no network calls.
04

Choosing the right amount

A payment screen rarely has one number on it. A Touch 'n Go confirmation can carry an advert for an RM 450 air conditioner under an RM 18.50 payment; a receipt lists a subtotal, a service charge and a total; a bank screen shows the balance. The parser labels every RM value it finds (total, fee, balance, cashback, discount, advertisement) and chooses from the valid candidates only. When several remain, the review form shows them as one-tap alternatives; when one remains, the row is hidden.

Screens that are not payments at all, such as a balance, a credit limit or reward points, are recognised and are not saved as expenses, or are flagged for review. Sharing the same screenshot twice is caught by its reference number, or by the same merchant and amount on the same day within a 48-hour window, and the user decides whether to add it anyway.

Three iPhone screenshots: a receipt review with RM 19.08 selected as the total and other amounts offered as possible amounts; the Touch 'n Go review with RM 18.50 chosen and no advertisement price offered; and a Possible Duplicate Expense alert for the same transaction reference with Cancel and Add Anyway.
The total wins and the alternatives stay one tap away; the advert's RM 450 is never offered; a repeated share is caught.
05

The rest of the app

Around capture sits a small expense app: a dashboard with today, week and month totals, a searchable and filterable history with edit and delete, analytics by category and payment source built with Swift Charts, and PayBook, a list of frequent bank-transfer payees with account numbers masked by default and a one-tap copy. A settings screen includes a self-test runner that executes the app's 48 parser and persistence checks on the device.

Three iPhone screenshots: analytics with a category donut chart and top category and payment source; the in-app test runner reporting All Tests Passed 48 of 48; and PayBook listing payees with masked account numbers.
Analytics, the in-app test run (48 of 48), and PayBook with masked account numbers.
06

What went wrong, and what it taught

Adverts looked like payments. A Touch 'n Go confirmation can show an RM 450 air-conditioner banner under an RM 22.00 transfer, and any pattern for an RM amount matches both. The fix gives every amount a meaning before any is chosen: advertisement keywords, the words on neighbouring lines, and position, using Vision's bounding boxes, since adverts sit in the bottom of the screen. Layout turned out to be a stronger signal than the text alone.

Interbank transfers name two banks. A CIMB to Maybank receipt mentions both, and the expense belongs to CIMB. Keyword matching alone was ambiguous, so the parser now reads the screen as a document: it collects the banks named after a recipient label first, then reads the from-block, and never attributes a payment to a bank that only appears as the recipient. Apple Pay is treated the same way, as a wrapper around the bank behind it.

Column-aligned receipts still break it. On a synthetic receipt with the label and the amount far apart, Vision returned "TOTAL" and "RM 19.08" as separate observations, the total lost its label, and a line item won. The review screen correctly downgraded to "Possible Expense Detected", and the case is recorded as a known limitation rather than hidden.

07

Testing and verification

The app carries its own test runner: 48 cases covering provider and merchant parsing, amount selection, false positives, duplicates and persistence, provider detection, ten reference screenshots from real layouts, and PayBook. The repository records a run on 25 September 2026 on an iPhone 17 simulator with iOS 27: both targets built and 48 of 48 passed. The suite feeds synthetic OCR text, so it tests the parser, not Vision's recognition quality.

For this write-up the source was checked directly: 42 Swift files, two targets (app and extension), 48 registered test cases, and no networking code. The tests were not re-run here because the machine used runs Windows. There is no XCTest target and no CI.

Every screenshot on this page is from the real app on a simulator. The payment screenshot and the receipt it reads are synthetic images made for testing; PayBook shows the app's built-in sample payees.

What's live, and what isn't

“It exists in the repository” and “it runs in production” are different claims. This is the difference, stated rather than left to be assumed.

  • Built, not liveCapture, parsing, review, duplicates, history, analytics, PayBookWorking on the simulator and, per the repository, on the developer's iPhone. Not published on the App Store or TestFlight.
  • Built, not liveIn-app test suite48 of 48 recorded on 25 September 2026; not re-run for this write-up.
  • Not connectedCamera capture, currency setting, CINot implemented yet: capture is from screenshots and Photos, and all amounts are in RM.

Technical stack

App

SwiftSwiftUISwiftDataSwift ChartsPhotosUI

Capture

Share Extension (UIKit host + SwiftUI)App Group shared containerApple Vision (VNRecognizeTextRequest)

Parsing

Rule-based Malaysian transaction parserMonetary candidate classificationProvider, merchant and category detectionDuplicate detector

Verification

48-case in-app test runnerImage pipeline diagnosticsSimulator builds with xcodebuild

Limitations

  • The parser is keyword-driven: an unfamiliar layout falls back to "Unknown" with low confidence.
  • Column-aligned paper receipts can lose the label on the total, so a line item may be proposed instead.
  • One synthetic screenshot read 9:42 PM as 9:42 AM.
  • Save errors are swallowed rather than shown, and deleting an expense leaves its receipt image on disk.
  • The extension's diagnostic log is not size-capped and contains transaction details.
  • iPhone only, portrait only, English only; no XCTest target and no CI.
  • The repository is private. Access can be granted on request.

What's next

  1. 01Report save errors instead of swallowing them
  2. 02Cap the diagnostic log and keep it to debug builds
  3. 03An XCTest target around the parser suite, run in CI
  4. 04Camera capture and the currency setting
  5. 05Delete receipt images with their expenses

Finished the technical breakdown?

Return to the concise HR overview or discover other projects in the portfolio.