Writing

Building an iOS app with no accounts and no data collection: lessons from Mood Share

Why Mood Share keeps every check-in on the phone with SwiftData, how sharing works with no server behind it, and what that promise costs in return.

Mood Share is a small iOS app for telling the people close to you how you are feeling before you walk through the door, and for keeping a private record of your moods over time. It has no sign-up screen, no backend, no usage analytics and no third-party SDKs, and its App Store privacy section is one sentence long: “The developer does not collect any data from this app.” This article is about the decisions behind that sentence, the trade-offs they forced, and why I think keeping everything on the device should be the default for any app that handles personal data.

I’m Wayne George, a software engineer in London. By day I build control systems, and through Frontier Consulting I build custom software and AI systems for UK businesses. Mood Share is one of the two apps I publish on the App Store; it went up in May 2024, it is free, and its source is public on GitHub, so nothing here has to be taken on trust. Public to read, that is: there is no licence file in the repository, so it is source available rather than open source.

What the app actually does

A check-in is a card, and filling one in takes three screens. The first is the score: a wheel from one to ten, each number paired with a face. The second is headed “Select words” and offers a list of twenty-eight, running from Happy, Hopeful and Calm through to Anxious, Lonely and Overwhelmed; you tap as many as fit. The third asks for the “Reason for your score”, in your own words. The finished card shows the score, the words and the reason, and the card is what you share. Cards collect on the Home tab, are charted over the past 7 or 30 days under Analytics, and are kept in an Archive you can scroll back through. A daily reminder nudges you if you have not logged anything.

That is the whole feature set. The app has two jobs: letting a partner or a family member know your mood before you get home, and tracking patterns you might want to reflect on or take to a therapist.

Why on-device only

Start with the data. The app records how a person feels, every day, in their own words. That is about as personal as data gets, and the honest question is not “how do I protect this on my server?” but “why is it on my server at all?”

I could not find a reason. Neither of the app’s jobs needs a copy of the data anywhere except the phone, and once I accepted that, a great deal of work simply disappeared: no user database, no password reset flow, no data subject access requests, no breach that could ever include Mood Share, no server bill, and no privacy policy that needs a lawyer. The policy is a few lines saying the app collects no personal data, because it does not.

Apple’s rules push the same way. Guideline 5.1.1 of the App Store Review Guidelines says: “If your app doesn’t include significant account-based features, let people use it without a login.” Most apps treat that as a hurdle to argue past. I treated it as the design.

An account is also a promise: that data will follow someone to a new phone, survive a reinstall and be recoverable when they forget a password. Each of those is a system you have to build and then operate for years. No account, no promise.

SwiftData instead of a backend

The app is written in SwiftUI with SwiftData for storage, which is why it requires iOS 17 or later. SwiftData sits on top of Core Data’s persistence and lets you declare your model layer with macros, which removes most of the boilerplate that used to make local storage feel like more effort than calling a REST API.

The entire schema for Mood Share is one class. The initialiser, which only supplies defaults, is omitted here:

@Model
class Card: Identifiable {
    var date: Date
    var complete: Bool
    var score: Int
    var words: [String]
    var positives: String
    var liked: String
    var toShare: String
}

The reason you type on the third screen lands in toShare. Two of the other fields, positives and liked, appear only in the model and in the sample data used for previews; no screen in the app reads them. An unused column in a local store costs nothing, so they stay.

The app attaches a container at the root with .modelContainer(for: Card.self), views read cards with @Query, and a new check-in is a modelContext.insert(...). There is no repository layer, no networking code and no cache invalidation: the store is the source of truth, and it lives in the app’s own sandboxed container on the phone.

Two things about that store matter. The first is that being on the device buys you encryption you did not have to write. In Apple’s own description, “every time a file on the data volume is created, Data Protection creates a new 256-bit key (the per-file key) and gives it to the hardware AES Engine, which uses the key to encrypt the file as it’s being written to flash storage” (Apple Platform Security). The second is that a SwiftData store can be synced through iCloud, but only if you opt in: the framework “requires two separate capabilities to perform automatic iCloud sync: the iCloud capability, which lets you configure CloudKit, and the Background Modes capability, which lets your app receive remote notifications from CloudKit” (Syncing model data across a person’s devices). Mood Share adds neither; the project has no entitlements file at all. That was a choice, and I come back to it below.

The reminders follow the same pattern: local notifications scheduled on the device with a calendar trigger (UserNotifications), so there is no push server and no device token to store. The only other persistent state is two values in UserDefaults: the appearance setting and the date you last logged a mood.

What “sharing” means without a server

The name sounds as though it needs a network: accounts, a contacts list, and a server that pushes your mood to your partner’s phone. That design would have needed everything I had just avoided.

Instead, sharing in Mood Share is an image. When you tap Share, the app takes a snapshot of the card as it appears on screen and hands it to the standard iOS share sheet, UIActivityViewController. From there it goes wherever you already talk to that person: Messages, Mail, or any messaging app you have installed. The social network destinations are excluded from the sheet, because a mood card is meant for one person, not a feed. The recipient does not need the app or an account, and nothing passes through anything I run. Showing a therapist how a month has gone works the same way: you show them the phone, or you send them the cards.

Rendering the card is the least glamorous code in the project: a small UIView extension that opens an image context and calls UIKit’s drawHierarchy(in:afterScreenUpdates:) to snapshot the view into it. SwiftUI’s ImageRenderer has been in the framework since iOS 16, so it was there from the first line of an app that requires iOS 17; it would have done the same job without dropping into UIKit, and I reached for the UIKit route out of habit. The lesson is that “sharing” is very often just “export in a format the other person can already open”; you do not need a protocol when a picture will do. The one bug fix I shipped after launch was to this flow. Version 1.0.2 went out four days after release, and its release note is the whole story: “Bug fix: Initial share attempt failure now fixed.”

The App Store privacy label

Every app on the App Store carries a privacy section, and Mood Share’s is the shortest kind there is: “The developer does not collect any data from this app.” Both of the apps I publish carry that line, NVR Companion as well. It is worth being clear about what it does and does not say.

What it says turns on Apple’s definition of “collect”, which “refers to transmitting data off the device in a way that allows you and/or your third-party partners to access it for a period longer than what is necessary to service the transmitted request in real time”; by the same definition, “data that is processed only on device is not ‘collected’ and does not need to be disclosed in your answers” (App privacy details). So an app can handle deeply personal data and still truthfully carry that line, as long as the data never leaves the phone. That is where Mood Share stays.

What it does not say is that anyone has checked. The information is self-reported, and Apple says so directly: it “is self-reported by the developer” (About privacy information on the App Store). That is one reason the source is public. If you want to know whether the claim is honest, you can read the code and search it for a networking call: every import across the app’s Swift files is an Apple framework, and the Xcode project does not declare a single package dependency. For a mood app, that is a better promise than any policy.

The trade-offs

None of this is free, and the costs deserve stating plainly.

There is no sync. Your cards live on one phone. If you get a new one and do not restore from a backup, the app starts empty.

There is no recovery I can offer. Delete the app and the cards go with it. Lose the phone and the only route back is your own device backup: “iCloud Backup includes app data for the apps you’ve downloaded on your device” (What does iCloud back up?), so a restore brings the store back with it. That is Apple’s mechanism rather than mine, and it depends on backups being switched on. Nobody can email me for their history, because I have never had it.

There is no telemetry. Nothing in the app reports back, so I only find out what is wrong when someone tells me at hello@waynegeorge.uk or leaves an App Store review. That slows improvement down, and I accept it.

Sharing is one-way and manual. Nobody learns you are having a bad day unless you choose to tell them. Some people would prefer that to be automatic; I think manual is healthier, but it is a real difference.

Most of these could be solved with iCloud sync, which SwiftData makes fairly painless and which would keep the data in the user’s own Apple account rather than on a server of mine. I may still add it. But it would have to be opt-in, it would turn a one-sentence privacy answer into something that needs explaining, and it would introduce the first class of bug I cannot reproduce on my own phone. For version one, the simpler promise was worth more than the feature.

Why this should be the default

I am not arguing that every app should work like Mood Share. Plenty are genuinely about collaboration, or about data that only makes sense on a server. I am arguing that for apps whose data is about one person, “on the device, no account, nothing collected” should be where you start, and the burden of proof should sit with every feature that wants to move away from it.

Three reasons. First, the platform now makes it the easy path: SwiftData for storage, Data Protection for encryption, local notifications for reminders, the share sheet for getting things to other people. The convenience used to live on the backend; for personal data it no longer does.

Second, you cannot lose what you do not hold. Every breach notice you have ever received was for data that somebody decided to keep. Not keeping it is the only mitigation that works every time.

Third, trust. A mood tracker asks people for the most honest thing they have. If the truthful answer to “where does this go?” is “nowhere”, they will tell it the truth, which is what makes the app worth using.

More writing