Missed Part 1? Read SpotShare, Part 1: Idea and Design.

I’m pulling back the curtain on how I architected, designed, and built SpotShare from scratch — with a lot of help from AI.

Quick peek at what it does: SpotShare loads posts around your location, has a more advanced profile with a progress/journey view, supports widgets, and more. Test coverage is close to 90%, which I’m pretty happy about.

One caveat before we dive in: I’m not “vibe coding.” I plan the work, then use AI to help implement it — I don’t let it drive the whole thing. If you use AI more loosely, make sure you’re checking for vulnerabilities, understanding the system, and weighing tradeoffs. Without human review, AI will happily ship risks.

ArchitecturePermalink

Goals were simple: easy to follow, maintainable (it’s mostly just me now, but I want a setup that scales), and testable. MVVM felt like the safe, production‑ready choice. That said, a lot of iOS folks still prefer MV, which honestly surprised me. If you don’t care much about unit tests and want to move fast with SwiftUI, MV can be simpler. But because MV tends to push logic into views, it’s harder to unit test — so for me, MVVM still wins.

The core of the setup is dependency inversion. SwiftUI’s Environment is great for passing dependencies down the tree, and protocols give you clean interfaces. I ended up using the Swift Dependencies library from Point‑Free — it solves the gap where ViewModels can’t access Environment values directly in SwiftUI. You could build something similar yourself with struct‑based injection, but I don’t regret this choice at all.

There’s more to the architecture, but the rest follows fairly standard patterns — nothing wild.

Results (At A Glance)Permalink

  • Nearby feed with efficient location queries and lightweight caching.
  • Profiles with a progress/journey timeline that stays fast as data grows.
  • Widget extensions with minimal duplication by sharing ViewModels and services.
  • Solid reliability: fast launches, smooth scroll, and predictable state.

Module BoundariesPermalink

  • Keep feature code close to its ViewModel and services. Views stay thin.
  • Use protocols for services (LocationService, PostRepository, AuthClient), then provide concrete implementations in the app target.
  • Add a boundary for “platform stuff” (permissions, sensors) so ViewModels only see pure interfaces.

Testing Strategy (Why 90% Feels Natural)Permalink

  • Unit tests focus on ViewModels and services. No UI logic inside Views.
  • Replace dependencies with fakes via DI. Assert behavior, not implementation details.
  • A few integration tests exercise critical flows end‑to‑end.
  • UI tests cover sign‑in, compose, and the nearby feed as smoke tests.

Dependency Inversion DetailsPermalink

  • Use Swift Dependencies to define injectable keys and scoping (app‑wide vs feature).
  • Prefer constructor injection in ViewModels; avoid hidden globals.
  • Treat dependencies as immutable where possible; make lifetimes explicit.

SwiftUI Specifics That HelpPermalink

  • @Observable for simple state; avoid accidental ObservedObject sprawl.
  • Move side effects to ViewModels; Views bind to published state only.
  • Be deliberate with navigation: keep routes in state to enable testability.

Concurrency And CancellationPermalink

  • Use structured concurrency in ViewModels (async/await, task groups when needed).
  • Cancel in‑flight work on state changes (e.g., leaving a screen, new search query).
  • Debounce user‑driven events to keep the UI snappy.

Accessibility, Analytics, PerformancePermalink

  • Labels and traits first; test Dynamic Type on common screens.
  • Minimal analytics with clear events, no PII. Log failures with context.
  • Measure with Instruments occasionally; fix hot paths, not everything.

Working With AIPermalink

Prompting is a skill. The usual flow is: describe the task, let AI draft it, then review. But you’ve probably hit the bump where the first prompt misses the mark or goes off in a weird direction. My go‑tos:

  • Don’t ask AI to do everything on critical tasks. Let it run on simple stuff; keep tighter control on complex parts.
  • Be explicit about goals — what “good” looks like and the priorities (performance, efficiency, correctness).
  • Start with a plan mode. If the plan looks right, execute. Or give it a skeleton and ask it to fill in.
  • Check in as you go. Validate the plan, ask follow‑ups, or cross‑check with another model.

I mainly use Claude Code right now. A couple setup tips: keep your Claude.md short and focused. Don’t paste your entire architecture — just the essentials (e.g., “We use MVVM and Clean Architecture”). Also note the common gotchas you keep correcting (like preferring @Observable over ObservedObject). That’s exactly the kind of thing worth pinning. Keep it lean; keep it useful.

Prompting Patterns That WorkPermalink

  • Spec → Plan → Implement → Review. Ask for a plan first on tricky changes.
  • Rubrics help: “optimize for correctness > performance > speed.”
  • Give examples and non‑examples. It reduces creative drift.
  • Ask for tests or acceptance criteria with the change.
  • Request diffs when editing: easier to review and revert.

SkillsPermalink

When I started SpotShare there weren’t many good SwiftUI skills available, but now the community has solid ones. Grab the good SwiftUI skills and reference them when relevant — it lets you keep Claude.md minimal.

Always ReviewPermalink

Always review AI‑generated code. I often run another AI pass for a code review before mine. It adds a thin layer of security and quality. Sure, it costs some tokens, but you can automate most of it in your PR pipeline or run reviews locally while you do other work. The confidence is worth it.

Happy building — and stay tuned for more SpotShare deep dives.

Like what you're reading?

If this article hit the spot, why not subscribe to the weekly newsletter?
Every weekend, you'll get a short and sweet summary of the latest posts and tips—free and easy, no strings attached!
You can unsubscribe anytime.