Skip to content

Repository files navigation

📱 WeatherApp — iOS Client

Native SwiftUI client for the Weather API Aggregator — proves the same backend contract that powers the web client serves a native mobile app too.

Live demo: not published (no App Store account for this project) — can also point at the live backend directly, see How to Run.

One of three clients (Web / iOS / Android) built on top of the same backend. This app talks directly to the Weather API — it never talks to Open-Meteo/OpenWeatherMap directly.

📦 What's Inside

  • 🔎 City search with debounced autocomplete (backend geocoding endpoint) — also used on the Favorites tab now, so a favorite can only be added from a real geocoded suggestion, not free-typed text that the weather-by-name lookup might later fail to resolve
  • 🌡️ Current weather + hourly/daily forecast chart (Swift Charts), with a °C/°F toggle — the hourly chart's default visible window is 12h (up from 7h) plus a non-scrolling "next 24h at a glance" sparkline above it, so a full day's temperatures are readable with far less paging
  • 👆 Tap the weather, sea-conditions or "Mais sobre hoje" cards on the Dashboard for an expanded detail sheet (fuller current-conditions breakdown, the full day's tide events, or a multi-day view of UV/activity/fishing/surf conditions)
  • 🧩 Home-screen widget (WeatherWidget, small + medium) — shows the last weather the app fetched (shared via an App Group), and fetches live GPS weather on its own when that snapshot is missing or older than 30 minutes, so a freshly placed widget shows real data straight away
  • ⚡ Cache badge — "dados frescos" vs "servido da cache há Xs", ticking live from the response's fromCache flag and timestamp
  • 🔁 Fallback banner — appears when the response was served by the secondary provider
  • 🔐 Auth (register/login, JWT in Keychain), favorite cities (add via autocomplete + swipe-to-delete), search history, saved unit preference
  • 🛡️ Admin section (Settings → "Administração", role-gated) — list every account and delete one (swipe-to-delete), except the caller's own
  • ✅ Loading, error and empty states throughout

🛠️ Tech Stack

Swift SwiftUI Swift Charts XCTest

Project generated/managed with XcodeGen (project.yml) rather than a hand-edited .xcodeproj, so the project structure is plain text and reviewable in git.

🏗️ Architecture

WeatherApp-iOS (SwiftUI)
   │  URLSession + Bearer token from Keychain — no BFF, talks to the API directly
   ▼
WeatherAPI (Spring Boot, sibling repo, localhost:8080)
   │  cache (Caffeine) → circuit breaker + retry → provider adapters
   ▼
Open-Meteo / OpenWeatherMap (external providers)
WeatherApp-iOS/
├── Models/          # Codable structs mirroring the backend DTOs exactly
├── Networking/      # APIClient (actor, URLSession), AuthStore (Keychain), KeychainHelper
├── ViewModels/      # one @Observable view model per screen
├── Views/           # Auth, Dashboard (weather card, cache badge, fallback banner, forecast chart,
│                    # + detail sheets), Favorites, History, Settings (incl. admin user list), MainTabView
├── Shared/          # WeatherWidgetSnapshot + App-Group UserDefaults store, compiled into both
│                    # the app and WeatherWidget targets
└── Info.plist       # NSAppTransportSecurity localhost exception (plain HTTP in local dev)

WeatherWidget/       # WidgetKit extension target: small/medium home-screen widget + its own live fetch

Why these choices

  • Direct-to-API, no BFF: unlike the web client (which proxies through Next.js Route Handlers to keep the JWT out of browser JS), a native app's Keychain is already a secure, sandboxed place to hold a token — no XSS surface to defend against, so there's no need for a server-side proxy layer.
  • Local-datetime forecast decoding: hourly[].time/daily[].date come back from the API without a timezone offset (Open-Meteo's timezone=auto already localizes them), so they're decoded as plain Date/DateComponents via a custom formatter instead of .iso8601, which would reject them.
  • XCUITest over manual driving: this environment doesn't have screen-recording permission for computer-use automation, so the golden path (register → search → cache badge flips → favorites → history → settings) is captured as a real, re-runnable XCUITest instead of a one-off manual walkthrough — arguably stronger verification since it re-runs on every future change.
  • Snapshot first, live fetch as a fallback: the widget's TimelineProvider reads a small Codable snapshot the main app writes to a shared App Group container after each successful Dashboard fetch. When that snapshot is missing or older than 30 minutes, the widget fetches the weather for the device's location itself (WidgetWeatherFetcher, using only Foundation/CoreLocation so it doesn't link the auth SDKs) and saves it back to the shared store. It uses an .after(...) reload policy so WidgetKit asks for the next update itself, and the app also schedules a 3-hourly background refresh (WidgetRefreshScheduler). Relying only on the app being opened, or only on BGAppRefreshTask, left the widget stale for hours, since iOS treats both as best-effort.

🚀 How to Run

Prerequisites: Xcode 16+. By default the app talks to the live Weather API deployment at https://weatherapi-4r5x.onrender.com (Render free tier, so the first request after a quiet period can take up to a minute). To use a local backend on http://localhost:8080 instead (see that repo's README), change WEATHER_API_BASE_URL in project.yml (app and widget targets) and run xcodegen generate.

open WeatherApp-iOS.xcodeproj
# ⌘R on the WeatherApp-iOS scheme, any iOS 17+ simulator

Or from the command line:

xcodebuild -project WeatherApp-iOS.xcodeproj -scheme WeatherApp-iOS \
  -destination 'platform=iOS Simulator,name=iPhone 17' build

If project.yml changes, regenerate the project with xcodegen generate.

The WeatherWidget extension has its own scheme, useful for iterating on the widget in isolation (Xcode's widget preview support works from either scheme):

xcodebuild -project WeatherApp-iOS.xcodeproj -scheme WeatherWidgetExtension \
  -destination 'platform=iOS Simulator,name=iPhone 17' build

✅ Tests

xcodebuild test -project WeatherApp-iOS.xcodeproj -scheme WeatherApp-iOS \
  -destination 'platform=iOS Simulator,name=iPhone 17'
  • Unit tests (WeatherApp-iOSTests): model decoding fixtures (including the local-datetime forecast parsing), APIClient error-decoding and token-refresh handling (retry-once on an expired access token, propagating the original error when the refresh token itself is rejected, single-flighting concurrent refreshes) against a mocked URLProtocol, AuthStore's logout-vs-in-flight-refresh race handling (both the resurrection-after-logout case and the newer forced-logout-on-refresh-failure case), cache-age formatting, weather-condition keyword matching, FavoritesViewModel.addFavorite(city:) (success, the 409 duplicate-favorite message, blank-input guard).
  • UI test (WeatherApp-iOSUITests/GoldenPathUITests.swift): drives the real app against a live backend end-to-end — register → search a city → confirm weather + forecast render → search again and confirm the cache badge flips to "servido da cache" → toggle the forecast chart tabs → add a favorite → jump back to it → check history → toggle units in settings.

Given the project's scope (three client apps on one backend), test effort is weighted toward decoding/business-logic and one comprehensive end-to-end flow, rather than unit-testing pure SwiftUI layout.

📝 Notes

  • History entries can be removed individually or cleared together. Favorites support swipe-to-delete.
  • The app uses the live HTTPS API by default. For local development, the simulator can reach a backend at http://localhost:8080 after changing WEATHER_API_BASE_URL.

📄 License

MIT — see LICENSE.


Developed by David Arsénio Martins 🌐 ividi.dev · 💻 github.com/VidiPT89

About

Native SwiftUI client for the Weather API — home-screen widget, cache badge and provider fallback banner, built with Swift Charts and XCUITest.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages