Appendix T: ReportMaker (Shop Manual)
Status: In development, distribution-ready binary. GPL v3. Public repo at github.com/fluhartyml/ReportMaker. Build-Along chapter: Part VII Build-Along 11. ReportMaker is a focused business-expense tracker for one job: capture spending during a trip, then produce a clean PDF the user can email to whoever reimburses them. The teaching value of the app is the bundle — Swift Charts + PDFKit + PhotosUI + SwiftData appearing together in one product instead of being demoed in isolation across four chapters.
What it is
The user creates a Trip with a name, dates, and a purpose. Inside that trip they add Expenses one at a time — date, category, vendor, amount, optional receipt photo. The trip's detail view shows a running total, a breakdown by category as a Swift Charts BarMark, and a per-day spend line when the trip spans more than one day. Tapping Generate Report produces a multi-page PDF (cover sheet + line-item table + one page per receipt photo) and presents the system share sheet so the user can email it.
Apple does not ship one of these. Expensify, SAP Concur, and FreshBooks do it for a monthly fee. ReportMaker is the small, native, no-account-required alternative.
Architecture overview
SwiftUI + SwiftData app with three optional frameworks layered in: Swift Charts (dashboard), PDFKit (report), PhotosUI (receipt capture). Twelve Swift files total.
- App entry:
ReportMakerApp.swift—@mainscene with the SwiftData model container. - Root:
ContentView.swift— trip list, swipe-to-delete, toolbar to add a trip. - Trip detail:
TripDetailView.swift— expense list, running totals, Swift Charts dashboard, Generate Report button. - Add sheets:
AddTripSheet.swiftandAddExpenseSheet.swift—.sheetpresentations with form-style inputs. - Data models:
Trip.swiftandExpense.swift— SwiftData@Modelclasses with cascade-delete relationship. - PDF pipeline:
ReportPDFRenderer.swift—UIGraphicsPDFRenderer+NSAttributedString+CGContextdrawing for the multi-page PDF. - Share:
ShareSheet.swift—UIActivityViewControllerwrapped viaUIViewControllerRepresentable. - About:
AboutView.swift— standard about page (uses the newAboutIconimageset added 2026-05-10 to fix the blank-icon bug). - Under the Hood:
UnderTheHoodView.swift— in-app source browser. - Notes:
DeveloperNotes.swift— canonical reference, structured as an array ofDeveloperNoterecords for in-app display.
Data model
@Model
final class Trip {
var name: String
var startDate: Date
var endDate: Date
var purpose: String
@Relationship(deleteRule: .cascade) var expenses: [Expense]
init(/* ... */) { /* ... */ }
}
@Model
final class Expense {
var date: Date
var vendor: String
var amount: Double
var categoryRaw: String // ExpenseCategory raw String value
var receiptImageData: Data?
var trip: Trip?
var category: ExpenseCategory {
get { ExpenseCategory(rawValue: categoryRaw) ?? .other }
set { categoryRaw = newValue.rawValue }
}
}
enum ExpenseCategory: String, CaseIterable {
case meals, lodging, transit, supplies, entertainment, other
}
The CloudKit-safe enum pattern
Note that Expense stores categoryRaw: String, not category: ExpenseCategory. CloudKit doesn't sync Swift enums directly — it syncs String. The model stores the raw value, and a computed category property wraps the enum back on top for Swift code to read. This is the canonical pattern for any SwiftData-with-CloudKit app that wants enums in its public API but needs the underlying storage to round-trip through CloudKit.
Swift Charts dashboard (Book 17 reference)
Two charts on the trip dashboard, both pure Swift Charts — no third-party library, no canvas drawing, no UIKit fallback:
BarMarkby category — one bar perExpenseCategory, height = sum of expense amounts in that category.LineMarkover time — only shown when the trip spans more than one day. One point per day, value = total spend that day.
The data shape Charts wants is one row per data point. The Trip model exposes totalsByCategory and totalsByDay as computed arrays of tuples that the chart consumes directly via Chart(data, id: \.0).
PDFKit pipeline (Book 17 reference)
ReportPDFRenderer drives a multi-page PDF with UIGraphicsPDFRenderer:
- Cover page — title, dates, purpose, total, category breakdown summary.
- Expenses page — per-line table with date / category / vendor / amount columns.
- Receipt pages — one page per receipt photo at the back.
Each page is drawn with NSAttributedString placement on a CGContext. No HTML-to-PDF conversion, no UIView snapshotting — direct text and image draws. This is the canonical Apple approach for native PDF generation; every "Export PDF" feature in iOS apps reaches for the same building blocks.
Receipt capture — PhotosPicker
Adding a receipt is a PhotosPicker bound to a Data binding. The picker returns a PhotosPickerItem; the code asks for its Transferable data and stores the bytes directly in the Expense model's receiptImageData field. No NSPhotoLibraryUsageDescription string required — PhotosPicker is the user-mediated pattern.
Storing image data inside the model keeps the demo simple. A production-scale app with serious receipt volume would offload images to the file system (or to CKAsset for CloudKit) and hold only a URL on the @Model — image bytes in SwiftData blow up the store fast.
Share pipeline
iOS doesn't yet ship a SwiftUI-native share sheet for arbitrary file URLs, so ShareSheet wraps UIActivityViewController in UIViewControllerRepresentable. Tapping Generate Report writes the PDF to the temporary directory and shows the share sheet, which lets the user email, AirDrop, or save the file to Files.app.
About icon fix (2026-05-10)
The About page used to render a blank space where the app icon should appear. The cause: Image("icon-light-1024") tried to load an asset from AppIcon.appiconset, but Asset Catalog AppIcon images aren't addressable by filename in SwiftUI. The fix: add an AboutIcon.imageset that copies the same light + dark 1024 PNGs and reference it as Image("AboutIcon"). This is the canonical Apple pattern — any image you want to Image(...) from SwiftUI needs to live in its own non-AppIcon imageset.
Build + deployment
- App name: ReportMaker.
- Platforms: iPhone primary. iPad and Mac via Designed for iPad.
- Frameworks: SwiftUI, SwiftData (persistence), Swift Charts (dashboard), PDFKit + UIKit (
UIGraphicsPDFRenderer), PhotosUI (PhotosPicker), MessageUI (feedback). - CloudKit: wired in the model container; the
categoryRawenum pattern is what makes the SwiftData store round-trip cleanly. - Distribution: distribution-ready binary on GitHub. App Store submission is post-shakedown work.
Known gotchas
- AppIcon vs imageset for the About page.
Image("icon-light-1024")fails silently when the source isAppIcon.appiconset. Use an explicitAboutIcon.imagesetinstead. - SwiftData with CloudKit needs raw-value enums. Storing
category: ExpenseCategoryas the persisted field breaks CloudKit sync. StoringcategoryRaw: Stringwith a computed wrapper is the pattern. - Image data in @Model blows up the database. Fine for a demo with one trip and a handful of expenses; a real reimbursement workflow with many trips and many receipts would migrate to file-system or
CKAssetstorage with URLs in the model. - PDF page sizes are hardcoded to US Letter. A future internationalization pass would parameterize this on the user's locale; for now the assumption matches the primary use case.
- Shakedown steps 6–10 are pending. Multi-expense + chart verification, Generate Report → PDF flow, and the delete-to-update behavior are on the testing checklist as of this manual's date.
Sources
- Project source: github.com/fluhartyml/ReportMaker (verified 2026-05-10).
- Canonical roadmap:
ReportMaker/DeveloperNotes.swiftin the project repo. - Build-Along chapter: Part VII Build-Along 11.
- Book 15 cross-reference: SwiftData two-model pattern with relationship + cascade delete.
- Book 17 cross-reference: Swift Charts dashboard (BarMark + LineMark) and PDFKit pipeline.
- Book 06 cross-reference: PhotosPicker user-mediated photo capture.
- Book 14 cross-reference:
UIActivityViewControllershare-sheet wrap.