Pet apps are a surprisingly demanding genre. Owners want to log walks, track meals, manage vet visits, and sometimes coordinate care across multiple family members—all in a single interface that feels as friendly as the animal itself. SwiftUI gives us the tools to build these experiences quickly, but without a clear pattern, the code can devolve into a tangle of state variables and inconsistent navigation. This article offers seven reusable patterns, each with a checklist you can apply to your own project. We focus on real-world constraints: offline resilience, accessibility, and the fact that a pet doesn't care about your view model architecture.
1. The Health Checklist Pattern: Why Owners Need More Than a To-Do List
Most pet health tracking apps start as a simple list of tasks: feed the dog, give medication, go for a walk. But owners quickly discover that health management is rarely linear. A single day might include multiple medication doses, a special diet, and a note about unusual behavior. The checklist pattern in SwiftUI must accommodate repeating tasks, one-off reminders, and the ability to mark items as partial or skipped. We built a prototype for a cat owner whose pet required insulin twice daily—the checklist had to distinguish between morning and evening doses, and allow the owner to log the actual blood sugar reading alongside the medication flag.
Core Implementation Strategy
Start with a ChecklistItem model that includes an identifier, a title, a boolean for completion, an optional note, and a time window. Use SwiftUI's List with swipe actions for quick toggles. For repeating items, store a recurrence rule (daily, weekly, custom interval) and compute the next occurrence using Calendar. The key is to separate the template (the checklist definition) from the instance (a specific day's log). We use @State for the current day's items and persist to UserDefaults or Core Data depending on the data complexity.
Common Pitfall: Overcomplicating State
One mistake we see often is storing the entire checklist history in a single array and filtering it on the fly. This works for small datasets but quickly becomes slow when the log spans months. Instead, store completed items in a separate entity with a date stamp, and only load the current day's items into memory. Use @FetchRequest with a predicate to keep the UI responsive.
Decision Checklist
- Will items repeat? If yes, define recurrence rules in the model.
- Do you need partial completion? Use an enum for status (pending, done, skipped, partial).
- How far back should the user browse? Limit history to 90 days unless synced to cloud.
- Should multiple caregivers share the checklist? Then use CloudKit or Firebase for sync.
2. Multi-Pet Profile Switcher: One App, Many Personalities
When an owner has two dogs and a cat, the app needs to switch context seamlessly. The naive approach—a tab bar with one tab per pet—breaks down beyond three animals and doesn't scale to shared features like a combined feed. We needed a pattern that lets the user select an active pet, then see only that pet's data, but also provides a dashboard view for all pets at a glance.
How It Works Under the Hood
Use an ObservableObject called PetStore that holds an array of Pet models and a currentPetIndex. Publish changes via @Published. The main content view observes PetStore and conditionally renders the appropriate detail view. For the global dashboard, create a separate view that iterates over all pets. The switcher itself can be a picker in the navigation bar or a horizontal scroll view with pet avatars. We prefer the scroll view because it feels more tactile and shows multiple pets at once.
Edge Cases
What happens when a pet is deleted? The store must handle index out-of-range gracefully. We set currentPetIndex to 0 if the current index exceeds the array count. Also, if the user has no pets, show an onboarding screen rather than a broken picker. For performance, lazy-load each pet's detail view using LazyVStack inside a TabView with .tabViewStyle(.page)—this gives a natural swipe gesture between pets without loading all data at once.
When to Avoid This Pattern
If the app is primarily about a single pet with occasional guest profiles, a simpler approach is to store the active pet ID in @AppStorage and avoid the store entirely. The multi-pet pattern adds complexity that isn't justified for a single-user, single-pet app.
3. Location-Aware Walk Logger: Offline First, Sync Later
Tracking a dog walk is one of the most common features in pet apps, but it's also one of the trickiest to implement well. Owners expect the route to be recorded accurately, even when the phone loses signal in a park or rural area. The pattern we recommend is an offline-first approach: store GPS points locally as the walk happens, then sync to the server when connectivity returns.
Implementation Details
Use CLLocationManager with allowsBackgroundLocationUpdates set to true. Collect points every 5–10 seconds to balance accuracy and battery life. Store each point as a CLLocationCoordinate2D plus a timestamp in a local Core Data entity. At the end of the walk, compute the total distance using the Haversine formula or Apple's distance(from:) method. For the UI, show a live map using Map with an overlay polyline. When the user ends the walk, attempt to upload the route to a remote server. If the upload fails, keep the route in a pending uploads queue and retry periodically using BGTaskScheduler.
Common Mistakes
- Not requesting background location permission early—users often deny it because the system dialog is vague. Show a custom explanation screen before requesting.
- Forgetting to stop location updates when the walk ends, which drains the battery.
- Storing raw locations without filtering. Apply a simple speed filter: discard points that imply unrealistic movement (e.g., > 40 km/h for a dog).
Offline Sync Checklist
- Store each walk as a separate Core Data entity with a sync status enum (pending, syncing, synced).
- Use
NWPathMonitorto detect connectivity changes and trigger sync. - Provide a manual sync button in settings for users who want control.
- Limit the pending queue size to 50 walks to avoid storage bloat.
4. Medication Reminder with Flexible Scheduling
Medication reminders are a staple of pet health apps, but the scheduling requirements vary wildly. Some medications must be given every 8 hours, others once a day with food, and still others only as needed. The pattern we use is a generic Reminder model with a schedule type enum: fixed interval, daily at time, weekly on days, or ad hoc. Each reminder can have an optional note and a list of past administrations.
Building the Scheduler
Use UNUserNotificationCenter for local notifications. When a reminder is created, compute the next fire date based on the schedule type and schedule a notification. For fixed intervals, use DateComponents with a repeating interval. For ad hoc reminders, don't schedule anything—let the user trigger a notification manually. Store the reminder's next fire date in Core Data so the app can display upcoming reminders even if the user clears notifications.
Handling Missed Doses
If the user doesn't mark a dose as given within a grace period (say, 30 minutes after the scheduled time), the reminder should escalate. We implement a simple escalation chain: first a standard notification, then a critical alert (if permitted) after 15 minutes, and finally a notification to a secondary caregiver if enabled. This requires careful permission handling—critical alerts must be requested separately and are only appropriate for life-saving medications.
Limits of Local Notifications
Local notifications are limited to 64 scheduled notifications per app. If a user has many reminders with daily recurrence, this limit is quickly reached. The workaround is to schedule only the next 7 days' worth of notifications and reschedule when the user opens the app. Alternatively, use a server-side push notification service for unlimited reminders, but that introduces network dependency.
5. Shared Caregiver Journal: Real-Time Updates Without Conflict
When multiple family members care for the same pet, the app needs to support concurrent updates without data loss. A journal that logs feeding times, walks, and notes must handle the case where two people log an event at the same moment. The pattern we recommend is a conflict-free replicated data type (CRDT) approach using CloudKit's record change tokens, but for most apps a simpler last-write-wins strategy with timestamps is sufficient.
Implementation with CloudKit
Create a JournalEntry record type with fields for author, timestamp, and content. Use CloudKit's subscription to push changes to all devices. When the user creates an entry, save it locally first (optimistic UI), then upload to CloudKit. If the upload fails, keep the entry in a pending state and retry. For conflict resolution, compare the server timestamp with the local timestamp—the later one wins. This is not perfect, but for a pet journal the risk of real conflict is low because entries are typically created minutes apart.
Offline Handling
Store all entries in Core Data with a cloudKitID field. When the app comes online, fetch the server's change token and pull new records. For local entries that haven't been uploaded, push them in order. Use NSManagedObjectContext's merge policy to handle conflicts. We set the policy to mergeByPropertyObjectTrump so local changes take precedence for the same record.
When Not to Use This Pattern
If the app is single-user or only one caregiver uses it at a time, a simple local journal with optional export is far simpler. The shared journal pattern adds significant complexity in testing and debugging, especially around conflict resolution. Only implement it if you have confirmed that multiple users will actively log events simultaneously.
6. Pet Weight Tracker with Trend Visualization
Weight tracking is a common feature in veterinary-recommended apps, but the UI often falls short. Owners want to see not just the last weight, but the trend over time, and they want to be alerted when the weight crosses a threshold. The pattern we use is a lightweight charting approach using Swift Charts (iOS 16+) combined with a simple linear regression to show the trend line.
Data Model and Charting
Store each weight entry as a WeightRecord with date and kilograms. Use Chart with PointMark for individual readings and LineMark for the trend. Compute the trend line using the least squares method—this is a few lines of math and doesn't require a third-party library. For the alert, allow the user to set an upper and lower bound. When a new weight is logged, check if it exceeds the bounds and show a warning. We also include a note field so the owner can record context (e.g.,
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!