dev.to article #28 (paste-ready) — Building 2 SwiftUI apps in parallel: TipJar Now + HabitHash dual launch (8-week plan)
Calendar: 2026-05-29 09:00 PT (1 week after dev.to #27)
Tags:#ios #swift #swiftui #indie #productivity
Word count: 2000 (technical + reuse pattern + lessons learned)
Series: "Day 60 indie iOS dev" (part 4)
TL;DR
I built two iOS apps in parallel: TipJar Now (one-tap QR code tip jar for service workers) and HabitHash (pseudonymous habit tracker with git-like commit hash). Same SwiftUI / StoreKit 2 / @observable pattern, copy-paste-modify ~80% of code from existing apps. Each MVP is ~8 Swift files, ~600 lines total.
Here's the engineering approach + reuse pattern + 8-week plan.
Why 2 apps in parallel (instead of 1)
Conventional indie wisdom: ship one thing, focus, iterate. But:
- Marginal cost is near zero when you reuse 80% of code (IAPManager, SettingsView, paywall, OnboardingView all identical)
- 2 apps = 2 ASO slots + 2 keyword landings. Apple Search shows me on twice as many queries
- Cross-promo loop: footer in TipJar Now points to HabitHash and vice versa
- Data-rich: with 2 launches at once, I can compare conversion / retention / refund across niches faster than sequential
Risk: 2x review cycle, 2x rejection probability (per app, independent). Mitigated by the pre-flight checklist (see my 14 Rejection Reasons 1-pager).
The reuse pattern
After 4 iOS apps, I have a stable shared pattern:
<App>/
├── App/<App>App.swift # @main + state injection (~15 LOC)
├── IAP/IAPManager.swift # StoreKit 2 wrapper (~90 LOC, identical)
├── Models/<Domain>.swift # Domain-specific (varies)
├── Services/<DomainStore>.swift # @Observable + persist (varies)
└── Views/
├── ContentView.swift # Main UI (varies)
├── <Edit>View.swift # Add/edit form
├── PaywallView.swift # IAP unlock UI (~120 LOC, mostly identical)
├── SettingsView.swift # Premium / About (~50 LOC, identical)
└── OnboardingView.swift # 3-screen tutorial (~80 LOC)
~80% reusable:
- IAPManager.swift: copy-paste, change
premiumProductID - PaywallView.swift: copy-paste, change feature list
- SettingsView About section: copy-paste, change Privacy Policy URL
- OnboardingView: copy-paste, change copy
~20% domain-specific:
- Models (Habit vs TipMethod vs Choice vs Event)
- Store (HabitStore vs TipJarStore)
- ContentView (grid vs QR display vs wheel)
- Domain validation logic
TipJar Now: domain models
Service workers (waiters / bartenders / drivers) need to display QR code on demand for one-tap tip transfers. Multi-method support (PayPal / Venmo / WeChat / PayPay).
enum TipMethodKind: String, Codable, CaseIterable {
case paypal, venmo, wechat, alipay, paypay, linePay
case cashApp, zelle, revolut, wise
var displayName: String { /* localized */ }
var symbol: String { /* SF Symbol icon */ }
}
struct TipMethod: Identifiable, Codable {
let id: UUID
var kind: TipMethodKind
var addressOrLink: String // PayPal email / Venmo handle / QR URL
var displayName: String?
var qrImageData: Data? // For WeChat / PayPay (custom QR)
var paymentURL: URL? {
// Build payment URL from kind + addressOrLink
}
}
QR generation uses CoreImage:
enum QRGenerator {
static func image(from string: String) -> UIImage? {
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(string.utf8)
filter.correctionLevel = "M"
guard let output = filter.outputImage else { return nil }
let scaled = output.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
guard let cg = context.createCGImage(scaled, from: scaled.extent) else { return nil }
return UIImage(cgImage: cg)
}
}
ContentView shows a single big QR card with method switcher in toolbar.
Free vs Pro:
- Free: 1 method
- Pro $1.99: unlimited methods + Apple Watch + custom themes + custom QR upload
HabitHash: domain models
Pseudonymous habit tracker. No account, no cloud (free). 5 habits max in free, with a "git-like" hash signature for visualizing your commit pattern.
struct Habit: Identifiable, Codable {
let id: UUID
var name: String
var emoji: String
var colorID: String
var commits: Set<Date> // Days committed
var hashSignature: String {
// Format: 4 hex digits (total commits) + 2 hex (last 30 days) + 2 hex (current streak)
let count = commits.count
let recent = commitCount(lastDays: 30)
let streak = currentStreak()
return String(format: "%04x%02x%02x", count, recent, streak)
}
func currentStreak() -> Int {
var count = 0
let cal = Calendar.current
var day = cal.startOfDay(for: Date())
while didCommit(on: day) {
count += 1
guard let prev = cal.date(byAdding: .day, value: -1, to: day) else { break }
day = prev
}
return count
}
}
The hash signature is a fun differentiator — feels like git activity for habits. Programmer-targeted ICP loves this.
ContentView shows:
- List of habits with name + emoji + streak
- 7-day commit grid (last 7 days, green if committed)
- Single tap to commit today
Free vs Pro:
- Free: 5 habits, 30-day history, 7-day grid
- Pro $2.99: unlimited habits + unlimited history + iCloud sync + widgets + Apple Watch
ICE scoring (why these 2 over others)
I had 10 iOS app candidates. Why these 2?
| App | Impact | Conf | Ease | ICE |
|---|---|---|---|---|
| TipJar Now | 8 | 9 | 9 | 6.48 |
| HabitHash | 9 | 8 | 7 | 5.04 |
| PromptVault iOS | 9 | 8 | 7 | 5.04 |
| FocusBlock | 8 | 8 | 7 | 4.48 |
| WaterNow | 7 | 8 | 8 | 4.48 |
TipJar Now wins ICE because:
- High Impact: real pain point for service workers (no current iOS solution)
- High Confidence: user testing shows clear value ("I'd pay $1.99 once for this")
- High Ease: 1 main screen + 1 form + 1 paywall + Apple Watch = ~80 hr build
HabitHash wins ICE because:
- High Impact: programmer-niche has high LTV (and high paywall conversion)
- High Confidence: GitHub graph aesthetic is universally loved
- Medium Ease: heatmap visualization + iCloud sync add complexity
8-week parallel plan
W1: TipJar Now skeleton (60 hr) | HabitHash skeleton (50 hr)
W2: TipJar Apple Watch | HabitHash heatmap visualization
W3: TipJar widget | HabitHash widget
W4: TipJar polish + onboarding | HabitHash onboarding + iCloud sync
W5: Localization (en/ja/zh) | Localization (en/ja/zh)
W6: Beta + bug fix | Beta + bug fix
W7: ASC submit (TipJar) | ASC submit (HabitHash)
W8: Apple review (both) | Both go live + content launch
Total: 4 weeks of mostly parallel work + 4 weeks of staggered launches. Both ship by Week 8.
What's hard
App Store Review at scale: each app's review may take 24-72 hr. Submitting 2 simultaneously means 2x reject probability. Mitigation: use my 14 Rejection Reasons checklist before submitting.
ASO competition: "tip jar" has low competition (good), "habit tracker" has high (bad). HabitHash needs niche differentiation: "pseudonymous habit tracker no account" (very specific keyword).
Cross-promo balance: too aggressive (footer banner on every screen) feels spammy. Recommended: footer banner only in Settings + Onboarding screen 3.
Code reuse cost: when you find a bug in IAPManager, you fix it in 4-5 places. Mitigation: extract to shared package (SPM local) eventually. For 4-app MVP, copy-paste is fine.
What's easy (because of 4 prior apps)
- ASC navigation: I now know exactly where Bundle ID / IAP / Pricing / Privacy / Submit pages are
- StoreKit 2 wiring: 90-line IAPManager copy-paste
- Paywall design: 120-line PaywallView with feature list update
- Localization: Same 3 languages (en/ja/zh-Hans), reuse phrases like "Unlock everything forever"
Anti-pattern: don't overcomplicate
Things I DIDN'T add to MVP (deferred to v1.1+):
- Multi-currency support (TipJar) — defer
- Heatmap year view (HabitHash) — defer to W2
- AI suggestions for habits (HabitHash) — feature creep
- Backend / cloud sync (both) — defer to Pro tier W4
- Apple Watch independent app (both) — defer to v1.1
- Custom QR image upload (TipJar) — defer to v1.1
- Custom themes (both) — defer
MVP = "minimum viable", not "minimum interesting". Ship the core feature, validate, iterate.
What I'm tracking (Day 30 each app)
TipJar Now:
- Downloads (target 200-500 organic Day 30)
- Conversion to Pro (target 5-8%)
- Top traffic source (Reddit r/bartending? TikTok? service-worker forums?)
HabitHash:
- Downloads (target 300-800 — programmer ICP, broader reach)
- Conversion to Pro (target 7-10%)
- Streak distribution (D7 = how many users have 7-day streak?)
I'll publish Day 30 numbers Substack #23 (likely 2026-06-30 or later).
If both apps clear $200/month sustained, I'll continue Wave 2 (Color Sweep game, ICE 3.92, deferred for higher-cert wins first).
If either fails (< $50/month), I'll kill it and double down on the survivor.
Code samples available
Both MVPs are open scaffolds:
- TipJar Now: github.com/jiejuefuyou/autoapp-tipjar-now (placeholder, public after Day 30)
- HabitHash: github.com/jiejuefuyou/autoapp-habithash (placeholder)
Reuse pattern + IAPManager template: github.com/jiejuefuyou/autoapp-ios-template (placeholder)
See also
- Issue #17: Buyout vs Subscription decision (full math)
- dev.to article #27: TF tester lock debug
- Apple StoreKit 2 — Apple Developer
- SwiftUI Observable — Apple WWDC23
Tags
#ios #swift #swiftui #indie #productivity #app #development #testflight
A/B subject candidates
- "Building 2 SwiftUI apps in parallel: TipJar Now + HabitHash dual launch (8-week plan)"
- "How I built 2 indie iOS apps in 8 weeks (80% code reuse pattern)"
- "Day 30 indie iOS dev: shipping 2 new apps (the parallel reuse pattern)"
Cross-platform plan
| Platform | Date | Variant |
|---|---|---|
| dev.to | 2026-05-29 09:00 PT | This piece |
| Substack | 2026-05-27 (already covered in #18) | n/a |
| X | 2026-05-29 09:30 PT | 8-tweet thread (TipJar + HabitHash demo gifs) |
| Reddit r/iOSProgramming | 2026-05-29 22:00 PT | Excerpt + link |
| HN Show HN | 2026-05-30 06:00 PT | "Show HN: 2 iOS apps in 8 weeks (80% code reuse)" |
| 知乎专栏 | 2026-06-05 09:00 JST | Chinese version |
| 公众号 | 2026-06-07 09:00 JST | Mobile-friendly version |
Top comments (0)