<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged widgetkit</title>
<link>https://coderlegion.com/tag/widgetkit</link>
<description>Powered by Question2Answer</description>
<item>
<title>From Barcode to PDF: A Private, Offline Medication Tracker with VisionKit, WidgetKit and PDFKit</title>
<link>https://coderlegion.com/32273/from-barcode-pdf-private-offline-medication-tracker-with-visionkit-widgetkit-and-pdfkit</link>
<description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bestlatestapps.com/blog/from-barcode-to-pdf/&quot;&gt;https://bestlatestapps.com/blog/from-barcode-to-pdf/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Scanning a box to add a medication, a SwiftData model shared with widgets, adherence statistics, a doctor-ready PDF report, and CSV/JSON export that keeps everything on the device&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bestlatestapps.com/assets/blog/from-barcode-to-pdf/track-your-pills-icon-512.jpg&quot; alt=&quot;Track Your Pills app icon&quot;&gt;&lt;/p&gt;
&lt;p&gt;I have two medication apps on the App Store. The first, &lt;strong&gt;Daily Pills | Reminder&lt;/strong&gt;, does one thing: remind you on time and count your supply. I've written about its notification engineering separately. The second, &lt;strong&gt;Track Your Pills&lt;/strong&gt;, came out of what people asked for next: &quot;Can I just scan the box?&quot; &quot;Can I show my doctor what I've actually taken?&quot; &quot;Can I see today's doses without opening the app?&quot;&lt;/p&gt;
&lt;p&gt;Track Your Pills launched in February 2023. Its listing covers medications added &quot;with photos, barcodes, or manual entry&quot;, tablets, capsules, syrups and injections, adherence calendars and statistics, PDF reports to &quot;share with healthcare providers&quot;, home-screen widgets, CSV and JSON export with backup and restore, and a firm privacy line: &quot;All data stored locally on your device… No account creation needed.&quot;&lt;/p&gt;
&lt;p&gt;This article covers the parts that are &lt;em&gt;different&lt;/em&gt; from a plain reminder app. Those are barcode capture with VisionKit, a SwiftData model the app and its widgets can share, adherence maths, a PDF report built with &lt;code&gt;UIGraphicsPDFRenderer&lt;/code&gt;, and export and import. I'm leaving out the notification side on purpose, because the Daily Pills article covers it. API names and availability are checked against Apple's documentation, and I describe the approach I'd use rather than the app's exact source.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-model-shared-by-the-app-and-its-widgets&quot;&gt;1. The model, shared by the app and its widgets&lt;/h3&gt;
&lt;p&gt;The app targets iOS 17.6 and later, so SwiftData is available. The model separates &lt;em&gt;what&lt;/em&gt; the medication is from &lt;em&gt;what happened&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import SwiftData
import Foundation

@Model
final class Medication {
    var name: String
    var form: String                 // &quot;tablet&quot;, &quot;capsule&quot;, &quot;syrup&quot;, &quot;injection&quot;
    var strength: String?            // &quot;500 mg&quot;, &quot;5 ml&quot;
    var barcode: String?
    @Attribute(.externalStorage) var photo: Data?
    var quantityRemaining: Double    // Double: syrups are measured in ml
    var dosePerIntake: Double
    var scheduleTimes: [DateComponents]
    var startDate: Date
    var endDate: Date?
    @Relationship(deleteRule: .cascade, inverse: \Intake.medication) var intakes: [Intake] = []

    init(name: String, form: String, quantityRemaining: Double, dosePerIntake: Double,
         scheduleTimes: [DateComponents], startDate: Date = .now) {
        self.name = name; self.form = form
        self.quantityRemaining = quantityRemaining; self.dosePerIntake = dosePerIntake
        self.scheduleTimes = scheduleTimes; self.startDate = startDate
    }
}

@Model
final class Intake {
    enum Status: String, Codable { case taken, missed, skipped }
    var scheduledAt: Date
    var loggedAt: Date?
    var statusRaw: String
    var medication: Medication?

    var status: Status { Status(rawValue: statusRaw) ?? .missed }

    init(scheduledAt: Date, status: Status, loggedAt: Date? = .now) {
        self.scheduledAt = scheduledAt; self.statusRaw = status.rawValue; self.loggedAt = loggedAt
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@Attribute(.externalStorage)&lt;/code&gt; keeps photos out of the main database file. Each intake records both the &lt;em&gt;scheduled&lt;/em&gt; time and the &lt;em&gt;logged&lt;/em&gt; time, which lets the report distinguish &quot;taken on time&quot; from &quot;taken two hours late&quot;.&lt;/p&gt;
&lt;p&gt;Widgets run in a separate process, so the store has to live somewhere both processes can reach. That's an &lt;strong&gt;App Group&lt;/strong&gt; container:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;enum SharedStore {
    static let appGroup = &quot;group.com.example.trackyourpills&quot;

    static func makeContainer() throws -&amp;gt; ModelContainer {
        let config = ModelConfiguration(
            &quot;Medications&quot;,
            groupContainer: .identifier(appGroup),   // same file for app and widget extension
            cloudKitDatabase: .none                  // &quot;No cloud sync required&quot;
        )
        return try ModelContainer(for: Medication.self, Intake.self, configurations: config)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the App Groups capability to both the app and widget targets with the same identifier. &lt;code&gt;cloudKitDatabase: .none&lt;/code&gt; is the explicit, privacy-preserving choice. SwiftData otherwise looks for a CloudKit container if the iCloud capability is present.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-adding-a-medication-by-scanning-its-barcode&quot;&gt;2. Adding a medication by scanning its barcode&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;DataScannerViewController&lt;/code&gt; (VisionKit, iOS 16+) gives you a live camera view that recognises text and machine-readable codes, with highlighting and guidance built in. You don't need to build a capture session.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import SwiftUI
import VisionKit

struct BarcodeScannerView: UIViewControllerRepresentable {
    var onCode: (String) -&amp;gt; Void

    static var canScan: Bool {
        DataScannerViewController.isSupported &amp;amp;&amp;amp; DataScannerViewController.isAvailable
    }

    func makeUIViewController(context: Context) -&amp;gt; DataScannerViewController {
        let scanner = DataScannerViewController(
            recognizedDataTypes: [.barcode(symbologies: [.ean13, .ean8, .upce, .code128, .dataMatrix])],
            qualityLevel: .balanced,
            recognizesMultipleItems: false,
            isHighFrameRateTrackingEnabled: false,
            isHighlightingEnabled: true)
        scanner.delegate = context.coordinator
        try? scanner.startScanning()
        return scanner
    }

    func updateUIViewController(_ controller: DataScannerViewController, context: Context) {}

    func makeCoordinator() -&amp;gt; Coordinator { Coordinator(onCode: onCode) }

    final class Coordinator: NSObject, DataScannerViewControllerDelegate {
        let onCode: (String) -&amp;gt; Void
        private var delivered = false
        init(onCode: @escaping (String) -&amp;gt; Void) { self.onCode = onCode }

        func dataScanner(_ scanner: DataScannerViewController,
                         didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
            guard !delivered else { return }
            for item in addedItems {
                if case .barcode(let code) = item, let value = code.payloadStringValue {
                    delivered = true
                    scanner.stopScanning()
                    onCode(value)
                    return
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practical notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Check both flags.&lt;/strong&gt; Apple's docs say &lt;code&gt;isSupported&lt;/code&gt; requires a device with the &lt;strong&gt;A12 Bionic chip or later&lt;/strong&gt;. &lt;code&gt;isAvailable&lt;/code&gt; additionally reflects camera permission and restrictions. When either is false, fall back to manual entry; never show a dead button.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add &lt;code&gt;NSCameraUsageDescription&lt;/code&gt;&lt;/strong&gt;, for example: &quot;Scan the barcode on your medication box to fill in its details.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick the symbologies you need.&lt;/strong&gt; Retail packaging is mostly EAN/UPC, and many pharmaceutical packs carry a GS1 DataMatrix. A narrower list means faster, more reliable recognition.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;from-code-to-name-an-on-device-lookup&quot;&gt;From code to name: an on-device lookup&lt;/h4&gt;
&lt;p&gt;The listing mentions &quot;Auto-fill from extensive medication name database&quot; and &quot;Barcode scanning for instant medication lookup&quot;, and also says &quot;No internet connection required&quot;. The approach that satisfies both is a &lt;strong&gt;bundled, read-only lookup table&lt;/strong&gt;: barcode → name, form and strength. Then fall back to the user's own history: if they've scanned this code before, reuse what they typed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;struct MedicationCatalog {
    struct Entry: Decodable { let name: String; let form: String; let strength: String? }
    private let byBarcode: [String: Entry]

    init() {
        let url = Bundle.main.url(forResource: &quot;catalog&quot;, withExtension: &quot;json&quot;)!
        byBarcode = (try? JSONDecoder().decode([String: Entry].self, from: Data(contentsOf: url))) ?? [:]
    }

    func lookup(_ code: String) -&amp;gt; Entry? { byBarcode[code] }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Always show the result as an editable form, never a silent auto-save. A wrong strength is a dangerous error.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-adherence-the-numbers-behind-the-calendar&quot;&gt;3. Adherence: the numbers behind the calendar&lt;/h3&gt;
&lt;p&gt;&quot;Visual adherence calendar&quot;, &quot;weekly and overall adherence statistics&quot; and &quot;per-medication adherence metrics&quot; all come from one calculation: of the doses that were &lt;strong&gt;due&lt;/strong&gt;, how many were &lt;strong&gt;taken&lt;/strong&gt;?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import Foundation

struct AdherenceStats {
    let due: Int
    let taken: Int
    let late: Int        // taken, but more than the grace window after schedule
    var rate: Double { due == 0 ? 1 : Double(taken) / Double(due) }
}

enum Adherence {
    static let lateThreshold: TimeInterval = 60 * 60     // 1 hour

    static func stats(for intakes: [Intake], in interval: DateInterval) -&amp;gt; AdherenceStats {
        let due = intakes.filter { interval.contains($0.scheduledAt) &amp;amp;&amp;amp; $0.status != .skipped }
        let taken = due.filter { $0.status == .taken }
        let late = taken.filter { ($0.loggedAt ?? $0.scheduledAt)
            .timeIntervalSince($0.scheduledAt) &amp;gt; lateThreshold }
        return AdherenceStats(due: due.count, taken: taken.count, late: late.count)
    }

    /// One value per day, for the calendar heat map.
    static func daily(for intakes: [Intake], days: Int, calendar: Calendar = .current)
        -&amp;gt; [(day: Date, rate: Double?)] {
        let today = calendar.startOfDay(for: .now)
        return (0..&amp;lt;days).reversed().compactMap { offset in
            guard let day = calendar.date(byAdding: .day, value: -offset, to: today),
                  let next = calendar.date(byAdding: .day, value: 1, to: day) else { return nil }
            let s = stats(for: intakes, in: DateInterval(start: day, end: next))
            return (day, s.due == 0 ? nil : s.rate)          // nil = nothing scheduled: grey cell
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two definitions you have to choose deliberately, and should explain on screen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Skipped is not missed.&lt;/strong&gt; If a doctor said &quot;skip today&quot;, that dose shouldn't hurt the adherence score. Exclude skipped doses from &lt;em&gt;due&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No dose scheduled ≠ 100%.&lt;/strong&gt; A day with nothing due should render as empty, not as a perfect green square.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Swift Charts' &lt;code&gt;RectangleMark&lt;/code&gt; makes the calendar heat map straightforward, with &lt;code&gt;foregroundStyle(by:)&lt;/code&gt; binned into three or four colour bands.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-a-doctor-ready-pdf-with-uigraphicspdfrenderer&quot;&gt;4. A doctor-ready PDF with UIGraphicsPDFRenderer&lt;/h3&gt;
&lt;p&gt;&quot;Generate detailed medication reports… Share reports with healthcare providers… Printable format for medical records.&quot; &lt;code&gt;UIGraphicsPDFRenderer&lt;/code&gt; (iOS 10+) turns drawing code into PDF data. A report is just text and table rows drawn page by page:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import UIKit

struct ReportRow { let name: String; let schedule: String; let adherence: String; let remaining: String }

enum PDFReport {
    static func make(title: String, period: String, rows: [ReportRow]) -&amp;gt; Data {
        let page = CGRect(x: 0, y: 0, width: 595, height: 842)          // A4 in points
        let margin: CGFloat = 40
        let format = UIGraphicsPDFRendererFormat()
        format.documentInfo = [kCGPDFContextTitle as String: title,
                               kCGPDFContextCreator as String: &quot;Track Your Pills&quot;]
        let renderer = UIGraphicsPDFRenderer(bounds: page, format: format)

        let titleAttrs: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 20)]
        let headAttrs: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 11)]
        let bodyAttrs: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 11)]
        let columns: [CGFloat] = [margin, 230, 360, 460]

        return renderer.pdfData { ctx in
            var y: CGFloat = 0

            func newPage() {
                ctx.beginPage()
                y = margin
                (title as NSString).draw(at: CGPoint(x: margin, y: y), withAttributes: titleAttrs)
                y += 30
                (period as NSString).draw(at: CGPoint(x: margin, y: y), withAttributes: bodyAttrs)
                y += 28
                for (i, h) in [&quot;Medication&quot;, &quot;Schedule&quot;, &quot;Adherence&quot;, &quot;Remaining&quot;].enumerated() {
                    (h as NSString).draw(at: CGPoint(x: columns[i], y: y), withAttributes: headAttrs)
                }
                y += 20
            }

            newPage()
            for row in rows {
                if y &amp;gt; page.height - margin - 40 { newPage() }       // paginate
                for (i, text) in [row.name, row.schedule, row.adherence, row.remaining].enumerated() {
                    (text as NSString).draw(in: CGRect(x: columns[i], y: y,
                                                       width: (i &amp;lt; 3 ? columns[i + 1] : page.width - margin) - columns[i] - 8,
                                                       height: 32), withAttributes: bodyAttrs)
                }
                y += 34
            }

            let footer = &quot;Generated on this device. Not medical advice; review with your clinician.&quot;
            (footer as NSString).draw(at: CGPoint(x: margin, y: page.height - margin),
                                      withAttributes: [.font: UIFont.italicSystemFont(ofSize: 9)])
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To preview it, wrap &lt;code&gt;PDFView&lt;/code&gt; (PDFKit, iOS 11+) in a &lt;code&gt;UIViewRepresentable&lt;/code&gt;, set &lt;code&gt;document = PDFDocument(data: data)&lt;/code&gt; and &lt;code&gt;autoScales = true&lt;/code&gt;. To share or print, write the data to a temporary file named something human, like &quot;Medication report – Oct 2026.pdf&quot;, and hand the URL to &lt;code&gt;ShareLink&lt;/code&gt;. The system share sheet includes Print, Mail and Files, so you don't need any extra UI for &quot;printable&quot;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-widgets-that-read-the-shared-store&quot;&gt;5. Widgets that read the shared store&lt;/h3&gt;
&lt;p&gt;&quot;Home screen widgets for quick medication view… Upcoming medication reminders… Adherence at a glance.&quot; The widget's timeline provider opens the same App Group container and computes the next doses:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import WidgetKit
import SwiftData
import SwiftUI

struct NextDoseEntry: TimelineEntry {
    let date: Date
    let upcoming: [(name: String, at: Date)]
    let todayRate: Double?
}

struct NextDoseProvider: TimelineProvider {
    func placeholder(in context: Context) -&amp;gt; NextDoseEntry {
        NextDoseEntry(date: .now, upcoming: [(&quot;Vitamin D&quot;, .now)], todayRate: 1)
    }

    func getSnapshot(in context: Context, completion: @escaping (NextDoseEntry) -&amp;gt; Void) {
        completion(makeEntry(at: .now))
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline&amp;lt;NextDoseEntry&amp;gt;) -&amp;gt; Void) {
        let entry = makeEntry(at: .now)
        // Refresh just after the next dose time, or in an hour if nothing is due.
        let next = entry.upcoming.first?.at.addingTimeInterval(60) ?? .now.addingTimeInterval(3600)
        completion(Timeline(entries: [entry], policy: .after(next)))
    }

    private func makeEntry(at now: Date) -&amp;gt; NextDoseEntry {
        guard let container = try? SharedStore.makeContainer() else {
            return NextDoseEntry(date: now, upcoming: [], todayRate: nil)
        }
        let context = ModelContext(container)
        let meds = (try? context.fetch(FetchDescriptor&amp;lt;Medication&amp;gt;())) ?? []
        let cal = Calendar.current
        let upcoming = meds.flatMap { med in
            med.scheduleTimes.compactMap { comps in
                cal.nextDate(after: now, matching: comps, matchingPolicy: .nextTime)
                    .map { (name: med.name, at: $0) }
            }
        }
        .sorted { $0.at &amp;lt; $1.at }
        .prefix(3)
        let today = DateInterval(start: cal.startOfDay(for: now), duration: 86_400)
        let stats = Adherence.stats(for: meds.flatMap(\.intakes), in: today)
        return NextDoseEntry(date: now, upcoming: Array(upcoming),
                             todayRate: stats.due == 0 ? nil : stats.rate)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the app logs a dose, call &lt;code&gt;WidgetCenter.shared.reloadTimelines(ofKind:)&lt;/code&gt;, so the widget updates now rather than at its next scheduled refresh.&lt;/p&gt;
&lt;p&gt;Keep widget work small. Fetch, compute three items and return. Widgets have tight time and memory budgets.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-export-backup-and-restore-without-a-server&quot;&gt;6. Export, backup and restore without a server&lt;/h3&gt;
&lt;p&gt;&quot;Export data in CSV or JSON formats… Complete backup and restore… Privacy-focused: all data stored locally.&quot; With no account and no cloud, export &lt;em&gt;is&lt;/em&gt; the backup strategy, so treat it as a first-class feature.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import Foundation
import CoreTransferable
import UniformTypeIdentifiers

struct BackupFile: Codable {
    struct Med: Codable {
        let name, form: String; let strength, barcode: String?
        let quantityRemaining, dosePerIntake: Double
        let scheduleTimes: [DateComponents]; let startDate: Date; let endDate: Date?
        let intakes: [IntakeDTO]
    }
    struct IntakeDTO: Codable { let scheduledAt: Date; let loggedAt: Date?; let status: String }

    var version = 1                 // bump when the format changes; migrate on import
    let exportedAt: Date
    let medications: [Med]
}

extension BackupFile: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .json) { backup in
            let encoder = JSONEncoder()
            encoder.dateEncodingStrategy = .iso8601
            encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
            return try encoder.encode(backup)
        }
        .suggestedFileName(&quot;TrackYourPills-backup.json&quot;)
    }
}

enum CSVExport {
    static func intakes(_ meds: [Medication]) -&amp;gt; String {
        func esc(_ s: String) -&amp;gt; String { &quot;\&quot;\(s.replacingOccurrences(of: &quot;\&quot;&quot;, with: &quot;\&quot;\&quot;&quot;))\&quot;&quot; }
        let iso = ISO8601DateFormatter()
        var lines = [&quot;medication,scheduled_at,logged_at,status&quot;]
        for med in meds {
            for i in med.intakes.sorted(by: { $0.scheduledAt &amp;lt; $1.scheduledAt }) {
                lines.append([esc(med.name), iso.string(from: i.scheduledAt),
                              i.loggedAt.map(iso.string(from:)) ?? &quot;&quot;, i.statusRaw].joined(separator: &quot;,&quot;))
            }
        }
        return lines.joined(separator: &quot;\n&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ShareLink(item: backup, preview: SharePreview(&quot;Backup&quot;))&lt;/code&gt; (iOS 16+) then lets the user save to Files, AirDrop it to another device or email it to themselves, all under their control. For restore, &lt;code&gt;.fileImporter(isPresented:allowedContentTypes:onCompletion:)&lt;/code&gt; with &lt;code&gt;[.json]&lt;/code&gt; picks the file. Decode it, check &lt;code&gt;version&lt;/code&gt;, and import inside a single &lt;code&gt;ModelContext&lt;/code&gt; transaction, so a bad file can't leave half a database.&lt;/p&gt;
&lt;p&gt;A design rule I'd hold to: &lt;strong&gt;JSON is the backup, CSV is the report.&lt;/strong&gt; CSV is for spreadsheets and doctors and loses structure; only JSON round-trips. Label them that way in the UI.&lt;/p&gt;
&lt;p&gt;If you use &lt;code&gt;fileExporter&lt;/code&gt; instead of &lt;code&gt;ShareLink&lt;/code&gt;, note that Apple's current docs mark the original &lt;code&gt;fileExporter(isPresented:document:contentType:defaultFilename:onCompletion:)&lt;/code&gt; as deprecated in iOS 27.2, in favour of the overload that adds &lt;code&gt;onCancellation:&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-interaction-warnings-handle-with-great-care&quot;&gt;7. Interaction warnings: handle with great care&lt;/h3&gt;
&lt;p&gt;The listing includes a &quot;Drug Interaction Checker&quot; with &quot;Safety alerts before adding new medications&quot; and &quot;Medical disclaimers and recommendations&quot;. The technical side is simple: a bundled table of known interacting pairs, checked when a medication is added. The responsibility side is not:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Offline data ages.&lt;/strong&gt; Show the data's version date in the warning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Absence of a warning isn't safety.&lt;/strong&gt; Word it as &quot;No interactions found &lt;em&gt;in this app's list&lt;/em&gt;&quot;, never &quot;safe to combine&quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never block, always refer.&lt;/strong&gt; Warn, show the source and recommend checking with a pharmacist or doctor.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I won't publish a table here; the content is the hard part, and it must come from a reliable, licensed source.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-real-world-example-track-your-pills&quot;&gt;8. Real-world example: Track Your Pills&lt;/h3&gt;
&lt;p&gt;All of this describes the approach I'd use, and recommend, for &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/us/app/track-your-pills/id1660782134&quot;&gt;Track Your Pills&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bestlatestapps.com/assets/blog/from-barcode-to-pdf/track-your-pills-screenshot-1.jpg&quot; alt=&quot;Track Your Pills App Store screenshot 1&quot;&gt;&lt;/p&gt;
&lt;p&gt;At the time of writing, its App Store listing shows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free&lt;/strong&gt;, in &lt;strong&gt;Medical&lt;/strong&gt; and &lt;strong&gt;Health &amp;amp; Fitness&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;First released in &lt;strong&gt;February 2023&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;7 ratings&lt;/strong&gt; in the US store, averaging about &lt;strong&gt;4.7 stars&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Supports &lt;strong&gt;iOS 17.6 and later&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How the listing maps to this article:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;What the listing says &lt;/th&gt;&lt;th&gt;Where it's covered&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&quot;Add medications with photos, barcodes, or manual entry&quot; / &quot;Barcode scanning for instant medication lookup&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;DataScannerViewController&lt;/code&gt; plus a bundled lookup, with manual fallback (section 2)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Support for tablets, capsules, syrups, and injections&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;form&lt;/code&gt; and a &lt;code&gt;Double&lt;/code&gt; quantity in the model (section 1)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Visual adherence calendar… Weekly and overall adherence statistics&quot; &lt;/td&gt;&lt;td&gt;Due vs taken, skipped excluded (section 3)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Generate detailed medication reports… Share reports with healthcare providers&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;UIGraphicsPDFRenderer&lt;/code&gt;, &lt;code&gt;PDFView&lt;/code&gt; preview, &lt;code&gt;ShareLink&lt;/code&gt; (section 4)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Home screen widgets… Upcoming medication reminders… Adherence at a glance&quot; &lt;/td&gt;&lt;td&gt;App Group SwiftData store read by a &lt;code&gt;TimelineProvider&lt;/code&gt; (sections 1, 5)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Export data in CSV or JSON formats… Complete backup and restore&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;Transferable&lt;/code&gt; JSON backup, CSV report, versioned import (section 6)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;All data stored locally… No cloud sync required… No account creation needed&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;cloudKitDatabase: .none&lt;/code&gt;, no server, user-controlled export (sections 1, 6)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://bestlatestapps.com/assets/blog/from-barcode-to-pdf/track-your-pills-screenshot-2.jpg&quot; alt=&quot;Track Your Pills App Store screenshot 2&quot;&gt;&lt;/p&gt;
&lt;p&gt;The reminder side of the listing (pre-alarms, quiet hours, escalating reminders) is local-notification work. The patterns in my Daily Pills article, deterministic identifiers and the 64-pending-notification budget, apply directly, and escalation makes that budget matter even more.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-before-you-ship-a-medication-tracker-a-checklist&quot;&gt;9. Before you ship a medication tracker: a checklist&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scanning degrades gracefully.&lt;/strong&gt; Unsupported devices and denied camera access both lead to manual entry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scanned data is always reviewed by the user&lt;/strong&gt; before it's saved.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The store lives in an App Group&lt;/strong&gt;, and widgets read it without crashing on first launch, before any data exists.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adherence definitions are explicit&lt;/strong&gt;: skipped vs missed, late threshold, and days with nothing due.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The PDF paginates&lt;/strong&gt;, has a title, a date range and a disclaimer, and opens cleanly in Files and Mail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The JSON backup round-trips.&lt;/strong&gt; Export, delete the app, reinstall, import: every record comes back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backups are versioned&lt;/strong&gt;, and import is all-or-nothing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy claims are literally true&lt;/strong&gt;: no CloudKit, no analytics surprises, and accurate App Privacy details.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interaction warnings are worded carefully&lt;/strong&gt; and dated, and always refer the user to a professional.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;A medication tracker becomes trustworthy through small, careful pieces: input that's fast but always confirmed, a clear model of what happened, statistics with honest definitions, and reports and exports that keep the user in control of their own data. VisionKit, SwiftData, WidgetKit and a PDF renderer cover nearly all of it, without a single server.&lt;/p&gt;
&lt;p&gt;Try it, or explore my other apps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Track Your Pills (iOS):&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/us/app/track-your-pills/id1660782134&quot;&gt;https://apps.apple.com/us/app/track-your-pills/id1660782134&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Daily Pills | Reminder (iOS):&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/us/app/daily-pills-reminder/id1510203960&quot;&gt;https://apps.apple.com/us/app/daily-pills-reminder/id1510203960&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All of my free iOS apps:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/us/developer/alok-singh/id1508795536&quot;&gt;https://apps.apple.com/us/developer/alok-singh/id1508795536&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;My portfolio:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://bestlatestapps.com/alok/&quot;&gt;https://bestlatestapps.com/alok/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you've built health features with no backend, how did you handle backups? Tell me in the comments.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;— Alok Kumar Singh&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://bestlatestapps.com/blog/from-barcode-to-pdf/&quot;&gt;https://bestlatestapps.com/blog/from-barcode-to-pdf/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32273/from-barcode-pdf-private-offline-medication-tracker-with-visionkit-widgetkit-and-pdfkit</guid>
<pubDate>Sat, 24 Oct 2026 12:40:29 +0000</pubDate>
</item>
</channel>
</rss>