Inside Dogear’s SwiftUI Architecture

R

Rockindash

Guest
In the last post, I said I was happy to go deep on the tech stack. This is that overview: the APIs Dogear calls, and the logic that turns them into a library.

Building Dogear Taught Me to Validate Before I Ship was about the pivot. Dogear is a personal library. You save a book, you capture a line, and the app brings that line back later.

There is no Dogear server. The phone holds the shelf, the passages, and the notes. The network is only used to find a book, fetch a cover, write an optional summary, handle a purchase, or send a reminder.

It is a native SwiftUI app on iOS 26, with a widget extension and a small notification extension. API keys are injected at build time. They are not in the source.

What lives on the phone​


The bookshelf is stored with SwiftData. One record per saved book: title, authors, ISBN, page count, subjects, a cover pointer, when you saved it, and a reading status. The statuses are to be read, currently reading, and finished.

Everything generated or captured sits beside that, as files on disk. Summaries, a memorable line, short insights, and your own notes are JSON, one file per book. Highlights, quotes, and bookmarked pages are a second folder: a manifest plus the page images. Those files are created when you save a book and deleted when you delete it. Browsing search results does not write anything.

Widgets cannot read the app’s private files. When the shelf changes, the app copies a small snapshot into a shared App Group: the books you can add a passage to, a pool of lines for the random-passage widget, and the cover images those widgets need. The widgets read that snapshot. They do not open the database, and they do not call the network.

Settings stay in local preferences. Unlocked Book Slots are also mirrored to iCloud, which matters later, because an App Store receipt does not name the book you bought a slot for.

Finding a book​


There are four ways onto the shelf: search, barcode, a CSV import, and typing the book yourself.

Search does not trust one catalog. It asks two of them at once and shows results as soon as the first one answers.

Apple Books comes from the iTunes Search API. No key. The request asks for ebooks, in the reader’s country, with a limit of 25 and a five-second timeout. Each hit becomes a book: title, author, year, genres, and the cover artwork. Some storefronts answer a normal title with unrelated public-domain books, so if the local results look off, Dogear repeats the search against the US store and keeps whichever list actually matches the query.

Open Library runs at the same time, against its search API, limited to 20 results and biased toward the language selected in the app. Same five-second timeout. The request sends an identified User-Agent, which is what Open Library asks for if you want a usable rate limit. The response is trimmed to the fields Dogear needs: title, subtitle, authors, publisher, cover, ISBN, ratings, year, and subjects.

Whichever catalog returns first is shown immediately. When the second lands, the two lists are merged. The same work often appears twice, sometimes under a slightly different title. Dogear collapses those by matching a normalized title and the first author, and it strips labels like “illustrated edition” or “movie tie-in” before comparing, so the same book from both catalogs becomes one row. Ranking then pushes down the failure I kept seeing: an exact title that is the wrong book, usually an obscure same-name edition or a stray translation.

Barcode scan is a different camera. AVFoundation reads the ISBN. That code goes to Open Library’s Books API, looked up directly by ISBN rather than by a text search. If Open Library knows it, the book arrives with metadata and a cover. If it does not, the scan counts as a miss and you can still add the book by hand.

CSV import is for a library you already tracked somewhere else, typically a Goodreads-style export. Each row is resolved through Open Library by ISBN first, then by title and author. If nothing matches, the row still becomes a local book. Import should not fail because a catalog has a gap.

Manual entry is the last door. Some books are not in any of these APIs.

The empty shelf is not blank. Open Library’s daily trending list is fetched on launch, cached for twelve hours, and used as decorative covers. If a refresh is slow, the app keeps the stale list. Trending is a backdrop. It is not the product.

Covers​


A shelf of gray rectangles feels broken even when the data is right.

Open Library’s cover service is the first stop, either by the cover id on the record or by ISBN. The URL asks Open Library not to substitute a placeholder. Without that, a missing cover comes back as a tiny successful image, and the app treats a blank as a real jacket. A miss should be a miss, so the next source can run.

If the book came from Apple Books, its artwork URL is rewritten from the small thumbnail to a large one. Apple serves the same file at the size you ask for.

Google Books is the fallback when those covers are missing or too small and the book has an ISBN. Dogear queries the Volumes API for that ISBN and asks only for image links. The list response is usually a thumbnail, so for a large cover it fetches that volume again and takes the biggest image available. It then rebuilds the URL as a high-resolution content image. There is a known trap: Google’s “largest” image sometimes comes back as a big “image not available” graphic, while the thumbnail is the real cover. Dogear keeps both and uses the one that is actually the book. If Google is down, rate-limited, or has no key configured, the app simply stays on the Open Library cover.

Once a real image is on the device, Core Image scales it up with Lanczos and a little sharpening, then caches the result. The shelf and the detail screen ask for different sizes, and a tiny source is not stretched without a limit, because that only makes a larger blur. This does not invent a new cover. It makes the one we already have hold up on screen.

A second pass picks a dominant color from that image. The detail screen and the widgets use it as a tint.

Capturing a passage​


The saved title is not why someone opens Dogear while they are reading. The line is.

From a saved book you can scan a page, pick a photo, or type the line. The Home Screen widget can jump straight into the scanner for a chosen book.

Scanning uses Apple’s document camera, the same kind of capture as Notes. It deskews and crops the page. Dogear does not run a second crop or a custom cleanup pass. If the shot is bad, you retake it. You can capture more than one page in a session.

The next screen is where you point at a sentence. That selection is Live Text, from VisionKit. You drag across the line, or double-tap to grow the selection toward a sentence. If Live Text is not available on the device, the screen says so.

Live Text is what your finger uses. A second recognizer, Vision’s text recognition, runs at the same time. That pass is not what you copy from. It supplies the geometry under the selection, a page number from the header or footer, and a transcript of a bookmarked page so search can find a line you never pinned. Page-number recognition turns language correction off. Correction helps prose and hurts a digit in a margin.

Growing a double-tap into a sentence uses Apple’s on-device language tokenizer. Deciding whether the line is a quote or a highlight is a set of text rules, not a model: quotation marks, words like “said” or “wrote,” a dash before a name, a speaker before a colon. You can change that on the review screen, edit the sentence, name who said it, fix the page number, or throw the capture away.

Save writes the text and the page images. Search and widgets read those saved strings later. They do not run recognition again.

The photographed page never goes to OpenAI. A summary is built from the catalog record, not from the page you scanned.

Bringing a line back​


Three widgets sit on the Home Screen. One opens search or the barcode scanner. One starts a new passage for a book you pick. One shows a random saved line, with the cover and a tint taken from that cover.

Tapping any of them opens the app through a custom dogear:// link: search, scan, add a passage, open a saved line, or the subscription screen. The add-passage widget lists your books from the shared snapshot. It does not query a server to find out what is on the shelf.

The random-passage widget reads that same snapshot. Whether you have Dogear++ is copied into the shared group as a simple flag, so the widget does not have to ask RevenueCat.

The reminder you schedule is a local notification. Daily, weekly, or a custom set of days, at a time you choose, for one book or the whole shelf. The title is the book. The body is the line. The tap opens that passage. Turning reminders off clears the ones still waiting. These notifications are scheduled on the device. They do not wait on a push service.

OneSignal sits next to that. It handles the permission prompt, a notification extension, and a quieter nudge. If reminders are on and you have not opened the library in five days, Dogear sends a custom event with the last book, a short excerpt, and the link back to that passage. Another nudge waits at least seven days. That excerpt leaves the phone on purpose, so the push can quote a line you already saved. The reminder you set yourself never depends on that round trip.

Summaries​


Summaries are optional, and they only exist for a book you have saved.

Dogear calls OpenAI’s Chat Completions API directly. There is no proxy. The key comes from the build configuration. If it is missing, generation fails cleanly.

Two models, for two jobs.

The smaller one, gpt-4.1-nano, writes the summaries and the insights. Summaries come in three lengths: one sentence, a short paragraph, and a plot summary that walks the book in order. For nonfiction, that last style walks the argument in the order the book makes it. Insights are three short lists: what you take away, when the book is worth reading, and who it is for.

The slightly stronger one, gpt-4.1-mini, picks a memorable line. The smaller model was blurring volumes in a series together. This call tries for a well-known line from that exact title, and if it is not confident, it says so and Dogear asks once more for an interesting line instead. The speaker is the character who said it, not automatically the author. If neither attempt is confident, the app shows no quote rather than inventing one.

You can ask for the text in English, Spanish, Brazilian Portuguese, French, or German. Two settings change the instructions rather than the model: spoiler-free, which stays on premise and tone, and simplified language, which shortens the sentences. The plot style may include an ending only when spoiler-free is off.

Every result is cached on that book, separately for each length, language, and those two settings. Opening the book again does not spend another request. Deleting the book deletes the cache.

The app’s own interface is localized in those same five languages. That setting and the summary language are independent.

Dogear++ and Book Slots​


The free shelf is small on purpose. Two books. The carousel layout. Single-sentence summaries. Twenty passages visible per book.

Passages past twenty stay stored. They are hidden until you upgrade, or until you delete a visible one. The paywall does not throw away a line you already captured.

Subscriptions go through RevenueCat and StoreKit. Dogear++ is a monthly or yearly plan. RevenueCat is the source of truth for whether it is active. The app listens for entitlement updates instead of guessing from a cached receipt. That entitlement unlocks the longer summaries, the other shelf layouts, the full library, and the full passage list.

Book Slot is a separate one-time purchase that unlocks a single extra book. The App Store receipt counts how many you bought. It does not say which titles they belong to. So each purchase stamps the book onto the transaction. Restore reads that stamp back. The list of unlocked books is also saved on the device, in iCloud, and on the RevenueCat customer, so a second device can catch up if one of those copies is missing. An unlocked book does not use up one of the two free slots.

What I measure​


TelemetryDeck records product signals: onboarding finished, a book added and how, a scan that failed, a summary generated, a paywall viewed or dismissed, a subscription started. The parameters are things like language and summary style. They are not the text of a passage. Debug builds are allowed to send, so the sessions I generate while building show up next to TestFlight.

A second, local store feeds the stats screen inside the app: summaries this month, scans, failures. A book counts once per month no matter how many times you regenerate its summary. That file never leaves the phone.

Neither system is a user database. There is no account to attach one to.

What I left out​


No account and no sync service. If you delete the app, the passages go with it. Book Slot unlocks can still come back from iCloud. The lines themselves are not stored there.

No generated covers. A wrong cover is worse than a soft one.

No model in the capture path. Recognition is Apple’s, on the device. Quote versus highlight is a rule you can override. The only generation is the optional summary, and it is built from the catalog record.

No catalog calls from a widget. If the snapshot is stale, the widget is stale until the app writes a new one.

Where this sits​


This is the Dogear build in TestFlight for Shipaton 2026, and the one in App Store review.
 

Thread statistics

Created
Rockindash,
Replies
0
Views
2
Back
Top