A minimal PDF reader for macOS. Native SwiftUI + PDFKit — no Electron, no bundled runtime, ~1 MB binary.
./build.shProduces build/Leaf.app (ad-hoc signed). Install it with:
cp -R build/Leaf.app /Applications/Requires macOS 15+ and the Xcode command line tools.
There are no binary releases. Leaf is signed ad-hoc rather than notarised, so a downloaded build would be refused by Gatekeeper with the misleading message "Leaf is damaged and can't be opened" — building it yourself avoids that entirely. If you do end up with a downloaded copy, clear the quarantine flag:
xattr -d com.apple.quarantine /Applications/Leaf.appThe two test documents are generated rather than committed, so a fresh clone has none. Recreate them with:
swift Resources/makesample.swift Sample.pdf # has a table of contents
swift Resources/makenotes.swift LectureNotes.pdf # has none, for the detectorEvery shot uses the generated sample documents above, never a real book.
Reading — tabs, thumbnails, and the workspace switcher in the toolbar, under the Soft White light setting.
Split — two documents side by side, each pane with its own tabs and its own scroll position. The contents panel follows whichever pane has focus.
Search — whole-document search with the surrounding sentence for each match and word-level highlighting on the page.
Candlelight, two-page — the warmest of the four light settings. Light and layout are remembered per workspace.
Reading mode (⇧⌘R) — sidebar, tabs and toolbar gone, the page running to
the window edge. Esc brings them back.
Detected structure — a document with no table of contents of its own, with its headings recovered from the text and filterable by kind.
Workspaces
A workspace is a saved reading environment: the documents you have open for a subject, which pane each one sits in, the page you left each of them on, and how they are being displayed. Switch workspaces and the whole setup comes back — "Electrodynamics" might be a textbook beside your lecture notes in a side-by-side split, while "Linear Algebra" is one book in two-page sepia.
Workspaces update themselves as you read, so there is nothing to save. Switch
from the toolbar, from View ▸ Workspace, or from the welcome screen; ⇧⌘N
starts a new one.
Documents open as tabs, one bar per pane. Open one with the + on the tab
bar, ⌘O, or by dropping a PDF onto the window. Drag a tab to the other
pane's bar to move it there, or reorder it within its own. A tab's position is
what tells you which pane it's in — there is nothing else to read.
The window is split exactly when the second pane has tabs. ⌥⌘S sends the
current tab across to start a split; closing the last tab on that side ends it.
Side-by-side vs. stacked is remembered per workspace — use the chevron beside
the split button, or View ▸ Arrangement.
Reading
- Three page layouts: single page, continuous scroll, and two-page spread (with the cover shown alone, as in a real book)
- Fit width, fit page, actual size, and free zoom
- Light: Daylight, Soft White, Warm White or Candlelight — a warmth ramp rather than one sepia setting, since how much is comfortable depends on the display, the room and the time of day
- Text selection and copy
- Reading mode (
⇧⌘R): sidebar, tabs and toolbar all go, leaving the page and nothing else.Escbrings them back - Split view (
⌥⌘S): two independently scrolling panes, side by side or stacked, each with its own document — a textbook beside its solutions, or a paper beside your notes. Each pane fits and zooms itself. Right-click any thumbnail, contents entry, bookmark or search result and choose Open in Split Pane to park it beside what you're reading — the point being to keep a theorem, a figure or a definition in view while you read the text that refers to it
Navigating
- Sidebar with four panels — thumbnails, contents, bookmarks, and search results — all following the tab you are reading
- Detected contents for documents that ship no outline, which is most
lecture notes and preprints. Leaf reads the text for the labels a technical
document prints anyway —
Theorem 4.1,Definition 2.3,4.2 Boundary conditions,Chapter 7— and builds the table of contents that should have been there. Filter it by kind to get just the theorems, or just the exercises. It is heuristic and says so: when a document has a real outline, that one is shown and the detected list sits behind a second tab - Bookmarks:
⌘Dmarks the current page, auto-named after the page's first line of text and renameable. Bookmarked pages show a marker on their thumbnail, and the whole set is reachable from Go ▸ Go to Bookmark - Page box in the toolbar accepts either the printed page label (roman numerals included) or a plain page number
- Full-document search with per-match context, word-level highlighting on the page, and next/previous stepping
- Recent documents, each reopening on the page you left it
Elsewhere
- Drag a PDF onto the window to open it
Leaf some.pdfworks from a shell; so does double-clicking in the Finder
⌘O |
Open |
⌘N |
New window |
⇧⌘N |
New workspace |
⌘D |
Add / remove bookmark |
⌘F |
Search |
⌘G / ⇧⌘G |
Next / previous match |
⌘↑ / ⌘↓ |
Previous / next page |
⌘1 / ⌘2 / ⌘0 |
Fit width / fit page / actual size |
⌘+ / ⌘- |
Zoom in / out |
⌃Tab / ⌃⇧Tab |
Next / previous tab |
⌥⌘S |
Split view |
⇧⌘R |
Reading mode (Esc to leave) |
⌘B |
Toggle sidebar |
⌥⌘R |
Show in Finder |
Sources/Leaf/LeafApp.swift |
App entry, menu bar, file-open routing |
Sources/Leaf/ReaderModel.swift |
Per-window state, workspace binding, search |
Sources/Leaf/Workspace.swift |
Workspace model and storage |
Sources/Leaf/Pane.swift |
One pane: its tabs, position and outline |
Sources/Leaf/TabBar.swift |
Per-pane tab bar, with drag between panes |
Sources/Leaf/PDFController.swift |
PDFKit bridge (paging, zoom, highlights) |
Sources/Leaf/ReaderWindow.swift |
Window layout and toolbar |
Sources/Leaf/Sidebar.swift |
Thumbnails, contents, bookmarks, search |
Sources/Leaf/ThumbnailStore.swift |
Lazy background thumbnail rendering |
Sources/Leaf/Bookmarks.swift |
Bookmarks, tracked across renames and moves |
Sources/Leaf/Recents.swift |
Recent files and resume positions |
Resources/makeicon.swift |
Draws the app icon at build time |
Sources/Leaf/DocumentStructure.swift |
Recovers headings from a document's text |
Resources/makesample.swift |
Regenerates Sample.pdf, the test fixture |
Resources/makenotes.swift |
Regenerates LectureNotes.pdf, an outline-less fixture |
- PDFKit and SwiftUI disagree about who owns a view. SwiftUI calls
makeNSViewmore than once per pane and installs only one of the views it gets, so the controller binds to whichever view reports itself inside a window rather than to the last one created.frameDidChangenotifications never arrive forPDFViewandlayout()is not reliably called, so re-fitting hangs offsetFrameSize. AndlayerUsesCoreImageFilters, which the page tints need, is ignored unless it is set before the view's layer exists — so it is set at construction, not when a tint is chosen. All four are commented at their call sites. - Bookmarks live in preferences, not in the PDF — Leaf never writes to your files. Each record carries a macOS file bookmark alongside the path, so renaming or moving a document keeps its bookmarks: the record is found by that bookmark and re-keyed the next time the file is opened. Records for files that have genuinely gone are left alone rather than pruned, since an unmounted volume looks identical to a deleted file.
- File access goes through
FileAccess/ScopedAccess, which mint and resolve security-scoped bookmarks. The scope is held for as long as aPDFDocumentis cached, because PDFKit reads pages lazily and would otherwise lose access mid-read once the app is sandboxed. ./build.shproduces an ad-hoc signed, unsandboxed app with the hardened runtime.SANDBOX=1 ./build.shadds the App Sandbox entitlements, which is what a store build needs. The sandbox is off by default because a sandboxed app runs against its own container: workspaces from an unsandboxed build start empty, and each document has to be picked once before it can be reopened.Resources/Leaf.entitlementsandResources/PrivacyInfo.xcprivacyexist in case anyone wants to take this to the Mac App Store. That would additionally need an Apple Developer account, distribution certificates and an Xcode project to produce a signed archive. There are no plans to do it.- Window restoration is deliberately disabled; relaunching otherwise resurrects every window from the previous session.
MIT — see LICENSE.
A personal project, written for my own physics and maths reading. Use it freely; no support is promised.





