Native iOS app · SwiftUI · Designed & built solo
I had never opened Xcode. A week later, I was using the app I built.
- Project
- Second Mind
- Role
- Design & iOS engineering, solo
- Platform
- iOS 17+ · SwiftUI · SwiftData
- Timeline
- 2026 · Ongoing personal project
import WidgetKit
import SwiftUI
/// This was ten. Six of them restated
/// "what's on today" in slightly different
/// shapes … what is left matches what
/// the app is actually for.
@main
struct SecondMindWidgetBundle: WidgetBundle {
var body: some Widget {
TodayWidget()
StreakWidget()
TodayLockWidget()
if #available(iOS 18.0, *) {
CompleteNextControl()
AddReminderControl()
}
}
}



1 sentence
The problem was capture. Say one line and it becomes a real reminder. No forms, no fields
3 triggers
It fires on time, on arriving at a saved place, or on an Apple Health signal. Not just alarms
Every morning
The honest measure for an app built for one user: it's still the first thing I open
I designed it, then I shipped it
Second Mind is a native iOS app written in Swift: a habit, routine, and reminder companion I designed and built for exactly one user: me. It runs on my phone, on-device, with Apple Health and location wired in.
The starting point was genuinely zero. I asked what an iPhone app is even written in, the answer was Swift, and I installed Xcode that day. Inside a week the first version was on my phone and I was using it. What you see below is the third version. The first two are further down the page, unretouched.
It runs on my phone, not in a mockup

That’s a real iOS home screen. The widget is reading live data straight out of the app’s own store: next habit, current streak, how much of today is left.
- SwiftUI
- Every screen written as code. No template, no page builder.
- SwiftData
- Nine on-device models: habits, routines, reminders, memories.
- HealthKit
- Steps, sleep and workouts read as triggers.
- CoreLocation · MapKit
- Geofenced places, so a routine can fire when I get home.
- WidgetKit
- Three home-screen widgets, fed by the app's own store.
- Cloudflare Worker
- Ask Mind's backend: a sentence in, typed JSON out.
178 Swift files across 61 commits, every one of them mine to review.
I used AI as an implementation partner while writing the Swift, but the product decisions, the design system, and the judgment calls below are mine, and I reviewed every change that went in.
The problem
My problem was never motivation. It was capture. The small, recurring things (a supplement, a birthday, a stretch) died between a notes app, a reminders app, and a habit tracker before I ever wrote them down. And the few reminders I did set were blind to context: they fired on a clock, never when I actually arrived somewhere or finished a workout.
I didn't need another productivity app. I needed a second mind: one that captures a thought in a sentence, remembers it, and brings it back at the exact moment it matters.
How my judgment changed
The interesting part of this project isn't the final screens. It's the distance between them. I rebuilt the whole app three times. Same person, same tools; what changed was judgment about hierarchy, visual weight, and when the app should act on its own.
Three designs, one app
Same person, same tools. Each screen captured from the Simulator at that version’s own commit.
MVP
First build
Stock SwiftUI, default components, empty states. The AI tab was still a “coming soon” placeholder.



Routine Pro (dark)
First rebuild
First real design system, but a heavy, dashboard-led dark theme that competed with the content for attention.



Current
Shipping today
A calmer light system built on tokens. Clear morning hierarchy, health context in view, and AI folded into existing routines.



The MVP leaned on defaults and empty states. The dark rebuild gave me a real system but fought the content for attention. The current version is quieter on purpose: a clear morning hierarchy, health context in view, and the AI folded into routines I already had instead of a separate "chat" tab.
The decision that shaped the app
Every capture flow lives or dies on friction. The core call was how Ask Mind should behave when I type or say something like "remind me to take vitamins at 8 AM."
The decision
Ask Mind: commit silently, or propose and let me accept?
- Problem
- Capture dies the moment it costs effort, but a misread sentence that silently becomes a daily 8 AM alarm is worse than not capturing at all.
- Rejected
- Open a form. Parse nothing, just show the create screen with the fields empty. That is the friction that kills capture in the first place.
- Also rejected
- Commit silently. Create the reminder and move on. Fastest possible capture, and every wrong guess turns into a real alarm I have to hunt down.
- Chosen
- Parse into a finished proposal. Every field is already filled (repeat, date, time) and one tap accepts it. Corrections arrive as suggestions, never as empty fields.
- Trade-off
- One extra tap on every capture, in exchange for never cleaning up after the model.




Ask Mind
Our AI intelligence
A photo of a bottle becomes a labelled product, a run-out date, and a routine. Typed, spoken, or snapped. The model proposes. You accept.
- 01
It sees the bottle
Gemini vision reads the label: Vitamin D3, 2000 IU, 90 capsules.
- 02
It asks one question
One follow-up. Once daily is enough to finish the picture.
- 03
It does the counting
90 capsules, one a day, run out around November 16th.
- 04
It files it away
Add to Morning routine. The reminder lives on-device in SwiftData.
SwiftUI · SwiftData
The reminder stays on the phone.
Cloudflare Worker
One turn, streamed from the edge.
Gemini
Vision plus a fixed JSON schema.
Cloudinary · Firestore
Photo stored. The app keeps the link.
Only a one-line health summary and place names leave the device.
One vocabulary, every screen
Because I was designing and shipping alone, drift was the real risk. So the app runs on one token vocabulary, named the same way in Figma and in code: primitives hold the raw palette, semantic tokens describe intent, and screens are only allowed to use the semantic layer. New screens can't quietly diverge from old ones.
Primitives
amber
50
100
200
300
400
500
600
700
800
900
Input / Focused
Button / Primary · Secondary

MCP
enum Primitive {
static let neutral50 = Color(hex: "#F8F7F4")
static let neutral900 = Color(hex: "#1C1B18")
static let accentDefault = Color(hex: "#F2B84B")
static let streakFlame = Color(hex: "#FF7A1A")
}
enum AppColors {
static let background = Primitive.neutral50
static let buttonPrimaryBg = Primitive.neutral950
static let buttonPrimaryLabel = Primitive.neutral0
static let borderActive = Primitive.accentDefault
}
Same names in Figma and in Swift. Switch the tab to see the tokens, then the button and input that consume them.



The honest outcome
This was built for one user, so I won't dress it up as market validation. The evidence I can actually stand behind is simpler: it's still the first thing I open every morning, and it survived being rebuilt three times because each version was easier to live with than the last.