MobilePro #227: Persistence Without the Plumbing
Latest Mobile Dev Insights: iOS, Android, Cross-Platform
Great apps aren’t built on clever features but on reliable foundations.
Persistence isn’t the flashiest part of app development, but it’s one of the first places every serious app eventually arrives. It could be anything you build, whether a notes app, a fitness tracker, or an e-commerce platform, your users expect their data to be available, consistent, and effortless. SwiftData was designed to make that foundation feel like natural Swift instead of a separate persistence framework, reducing the boilerplate without hiding the important concepts.
That idea extends well beyond this week’s tutorial. As AI coding assistants become more capable, developers are discovering that clean architecture and well-organized codebases matter more than ever. This week’s news reflects that shift: from research showing how technical debt limits AI effectiveness to Apple’s efforts to improve security workflows and Google’s continued investment in safer Android experiences. Better tools help, but they’re always built on better foundations.
TL;DR
SwiftData turns ordinary Swift classes into persistent models with a single @Model
ModelContainer owns the persistent store, while ModelContext handles reading, writing, and saving data.
@Query keeps SwiftUI views synchronized with the underlying data automatically.
Not every Swift type is persistable; store raw data and derive richer types when needed.
Sharing a single container across the app ensures every screen works from the same source of truth.
Like AI-assisted development, persistence works best when the underlying architecture stays simple and explicit.
This week’s news corner
Google Play expands Age Signals API for safer, age-appropriate Android apps: Google is rolling out the Play Age Signals API to all Play developers globally, giving Android apps a privacy-preserving way to tailor content and safety features based on a user’s age range without collecting personal information. Parents can manage age sharing centrally through Google Family Link, while developers retain flexibility to implement age-appropriate experiences that suit their apps.
OpenAI pushes back with evidence in Apple trade secrets lawsuit: OpenAI has published a detailed public response to Apple’s trade secrets lawsuit, releasing emails and internal messages that it says contradict key allegations made by Apple. The company argues that the dispute stems from administrative errors—such as misdirected legal emails and Apple’s own offboarding processes—rather than the misuse of confidential information, while maintaining that it neither has nor wants Apple’s trade secrets. For developers, the dispute highlights the increasingly intense competition between Apple and OpenAI as both companies race to build the next generation of AI-powered hardware and software.
Apple caps AI-generated bug reports to keep security pipeline manageable: Apple has introduced limits on the number of security bug reports researchers can submit after a surge of AI-generated vulnerability reports overwhelmed its review process. While AI has helped uncover legitimate security flaws, the flood of low-quality submissions has forced Apple to add submission caps and new review controls to prioritize critical issues.
Messy codebases are limiting the potential of AI coding assistants: A new analysis argues that legacy code, unused dependencies, and accumulated technical debt are becoming major bottlenecks for AI-assisted development, forcing coding assistants to waste context on irrelevant code instead of the task at hand. As AI generates more code, teams risk creating a feedback loop where larger, messier codebases reduce AI accuracy, increase token costs, and slow developer productivity.
A glimpse of BuildWithAI newsletter
Building with AI is quickly becoming part of every developer’s workflow. Each week, Build with AI explores practical AI engineering, agentic development, LLMs, MCP, coding tools, and the techniques shaping modern software development. Here’s a glimpse into a recent featured article:
The knowledge shift – Why judgment is the scarce resource
If the mechanical typing of syntax is no longer the primary bottleneck of software development, what happens to the developer?
Historically, we valued engineers who had deep knowledge of language quirks, standard libraries, and the small, detailed rules of syntax. The developer who could instantly recall the exact argument order for obscure bash commands, or who could write flawless, highly optimized C++ without consulting documentation, was prized as a strong senior engineer. That fluency was real and hard-won.
Today, the cost to generate a syntactically valid loop, a Dockerfile, or a Kubernetes manifest is functionally zero. The LLM is an infinite, instantaneous documentation parser and syntax generator.
💡 Because syntax knowledge has been commoditized, the nature of value within engineering has shifted. The new scarce resource, and the skill that will define the senior engineers of the next decade, is engineering judgment.
Engineering judgment and agency
Judgment is the most visible facet of a larger change. The deeper shift is one of agency: the engineer moves from producing code to owning the decisions.
When you no longer type the implementation, your work is to decide what gets built, how it is shaped, and how tightly or loosely the agent is allowed to run. A simple, well-specified task can run on a long leash, the agent working many steps before you check it. A risky change to a payment path runs on a short one, where you inspect every step.
Agency is the capacity to act and effect outcomes. For the engineer, it rests on three things: competence (the skill to do the work), authority (the standing to make the call), and information (knowing enough to choose well), plus the willingness to act under risk. The same three describe the agent. Its competence is the tools it can call, its authority is the access and permissions it has been granted, and its information is its memory and context. One concept, both sides of the work, human and agentic.
Autonomy is something else, and putting it next to agency surfaces the failure mode that matters. Autonomy is what the engineer actually does on their own, the independent action they take without someone stepping in. The dangerous case is high autonomy paired with low agency: acting independently without the competence, authority, or information to act well. It breaks the same way on both sides. An engineer who acts beyond their competence or authority ships the wrong thing, and an agent granted autonomy without the tools, permissions, or context to succeed does exactly that too.
Judgment is the capability that sits above the codebase. It is the coach’s work of directing play, not the player’s work of executing it.
Engineering judgment in agentic workflow
But what exactly is engineering judgment in an agentic workflow?
This is the vibe-versus-agentic distinction taken one level deeper. The vibe coder pushes a request and accepts whatever comes back; the agentic engineer makes a series of decisions the vibe coder never reaches. Four of those decisions matter most:
Deciding what not to build: The fastest way to take on technical debt is to build a feature you don’t need simply because the AI makes it easy. Judgment is understanding the product requirements deeply enough to reject unnecessary complexity.
Architectural boundary setting: A junior developer can ask an AI to build a logging service. A senior developer with judgment knows exactly how to decouple that logging service from the core business logic via interfaces, ensuring the AI cannot accidentally tightly couple the domains.
SwiftData Features Every iOS Developer Should Know
Persistence is one of those problems every non-trivial app eventually runs into. You build a screen that adds data, another screen that displays it, and then discover they’re not looking at the same data at all — or that everything vanishes the moment the app quits. SwiftData, Apple’s modern persistence framework, was built to solve exactly this, with an API that feels like natural Swift rather than a bolted-on database layer. Here are the core SwiftData features worth knowing.
@Model: Turning a Plain Class into a Persistent One
SwiftData’s starting point is the @Model macro. Annotate any class with it and every stored property automatically becomes persistable — no protocol conformance, no boilerplate mapping code, no separate schema file to keep in sync by hand:
@Model
class Cat {
var date: Date
var rating: Int
var catName: String
}This is what makes SwiftData feel lightweight compared to Core Data: a model is still just a Swift class you write and use normally throughout your app. SwiftData handles the storage plumbing behind the scenes.
Know What Doesn’t Translate Automatically
Not every Swift or SwiftUI type is storable as-is. SwiftUI’s Image type is a common example — it can’t be persisted directly, so a property like this will throw a compile error the moment @Model is applied to the containing class.
The fix is usually to store the underlying Data instead, and expose a computed property for the convenience of working with the rendered type:
var photoData: Data?
var photo: UIImage? {
photoData.flatMap { UIImage(data: $0) }
}It’s a small pattern, but it’s one you’ll reach for often: store the raw, persistable form, and derive the convenient form on demand rather than trying to persist it directly.
ModelContainer and Schema: The Storage Layer
A ModelContainer is the object that actually owns your persistent store. You tell it which model types to manage via a Schema, and it takes care of setting up storage on disk:
let schema = Schema([Cat.self])
let configuration = ModelConfiguration(schema: schema)
modelContainer = try ModelContainer(
for: schema,
configurations: [configuration]
)In practice, this setup lives in one place — often a small dedicated class — and gets attached to the app once, typically in the App struct’s body via the .modelContainer(_:) modifier. From there, every view in the hierarchy can reach the same underlying store.
ModelContext: Where Reads and Writes Actually Happen
If the container is the store, the ModelContext is your workspace for interacting with it. Inserting a new object, deleting an existing one, and saving changes all go through the context:
context.insert(newCat)
try? context.save()
context.delete(existingCat)
try? context.save()Note the pattern: mutate, then save. SwiftData won’t silently persist a change for you — save() is what actually commits it to disk. Forgetting that call is a common source of “my data isn’t sticking around” bugs.
@Query: Fetching and Sorting Without the Boilerplate
Rather than manually fetching data and keeping a local copy in sync, SwiftData views can declare @Query and let the framework do the work:
@Query(sort: \Cat.date) private var cats: [Cat]This single line replaces a @State array, a manual fetch call, and any code you’d otherwise write to keep that array current. The array is always sorted by date and always reflects what’s actually in the store — including changes made from a completely different view. This is also what solves the classic “two screens, two different copies of the data” problem: as long as both screens query the same model type, they’re always looking at the same underlying source of truth.
Sharing the Container Across Views
For operations like insert and delete, views need access to the context itself, not just the queried results. A container class marked @Observable and @MainActor, injected via .environment(_:) at the app level, makes that available anywhere with:
@Environment(DataContainer.self) private var dataContainer@MainActor is worth calling out specifically: it guarantees the container is only ever touched from the main queue, which avoids a category of data-race bugs that used to be easy to introduce with Core Data’s context handling.
A Practical Gotcha: Previews
SwiftUI’s #Preview support doesn’t always play nicely with SwiftData-backed views, particularly ones using @Query and @Environment together. It’s common enough that many SwiftData tutorials — and real projects — simply remove #Preview blocks from affected views and rely on running the app directly (in the simulator or on device) to check UI changes. Worth knowing going in, so it doesn’t look like a bug in your own code.
Wrapping Up
SwiftData’s real strength is how little ceremony it adds on top of code you’d write anyway: a plain class becomes persistent with one macro, a query replaces manual fetch-and-sync logic, and a shared context keeps every screen honest about what’s actually in storage. Once the container is set up and injected once at the app level, the rest of the framework mostly stays out of the way — which is exactly what you want from a persistence layer.
This article is based on SwiftUI For Beginners published by Packt.
📚 Go Deeper
If you’re ready to start building beautiful, native Apple apps, SwiftUI for Beginners provides a practical, step-by-step introduction to SwiftUI, guiding you from your first views to building fully functional iOS applications with confidence.
SwiftUI For Beginners
🤖 Add maps, photos, persistent data, and search to a real iOS application
🛠️ Build a complete CATLog app from scratch, applying each concept as you go
🔀 Test your app with TestFlight and publish it to the App Store
💭 Let’s Talk
What’s one part of app development you wish felt as simple as SwiftData makes persistence?
Reply and let us know.
Advertise with us
Interested in sponsoring this newsletter and reaching a highly engaged audience of tech professionals? Simply reply to this email and our team will get in touch with next steps.





