feat(example): Add Weather sample app#395
Conversation
commit: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 4 issues found in the latest run.
- ✅ Fixed: DTO enum missing snake_case raw values for decoding
- Added explicit snake_case raw values to SolarDayProgressKindDTO enum ('before_sunrise' and 'after_sunset') to match the JSON schema.
- ✅ Fixed: API client and schema disagree on response shape
- Created WeatherLocationsResponseDTO wrapper and updated API client methods to decode the schema-compliant response with 'locations' key.
- ✅ Fixed: SunMiniCard shows "Sunset was" before sunrise occurs
- Updated otherLabel logic to return 'Sunset at' for beforeSunrise case instead of 'Sunset was' since sunset is still in the future.
- ✅ Fixed: Hardcoded Apple development team ID in project
- Removed hardcoded DEVELOPMENT_TEAM from all build configurations and removed machine-specific simulatorId from config.yaml.
Or push these changes by commenting:
@cursor push 865da5b8e9
Preview (865da5b8e9)
diff --git a/example_projects/Weather/.xcodebuildmcp/config.yaml b/example_projects/Weather/.xcodebuildmcp/config.yaml
new file mode 100644
--- /dev/null
+++ b/example_projects/Weather/.xcodebuildmcp/config.yaml
@@ -1,0 +1,13 @@
+schemaVersion: 1
+enabledWorkflows:
+ - simulator
+ - ui-automation
+debug: false
+sentryDisabled: false
+sessionDefaults:
+ projectPath: Weather.xcodeproj
+ scheme: Weather
+ simulatorName: iPhone 17 Pro
+setupPreferences:
+ platforms:
+ - iOS
diff --git a/example_projects/Weather/AGENTS.md b/example_projects/Weather/AGENTS.md
new file mode 100644
--- /dev/null
+++ b/example_projects/Weather/AGENTS.md
@@ -1,0 +1,3 @@
+# AGENTS.md
+
+- If using XcodeBuildMCP, use the installed XcodeBuildMCP skill before calling XcodeBuildMCP tools.
diff --git a/example_projects/Weather/README.md b/example_projects/Weather/README.md
new file mode 100644
--- /dev/null
+++ b/example_projects/Weather/README.md
@@ -1,0 +1,101 @@
+# Atmos Weather
+
+Atmos Weather is a native SwiftUI weather app prototype for iOS.
+
+## Launch with mock weather data
+
+Build and run the app with XcodeBuildMCP first:
+
+```bash
+../../build/cli.js simulator build-and-run
+```
+
+Then relaunch the installed app with the mock API argument:
+
+```bash
+../../build/cli.js simulator launch-app \
+ --bundle-id com.sentry.weather.Weather \
+ --args=--mock-weather-api
+```
+
+## JSON fixtures
+
+Fixture JSON files live in:
+
+```text
+WeatherTests/Fixtures/
+```
+
+Current fixtures:
+
+- `WeatherTests/Fixtures/default-locations.json`
+- `WeatherTests/Fixtures/search-locations.json`
+- `WeatherTests/Fixtures/weather-report-loc-current-san-francisco.json`
+
+## API schemas
+
+OpenAI-compatible API schema files live in:
+
+```text
+Schemas/
+```
+
+Current schemas:
+
+- `Schemas/default-locations.schema.json`
+- `Schemas/search-locations.schema.json`
+- `Schemas/weather-report.schema.json`
+
+These schemas describe the JSON response shape expected by the DTO layer.
+
+## Expected API endpoints
+
+The production client is `URLSessionWeatherAPIClient`. It currently expects a JSON API rooted at:
+
+```text
+https://api.atmosweather.example/v1
+```
+
+All endpoints are `GET` requests.
+
+| Purpose | Method | Path | Request shape | Schema |
+| --- | --- | --- | --- | --- |
+| Default saved locations | `GET` | `/locations/default` | No path params, query params, or body. | `Schemas/default-locations.schema.json` |
+| Search locations | `GET` | `/locations/search` | Query string: `query=<string>` | `Schemas/search-locations.schema.json` |
+| Weather report for a location | `GET` | `/weather/{locationID}` | Path param: `locationID=<WeatherLocationDTO.id>` | `Schemas/weather-report.schema.json` |
+
+### Request examples
+
+Default locations:
+
+```http
+GET /v1/locations/default
+```
+
+Search locations:
+
+```http
+GET /v1/locations/search?query=San%20Francisco
+```
+
+Weather report:
+
+```http
+GET /v1/weather/loc-current-san-francisco
+```
+
+### Response expectations
+
+- Responses must be JSON.
+- Successful responses should use a `2xx` HTTP status code.
+- Non-`2xx` responses are treated as API failures.
+
+## Tests
+
+Run the app test suite through XcodeBuildMCP:
+
+```bash
+../../build/cli.js simulator test
+```
+
+UI tests inject `--mock-weather-api` themselves so they do not depend on the production API endpoint.
\ No newline at end of file
diff --git a/example_projects/Weather/Schemas/default-locations.schema.json b/example_projects/Weather/Schemas/default-locations.schema.json
new file mode 100644
--- /dev/null
+++ b/example_projects/Weather/Schemas/default-locations.schema.json
@@ -1,0 +1,93 @@
+{
+ "type": "json_schema",
+ "json_schema": {
+ "name": "default_locations_response",
+ "strict": true,
+ "schema": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "locations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "country": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "temperatureC": {
+ "type": "integer"
+ },
+ "highC": {
+ "type": "integer"
+ },
+ "lowC": {
+ "type": "integer"
+ },
+ "condition": {
+ "type": "string",
+ "enum": [
+ "sunny",
+ "mostly_sunny",
+ "partly_cloudy",
+ "cloudy",
+ "clear_day",
+ "clear_night",
+ "light_rain",
+ "heavy_rain",
+ "light_snow",
+ "snow_showers",
+ "thunderstorms",
+ "hazy"
+ ]
+ },
+ "localTime": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "hour": {
+ "type": "integer"
+ },
+ "minute": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "hour",
+ "minute"
+ ]
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "subtitle",
+ "country",
+ "temperatureC",
+ "highC",
+ "lowC",
+ "condition",
+ "localTime"
+ ]
+ }
+ }
+ },
+ "required": [
+ "locations"
+ ]
+ }
+ }
+}
diff --git a/example_projects/Weather/Schemas/search-locations.schema.json b/example_projects/Weather/Schemas/search-locations.schema.json
new file mode 100644
--- /dev/null
+++ b/example_projects/Weather/Schemas/search-locations.schema.json
@@ -1,0 +1,93 @@
+{
+ "type": "json_schema",
+ "json_schema": {
+ "name": "search_locations_response",
+ "strict": true,
+ "schema": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "locations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "subtitle": {
+ "type": "string"
+ },
+ "country": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "temperatureC": {
+ "type": "integer"
+ },
+ "highC": {
+ "type": "integer"
+ },
+ "lowC": {
+ "type": "integer"
+ },
+ "condition": {
+ "type": "string",
+ "enum": [
+ "sunny",
+ "mostly_sunny",
+ "partly_cloudy",
+ "cloudy",
+ "clear_day",
+ "clear_night",
+ "light_rain",
+ "heavy_rain",
+ "light_snow",
+ "snow_showers",
+ "thunderstorms",
+ "hazy"
+ ]
+ },
+ "localTime": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "hour": {
+ "type": "integer"
+ },
+ "minute": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "hour",
+ "minute"
+ ]
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "subtitle",
+ "country",
+ "temperatureC",
+ "highC",
+ "lowC",
+ "condition",
+ "localTime"
+ ]
+ }
+ }
+ },
+ "required": [
+ "locations"
+ ]
+ }
+ }
+}
diff --git a/example_projects/Weather/Schemas/weather-report.schema.json b/example_projects/Weather/Schemas/weather-report.schema.json
new file mode 100644
--- /dev/null
+++ b/example_projects/Weather/Schemas/weather-report.schema.json
@@ -1,0 +1,510 @@
+{
+ "type": "json_schema",
+ "json_schema": {
+ "name": "weather_report_response",
+ "strict": true,
+ "schema": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "current": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "temperatureC": {
+ "type": "integer"
+ },
+ "highC": {
+ "type": "integer"
+ },
+ "lowC": {
+ "type": "integer"
+ },
+ "feelsLikeC": {
+ "type": "integer"
+ },
+ "condition": {
+ "type": "string",
+ "enum": [
+ "sunny",
+ "mostly_sunny",
+ "partly_cloudy",
+ "cloudy",
+ "clear_day",
+ "clear_night",
+ "light_rain",
+ "heavy_rain",
+ "light_snow",
+ "snow_showers",
+ "thunderstorms",
+ "hazy"
+ ]
+ },
+ "solarProgress": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "type": "string",
+ "enum": [
+ "before_sunrise",
+ "daylight",
+ "after_sunset"
+ ]
+ },
+ "daylightFraction": {
+ "type": [
+ "number",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "daylightFraction"
+ ]
+ },
+ "sunrise": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "hour": {
+ "type": "integer"
+ },
+ "minute": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "hour",
+ "minute"
+ ]
+ },
+ "sunset": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "hour": {
+ "type": "integer"
+ },
+ "minute": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "hour",
+ "minute"
+ ]
+ },
+ "airQualityIndex": {
+ "type": "integer"
+ },
+ "airQualityCategory": {
+ "type": "string",
+ "enum": [
+ "good",
+ "moderate",
+ "unhealthy_for_sensitive_groups",
+ "unhealthy",
+ "very_unhealthy",
+ "hazardous"
+ ]
+ },
+ "uvIndex": {
+ "type": "integer"
+ },
+ "uvCategory": {
+ "type": "string",
+ "enum": [
+ "none",
+ "low",
+ "moderate",
+ "high",
+ "very_high",
+ "extreme"
+ ]
+ },
+ "windKph": {
+ "type": "integer"
+ },
+ "windDirectionDegrees": {
+ "type": "number"
+ },
+ "humidity": {
+ "type": "integer"
+ },
+ "visibilityKilometers": {
+ "type": "number"
+ },
+ "pressureMillibars": {
+ "type": "integer"
+ },
+ "pressureTrend": {
+ "type": "string",
+ "enum": [
+ "rising",
+ "steady",
+ "falling"
+ ]
+ },
+ "precipChance": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "id",
+ "temperatureC",
+ "highC",
+ "lowC",
+ "feelsLikeC",
+ "condition",
+ "solarProgress",
+ "sunrise",
+ "sunset",
+ "airQualityIndex",
+ "airQualityCategory",
+ "uvIndex",
+ "uvCategory",
+ "windKph",
+ "windDirectionDegrees",
+ "humidity",
+ "visibilityKilometers",
+ "pressureMillibars",
+ "pressureTrend",
+ "precipChance"
+ ]
+ },
+ "hourly": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "hour": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "type": "string",
+ "enum": [
+ "current",
+ "clock"
+ ]
+ },
+ "hour": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "minute": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "hour",
+ "minute"
+ ]
+ },
+ "temperatureC": {
+ "type": "integer"
+ },
+ "condition": {
+ "type": "string",
+ "enum": [
+ "sunny",
+ "mostly_sunny",
+ "partly_cloudy",
+ "cloudy",
+ "clear_day",
+ "clear_night",
+ "light_rain",
+ "heavy_rain",
+ "light_snow",
+ "snow_showers",
+ "thunderstorms",
+ "hazy"
+ ]
+ }
+ },
+ "required": [
+ "id",
+ "hour",
+ "temperatureC",
+ "condition"
+ ]
+ }
+ },
+ "daily": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "day": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "type": "string",
+ "enum": [
+ "today",
+ "weekday"
+ ]
+ },
+ "weekdayRawValue": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "enum": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ null
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "weekdayRawValue"
+ ]
+ },
+ "condition": {
+ "type": "string",
+ "enum": [
+ "sunny",
+ "mostly_sunny",
+ "partly_cloudy",
+ "cloudy",
+ "clear_day",
+ "clear_night",
+ "light_rain",
+ "heavy_rain",
+ "light_snow",
+ "snow_showers",
+ "thunderstorms",
+ "hazy"
+ ]
+ },
+ "lowC": {
+ "type": "integer"
+ },
+ "highC": {
+ "type": "integer"
+ },
+ "weekLowC": {
+ "type": "integer"
+ },
+ "weekHighC": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "id",
+ "day",
+ "condition",
+ "lowC",
+ "highC",
+ "weekLowC",
+ "weekHighC"
+ ]
+ }
+ },
+ "precipitationDetailCurrent": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "temperatureC": {
+ "type": "integer"
+ },
+ "highC": {
+ "type": "integer"
+ },
+ "lowC": {
+ "type": "integer"
+ },
+ "feelsLikeC": {
+ "type": "integer"
+ },
+ "condition": {
+ "type": "string",
+ "enum": [
+ "sunny",
+ "mostly_sunny",
+ "partly_cloudy",
+ "cloudy",
+ "clear_day",
+ "clear_night",
+ "light_rain",
+ "heavy_rain",
+ "light_snow",
+ "snow_showers",
+ "thunderstorms",
+ "hazy"
+ ]
+ },
+ "solarProgress": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "type": "string",
+ "enum": [
+ "before_sunrise",
+ "daylight",
+ "after_sunset"
+ ]
+ },
+ "daylightFraction": {
+ "type": [
+ "number",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "daylightFraction"
+ ]
+ },
+ "sunrise": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "hour": {
+ "type": "integer"
+ },
+ "minute": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "hour",
+ "minute"
+ ]
+ },
+ "sunset": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "hour": {
+ "type": "integer"
+ },
+ "minute": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "hour",
+ "minute"
+ ]
+ },
+ "airQualityIndex": {
+ "type": "integer"
+ },
+ "airQualityCategory": {
+ "type": "string",
+ "enum": [
+ "good",
+ "moderate",
+ "unhealthy_for_sensitive_groups",
+ "unhealthy",
+ "very_unhealthy",
+ "hazardous"
+ ]
+ },
+ "uvIndex": {
+ "type": "integer"
+ },
+ "uvCategory": {
+ "type": "string",
+ "enum": [
+ "none",
+ "low",
+ "moderate",
+ "high",
+ "very_high",
+ "extreme"
+ ]
+ },
+ "windKph": {
+ "type": "integer"
+ },
+ "windDirectionDegrees": {
+ "type": "number"
+ },
+ "humidity": {
+ "type": "integer"
+ },
+ "visibilityKilometers": {
+ "type": "number"
... diff truncated: showing 800 of 6515 linesYou can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 6 total unresolved issues (including 4 from previous reviews).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Invalid clock time crashes instead of throwing error
- Added guard validation in LocalClockTime.init(dto:) to check hour (0...23) and minute (0...59) ranges and throw WeatherDTOMappingError.invalidClockTime before calling the precondition-backed domain initializer.
- ✅ Fixed: Task cancellation mishandled as weather API error
- Added specific catch clause for CancellationError that returns early without setting weatherErrorMessage, preventing false error banners when tasks are cancelled due to location changes.
Or push these changes by commenting:
@cursor push 178a174816
Preview (178a174816)
diff --git a/example_projects/Weather/Weather/ContentView.swift b/example_projects/Weather/Weather/ContentView.swift
--- a/example_projects/Weather/Weather/ContentView.swift
+++ b/example_projects/Weather/Weather/ContentView.swift
@@ -150,6 +150,8 @@
do {
report = try await weatherService.weather(for: locationID)
+ } catch is CancellationError {
+ return
} catch {
weatherErrorMessage = "Weather unavailable"
}
diff --git a/example_projects/Weather/Weather/Services/WeatherClientDTOs.swift b/example_projects/Weather/Weather/Services/WeatherClientDTOs.swift
--- a/example_projects/Weather/Weather/Services/WeatherClientDTOs.swift
+++ b/example_projects/Weather/Weather/Services/WeatherClientDTOs.swift
@@ -142,6 +142,7 @@
case missingWeekday
case invalidWeekday(Int)
case invalidWindDirection(Double)
+ case invalidClockTime(hour: Int, minute: Int)
}
extension WeatherReport {
@@ -241,6 +242,9 @@
private extension LocalClockTime {
init(dto: LocalClockTimeDTO) throws {
+ guard (0...23).contains(dto.hour), (0...59).contains(dto.minute) else {
+ throw WeatherDTOMappingError.invalidClockTime(hour: dto.hour, minute: dto.minute)
+ }
self.init(hour: dto.hour, minute: dto.minute)
}
}You can send follow-ups to the cloud agent here.
Align API decoding with the response schemas, validate DTO clock values before constructing domain models, and fix UI edge cases reported in PR review. Remove account- and machine-specific Weather example configuration so the sample project is portable for contributors. Co-Authored-By: OpenAI Codex <noreply@openai.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: SunMiniCard shows wrong event after sunset
- Changed afterSunset case to show next sunrise as primary display instead of repeating the past sunset event.
- ✅ Fixed: Noise texture is invisible due to rounding
- Removed .rounded(.down) call that was causing alpha to always be 0, making noise texture visible.
Or push these changes by commenting:
@cursor push d434957ba2
Preview (d434957ba2)
diff --git a/example_projects/Weather/Weather/Views/Backgrounds/AtmosBackground.swift b/example_projects/Weather/Weather/Views/Backgrounds/AtmosBackground.swift
--- a/example_projects/Weather/Weather/Views/Backgrounds/AtmosBackground.swift
+++ b/example_projects/Weather/Weather/Views/Backgrounds/AtmosBackground.swift
@@ -72,7 +72,7 @@
Canvas { context, size in
for x in stride(from: 0, through: size.width, by: 18) {
for y in stride(from: 0, through: size.height, by: 18) {
- let alpha = WeatherMetricHelpers.deterministicPercent(seed: Int(x + y), index: Int(x * 3 + y)).rounded(.down) * 0.006
+ let alpha = WeatherMetricHelpers.deterministicPercent(seed: Int(x + y), index: Int(x * 3 + y)) * 0.006
context.fill(
Path(CGRect(x: x, y: y, width: 1, height: 1)),
with: .color(.white.opacity(alpha))
diff --git a/example_projects/Weather/Weather/Views/Sections/SunMiniCard.swift b/example_projects/Weather/Weather/Views/Sections/SunMiniCard.swift
--- a/example_projects/Weather/Weather/Views/Sections/SunMiniCard.swift
+++ b/example_projects/Weather/Weather/Views/Sections/SunMiniCard.swift
@@ -34,18 +34,18 @@
private var nextLabel: String {
switch current.solarProgress {
- case .beforeSunrise:
+ case .beforeSunrise, .afterSunset:
"SUNRISE"
- case .daylight, .afterSunset:
+ case .daylight:
"SUNSET"
}
}
private var nextTime: String {
switch current.solarProgress {
- case .beforeSunrise:
+ case .beforeSunrise, .afterSunset:
current.sunrise.fullClockLabel
- case .daylight, .afterSunset:
+ case .daylight:
current.sunset.fullClockLabel
}
}You can send follow-ups to the cloud agent here.
Normalize the API value 360 degrees to the domain model's zero-degree north representation before constructing WindDirection. Co-Authored-By: OpenAI Codex <noreply@openai.com>
Show the sun card's after-sunset state as a past event and restore the background noise layer by preserving deterministic alpha values. Co-Authored-By: OpenAI Codex <noreply@openai.com>
Update the sun card after-sunset state to surface sunrise as the next solar event while keeping the passed sunset in the secondary label. Co-Authored-By: OpenAI Codex <noreply@openai.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Stale error message applied to wrong location
- Added stale-location guard before setting weatherErrorMessage in the catch block to prevent old tasks from overwriting error state after location switch.
- ✅ Fixed: Triplicate
Array[safe:]extension across view files- Extracted the duplicate Array[safe:] extension into a shared ArrayExtensions.swift file and removed it from AtmosBackground, ConditionGrid, and DailyForecastCard.
Or push these changes by commenting:
@cursor push 83a718035f
Preview (83a718035f)
diff --git a/example_projects/Weather/Weather/ContentView.swift b/example_projects/Weather/Weather/ContentView.swift
--- a/example_projects/Weather/Weather/ContentView.swift
+++ b/example_projects/Weather/Weather/ContentView.swift
@@ -155,6 +155,7 @@
report = loadedReport
} catch is CancellationError {
} catch {
+ guard selectedLocation?.id == locationID else { return }
weatherErrorMessage = "Weather unavailable"
}
diff --git a/example_projects/Weather/Weather/Extensions/ArrayExtensions.swift b/example_projects/Weather/Weather/Extensions/ArrayExtensions.swift
new file mode 100644
--- /dev/null
+++ b/example_projects/Weather/Weather/Extensions/ArrayExtensions.swift
@@ -1,0 +1,14 @@
+//
+// ArrayExtensions.swift
+// Weather
+//
+// Created by Cameron on 05/05/2026.
+//
+
+import Foundation
+
+extension Array {
+ subscript(safe index: Int) -> Element? {
+ indices.contains(index) ? self[index] : nil
+ }
+}
diff --git a/example_projects/Weather/Weather/Views/Backgrounds/AtmosBackground.swift b/example_projects/Weather/Weather/Views/Backgrounds/AtmosBackground.swift
--- a/example_projects/Weather/Weather/Views/Backgrounds/AtmosBackground.swift
+++ b/example_projects/Weather/Weather/Views/Backgrounds/AtmosBackground.swift
@@ -234,9 +234,3 @@
.opacity(opacity)
}
}
-
-private extension Array {
- subscript(safe index: Int) -> Element? {
- indices.contains(index) ? self[index] : nil
- }
-}
diff --git a/example_projects/Weather/Weather/Views/Sections/ConditionGrid.swift b/example_projects/Weather/Weather/Views/Sections/ConditionGrid.swift
--- a/example_projects/Weather/Weather/Views/Sections/ConditionGrid.swift
+++ b/example_projects/Weather/Weather/Views/Sections/ConditionGrid.swift
@@ -366,9 +366,3 @@
return path
}
}
-
-private extension Array {
- subscript(safe index: Int) -> Element? {
- indices.contains(index) ? self[index] : nil
- }
-}
diff --git a/example_projects/Weather/Weather/Views/Sections/DailyForecastCard.swift b/example_projects/Weather/Weather/Views/Sections/DailyForecastCard.swift
--- a/example_projects/Weather/Weather/Views/Sections/DailyForecastCard.swift
+++ b/example_projects/Weather/Weather/Views/Sections/DailyForecastCard.swift
@@ -110,9 +110,3 @@
.frame(height: 12)
}
}
-
-private extension Array {
- subscript(safe index: Int) -> Element? {
- indices.contains(index) ? self[index] : nil
- }
-}You can send follow-ups to the cloud agent here.
Avoid applying stale weather errors after location changes and centralize the shared safe array subscript used by Weather views. Co-Authored-By: OpenAI Codex <noreply@openai.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Hero subtitle hardcodes "My Location" for all cities
- Threaded the subtitle property from WeatherLocation through ContentView, AtmosWeatherScreen, and WeatherHeroView to display location-specific subtitles instead of the hardcoded 'My Location' text.
Or push these changes by commenting:
@cursor push 4554cb49ff
Preview (4554cb49ff)
diff --git a/example_projects/Weather/Weather/ContentView.swift b/example_projects/Weather/Weather/ContentView.swift
--- a/example_projects/Weather/Weather/ContentView.swift
+++ b/example_projects/Weather/Weather/ContentView.swift
@@ -51,6 +51,7 @@
if let report, let selectedLocation {
AtmosWeatherScreen(
locationName: selectedLocation.name,
+ locationSubtitle: selectedLocation.subtitle,
current: report.current,
hourly: report.hourly,
daily: report.daily,
diff --git a/example_projects/Weather/Weather/Views/AtmosWeatherScreen.swift b/example_projects/Weather/Weather/Views/AtmosWeatherScreen.swift
--- a/example_projects/Weather/Weather/Views/AtmosWeatherScreen.swift
+++ b/example_projects/Weather/Weather/Views/AtmosWeatherScreen.swift
@@ -2,6 +2,7 @@
struct AtmosWeatherScreen: View {
let locationName: String
+ let locationSubtitle: String
let current: CurrentWeather
let hourly: [HourlyForecast]
let daily: [DailyForecast]
@@ -17,7 +18,7 @@
ScrollView {
VStack(spacing: 0) {
- WeatherHeroView(locationName: locationName, current: current, units: units)
+ WeatherHeroView(locationName: locationName, locationSubtitle: locationSubtitle, current: current, units: units)
VStack(spacing: 10) {
HourlyForecastCard(forecasts: hourly, current: current, units: units)
diff --git a/example_projects/Weather/Weather/Views/Sections/WeatherHeroView.swift b/example_projects/Weather/Weather/Views/Sections/WeatherHeroView.swift
--- a/example_projects/Weather/Weather/Views/Sections/WeatherHeroView.swift
+++ b/example_projects/Weather/Weather/Views/Sections/WeatherHeroView.swift
@@ -2,6 +2,7 @@
struct WeatherHeroView: View {
let locationName: String
+ let locationSubtitle: String
let current: CurrentWeather
let units: WeatherUnits
@@ -12,7 +13,7 @@
.tracking(-0.3)
.accessibilityIdentifier("weather.heroLocation")
- Text("My Location")
+ Text(locationSubtitle)
.font(.system(size: 13))
.tracking(0.2)
.foregroundStyle(current.theme.foregroundMuted)You can send follow-ups to the cloud agent here.
Plumb dew point and location subtitles through the Weather example and make cancelled location searches clear their current loading state safely. Co-Authored-By: OpenAI Codex <noreply@openai.com>
Keep Weather loading flags scoped to the active request and handle search cancellations separately from genuine search errors. Co-Authored-By: OpenAI Codex <noreply@openai.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Hourly curve points misaligned with forecast cells
- Changed curve point x-coordinates from step-based distribution to cell-center alignment using (index + 0.5) * cellWidth formula.
Or push these changes by commenting:
@cursor push b90a74ec6b
Preview (b90a74ec6b)
diff --git a/example_projects/Weather/Weather/Views/Sections/HourlyForecastCard.swift b/example_projects/Weather/Weather/Views/Sections/HourlyForecastCard.swift
--- a/example_projects/Weather/Weather/Views/Sections/HourlyForecastCard.swift
+++ b/example_projects/Weather/Weather/Views/Sections/HourlyForecastCard.swift
@@ -100,13 +100,13 @@
let minTemp = temperatures.min() ?? 0
let maxTemp = temperatures.max() ?? minTemp + 1
let range = max(maxTemp - minTemp, 1)
- let step = size.width / CGFloat(max(forecasts.count - 1, 1))
+ let cellWidth = size.width / CGFloat(forecasts.count)
let pad: CGFloat = 8
return forecasts.enumerated().map { index, forecast in
let normalized = CGFloat(forecast.temperatureC - minTemp) / CGFloat(range)
return CGPoint(
- x: CGFloat(index) * step,
+ x: (CGFloat(index) + 0.5) * cellWidth,
y: pad + (1 - normalized) * (size.height - pad * 2)
)
}You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit e6d3b4b. Configure here.
Center hourly curve points over their corresponding forecast cells so the curve dots align with the hour labels and weather icons. Co-Authored-By: OpenAI Codex <noreply@openai.com>


This pull request introduces the initial Atmos Weather iOS app prototype, including project configuration, documentation, assets, and core application logic. The changes establish the project's structure, define API schemas, provide developer documentation, and implement the main SwiftUI view for the app.
Project configuration and assets:
Weather.xcscheme) to support building, running, and testing the app, including configuration for launching with mock weather data.Assets.xcassets), providing required resources for the app's appearance. [1] [2] [3]config.yaml) for defining build and test workflows and simulator preferences.Documentation and API schemas:
README.mdwith instructions for building, running, and testing the app, as well as details about API endpoints, fixtures, and schemas.AGENTS.mdwith guidance for using XcodeBuildMCP tools.Core application logic:
ContentView.swift), handling weather data loading, location selection, error handling, and presentation of sheets for locations, settings, and precipitation details.