SkyWatch — Interview Talking Points
This file gives you the three things you need to talk about SkyWatch in a real interview: a 30-second pitch, a 3-minute deep dive, and 12 likely questions with senior-level answers.
The 30-second pitch
“SkyWatch is a weather app I shipped to TestFlight, built end-to-end on Apple’s stack — WeatherKit for forecasts, CloudKit private DB for saved locations that sync across devices, MapKit with a scrubbable precipitation overlay, and a Lock Screen widget. The interesting engineering was the cache layer — WeatherKit charges per call beyond 500 K a month, so I built a per-forecast-type TTL cache with coordinate quantization that kept the per-user budget under 8 calls a day. Two-week build, all SwiftUI, Swift 6 concurrency throughout.”
Why it works: the pitch names specific Apple frameworks, gives a concrete engineering challenge, and quantifies the outcome. The interviewer now has three follow-up threads they can pull.
The 3-minute deep dive
If the interviewer says “tell me more about the cache layer”:
“Sure. The constraint was Apple’s pricing — WeatherKit is free up to 500 K calls a month. Cheap, but easy to blow through if you’re naive: every home-screen load is potentially 3 to 4 calls (current, hourly, daily, minute), times 5 saved locations, times pull-to-refresh.
So I built an actor-based cache with three layers. First, per-forecast-type TTLs —
currentWeatherstays fresh 30 minutes;dailyForecastlasts 6 hours. Second, coordinate quantization to about 3 decimal places, so users near the same place share entries. Third, an in-memory + disk split — disk is in a shared App Group container so the widget reads the same cache the main app writes, without ever making its own API call.The widget is the part most people get wrong. Their
TimelineProvidercalls the network directly, which on a cold widget refresh is a 5-second wait that violates Apple’s recommendation. Mine reads from cache only; if the cache is stale beyond 6 hours, the widget shows ‘Tap to refresh’ rather than fake data.The result: empirically, our test users came in around 4 to 6 WeatherKit calls per day. That’s safely inside the budget even if SkyWatch grew to 50 K daily users for free. And the architecture means I can swap WeatherKit out — if a future iOS deprecates it or we add Android, only
WeatherProviding’s implementation changes; everything above the protocol is untouched.“
That’s a 3-minute answer that signals: you understand cost, you’ve made tradeoffs, you’ve gotten the widget pattern right, and you’ve designed for change.
12 questions this capstone earns you
For each, the question is what the interviewer asks; the answer is the senior-level response you should be ready to give.
1. “Why WeatherKit and not OpenWeatherMap or Weather.gov?”
WeatherKit’s free tier is enough for indie scale, it integrates natively (no API key in the binary, no token rotation), and Apple Review prefers first-party APIs. The trade is iOS-16-minimum and Apple Developer Program required — both acceptable for an iOS-only consumer app. If I were going cross-platform, I’d pick weather.gov or NOAA for the US-only case (truly free, government source) and build a fallback layer.
2. “How do you handle WeatherKit being down?”
Two layers. First, the cache layer returns stale data on any error — better stale than blank. Second, a non-modal status indicator at the top tells the user “Last updated 12 min ago” so they know the data isn’t live. I never show a modal error or a spinner that can’t be dismissed. The only fatal path is “user signed out of iCloud” — there I show a full-screen ‘sign in to sync’ message.
3. “Walk me through what happens when I pull to refresh.”
The view sends a refresh event to AppState. AppState rate-limits to once per 5 minutes per location. If allowed, it calls WeatherService.weather(for:) with forceRefresh: true, which bypasses the cache, calls WeatherKit, writes the result to the cache (memory + disk), and returns. AppState publishes the new weather; the view re-renders. If WeatherKit fails, we keep the previous data and show the status indicator. The refresh either succeeds or is a no-op from the user’s perspective — never a half-state.
4. “How do widgets stay fresh without burning the API budget?”
The widget never calls WeatherKit itself. It reads from a shared App Group cache that the main app populates. A BGAppRefreshTask in the main app runs every ~15 minutes (the iOS-managed minimum), refreshes the cache, and the widget’s timeline picks up the new value at its next reload. If the user hasn’t opened the app for hours, the widget shows the last known data with a timestamp, not stale-data-pretending-to-be-fresh.
5. “How would you scale this to 1 million users?”
Two scales to think about. WeatherKit-wise, at 5 calls/user/day × 1 M users = 150 M calls/month. We’d blow Apple’s free tier and pay around $150 K/month in WeatherKit overage if my math is right (Apple charges $49.99 per million calls beyond 500 K). At that point I’d add a server: a thin proxy that fans out one WeatherKit call to many users for the same coordinate bucket. CloudKit-wise, the private DB scales per-user, so it scales automatically; no work needed.
6. “How do you test the cache layer?”
The cache is an actor with a clear protocol-mockable upstream. Tests inject a fake WeatherProviding that records calls, and assert: (a) first call hits upstream; (b) second call within TTL doesn’t; (c) third call after TTL expires hits upstream again. I also have a deterministic clock injected so tests don’t actually sleep. Coverage on WeatherCache is 100% — it’s the highest-risk file in the codebase.
7. “Why CloudKit and not SwiftData with CloudKit, or Core Data?”
For just saved locations — 4 fields, < 100 records per user, no relationships — SwiftData adds machinery I don’t need. Direct CKRecord lets me see and control the conflict resolution policy. SwiftData+CloudKit hides that behind a black box that’s hard to debug when sync misbehaves. I used SwiftData in a different project (FitTrack); here, the raw CloudKit was the right tool.
8. “What’s your CloudKit conflict resolution strategy?”
Last-write-wins on name and order, but I keep an audit log. When a record fetch returns a server change tag mismatch, I refetch the latest version, compare the user-editable fields, and write back with the resolved values. For ordering specifically — which is the most likely user-visible conflict — I rebuild the order on conflict by sorting all locations by createdAt if the explicit order field is contested. I documented this in ADR-002.
9. “How do you handle the WeatherKit call budget at the architectural level?”
Three layers. (1) Caching with per-type TTL. (2) Coordinate quantization to about 3 decimal places. (3) A monthly counter, surfaced via a metrics debug panel I can pull up to spot regressions. The counter is keyed by call type so I can attribute spikes — a single regression where minuteForecast started fetching on every scroll could blow the budget; the counter catches it within a day.
10. “Tell me about a bug you fixed.”
Widget extension memory limit is 30 MB. Mine was crashing on launch with no useful log. Took 3 hours to track down — I was loading the entire Weather Codable from disk for every entry, including all forecasts. Refactored the shared cache to expose a WidgetCache projection that only loads the fields the widget needs. Memory dropped to 8 MB, no more OOM. The lesson: widget extension RAM is generally about a third of the main app’s, treat it as a strictly bounded environment.
11. “Why no analytics?”
I value the Privacy Nutrition Label more than I value funnel data on a capstone project. If SkyWatch were a real product with subscription monetization, I’d add a thin first-party analytics layer logged to my own server — not Firebase, not Mixpanel, because I’d rather report “we don’t share data with anyone” honestly than “we collect for app functionality only” with an asterisk. For a portfolio project, that asterisk isn’t worth the friction.
12. “Walk me through your CI/CD.”
GitHub Actions, macos-14 runner. On every PR: SPM resolve, build, run unit + UI tests, SwiftLint, fail on any error. On merge to main: same, plus fastlane beta lane — match pulls the cert from a private repo, gym builds, pilot uploads to TestFlight with skip-waiting-for-processing. Build numbers auto-increment via agvtool. Average green build is about 18 minutes. If I were doing this commercially I’d cache the SPM build folder more aggressively, which would cut that to about 8.
Red-flag answers to avoid
If the interviewer asks “why did you build this,” don’t say “to learn WeatherKit.” Say: “to build a serious portfolio piece that demonstrates I can integrate multiple Apple SDKs in a coherent product with real engineering tradeoffs.” The first answer is a tutorial; the second is a project.
If they ask “how long did it take,” don’t say “a weekend.” Even if technically true, it makes the work sound trivial. Say: “Two weeks of focused work, plus ongoing polish — the implementation guide is in the repo.”
Now go build the next one: Capstone 2 — FitTrack.