feat(weather): add iOS current weather widget foundation - #576
YinCheng0106 wants to merge 14 commits into
Conversation
Platform: ios
Platform: ios
Platform: ios
Platform: ios New(zh-Hant): iOS 小工具可顯示 DPIP 所選地區的目前天氣 New(en-US): iOS widgets can show current weather for the area selected in DPIP
Platform: ios New(zh-Hant): iOS 天氣小工具新增天氣圖示、資料狀態與點擊開啟首頁 New(en-US): iOS weather widgets add condition icons, data states, and Home deep linking
Platform: ios New(zh-Hant): iOS 天氣小工具可依日出日落自動切換日夜天氣圖示 New(en-US): iOS weather widgets can switch day and night icons based on sunrise and sunset
Platform: ios
Platform: ios
Platform: ios New(zh-Hant): iOS 天氣小工具支援繁體中文與英文介面 New(en-US): iOS weather widgets support Traditional Chinese and English
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate issues affect timeline continuity, calibrated time handling, region synchronization, request ordering, and localized condition display.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds the first-stage iOS current-weather Widget foundation, connecting Flutter weather data to an App Group-backed WidgetKit extension.
Changes:
- Adds schema v3 snapshots, publishing, region synchronization, and native storage.
- Adds stale/day-night WidgetKit UI, localization, deep linking, and extension configuration.
- Centralizes solar-time utilities and expands Dart/native tests.
Review findings:
- Moderate (1 vote):
ios/DPIPWidgets/CurrentWeatherWidgetTimeline.swiftschedules only one future day/night transition. - Moderate (2 votes):
ios/DPIPWidgets/DPIPWidgets.swiftuses uncalibrated device dates for server observation timestamps. - Moderate (1 vote):
ios/DPIPWidgets/DPIPWidgets.swiftcan remain in the post-transition state indefinitely. - Moderate (3 votes):
lib/features/home/presentation/home_weather_controller.dartmay retain a previous-region snapshot after relaunch. - Moderate (1 vote):
lib/features/home/presentation/home_weather_controller.dartlacks latest-request-wins protection. - Nit (2 votes):
lib/features/weather/domain/current_weather_widget_snapshot.dartduplicates weather-code classification. - Moderate (1 vote):
lib/features/weather/domain/current_weather_widget_snapshot.dartpersists non-localized Traditional Chinese condition labels.
File summaries
| File | Description |
|---|---|
test/features/weather/current_weather_widget_snapshot_test.dart |
Tests snapshot schema and condition mapping. |
test/features/weather/current_weather_widget_publisher_test.dart |
Tests snapshot serialization and publishing. |
test/features/weather/current_weather_widget_coordinator_test.dart |
Tests region coordination. |
test/features/home/weather_sky/sky_keyframe_test.dart |
Updates solar-time imports. |
test/features/home/presentation/home_weather_controller_test.dart |
Tests region synchronization and callbacks. |
test/features/home/home_providers_test.dart |
Tests provider wiring. |
test/core/weather/solar_time_test.dart |
Tests solar transitions. |
test/core/platform/widget_snapshot_writer_test.dart |
Tests platform writer failures. |
lib/features/weather/weather_providers.dart |
Registers Widget synchronization services. |
lib/features/weather/domain/current_weather_widget_sync.dart |
Defines the Widget sync contract. |
lib/features/weather/domain/current_weather_widget_snapshot.dart |
Defines schema v3 snapshots and mapping. |
lib/features/weather/data/current_weather_widget_publisher.dart |
Serializes and publishes snapshots. |
lib/features/weather/current_weather_widget_coordinator.dart |
Coordinates region-aware snapshots. |
lib/features/home/presentation/widgets/weather_sky/weather_sky_background.dart |
Uses shared solar-time utilities. |
lib/features/home/presentation/widgets/home_sheet_header.dart |
Uses shared solar-time utilities. |
lib/features/home/presentation/widgets/home_forecast_section.dart |
Uses shared solar-time utilities. |
lib/features/home/presentation/home_weather_controller.dart |
Publishes and invalidates Widget data. |
lib/features/home/home_providers.dart |
Wires Home weather to Widget synchronization. |
lib/core/weather/solar_time.dart |
Provides shared solar calculations. |
lib/core/platform/widget_snapshot_writer.dart |
Defines the native snapshot bridge. |
lib/core/error/failure.dart |
Adds Widget snapshot failure types. |
ios/RunnerTests/RunnerTests.swift |
Tests native snapshot file operations. |
ios/RunnerTests/CurrentWeatherWidgetTests.swift |
Tests decoding, symbols, and timelines. |
ios/Runner/WidgetSnapshotPlugin.swift |
Bridges App Group storage and WidgetKit reloads. |
ios/Runner/RunnerRelease.entitlements |
Enables the release App Group. |
ios/Runner/Runner.entitlements |
Enables the debug App Group. |
ios/Runner/Info.plist |
Configures App Group and URL scheme metadata. |
ios/Runner/AppDelegate.swift |
Registers the native Widget plugin. |
ios/Runner.xcodeproj/project.pbxproj |
Adds the Widget extension target. |
ios/DPIPWidgetsExtension.entitlements |
Enables extension App Group access. |
ios/DPIPWidgets/WidgetSnapshotStore.swift |
Reads shared snapshots. |
ios/DPIPWidgets/Localizable.xcstrings |
Provides Widget localization. |
ios/DPIPWidgets/Info.plist |
Configures extension metadata. |
ios/DPIPWidgets/DPIPWidgetsBundle.swift |
Defines the Widget bundle. |
ios/DPIPWidgets/DPIPWidgets.swift |
Implements Widget UI and provider logic. |
ios/DPIPWidgets/CurrentWeatherWidgetTimeline.swift |
Builds stale and day/night timelines. |
ios/DPIPWidgets/CurrentWeatherWidgetSnapshot.swift |
Decodes Widget snapshots. |
ios/DPIPWidgets/Assets.xcassets/WidgetBackground.colorset/Contents.json |
Defines the Widget background asset. |
ios/DPIPWidgets/Assets.xcassets/Contents.json |
Defines asset catalog metadata. |
ios/DPIPWidgets/Assets.xcassets/AppIcon.appiconset/Contents.json |
Configures the Widget icon. |
ios/DPIPWidgets/Assets.xcassets/AccentColor.colorset/Contents.json |
Configures the accent color. |
Review details
Suppressed comments (4)
ios/DPIPWidgets/CurrentWeatherWidgetTimeline.swift:68
- Only one future day/night transition is added, and the provider returns a
.nevertimeline. After that transition the Widget has no later entry to flip back at the next sunrise/sunset, so a Widget that stays installed can remain on the wrong day/night symbol indefinitely. Schedule enough subsequent transitions (or a reload that can compute them) for the claimed ongoing day/night behavior.
if snapshot.nextDayNightTransitionTime > 0,
transitionAt > now {
dates.append(transitionAt)
}
ios/DPIPWidgets/DPIPWidgets.swift:63
- The timeline contains only the single transition recorded in the snapshot and uses
.never, so after that sunrise/sunset entry is consumed there is no later transition scheduled. If the app is not reopened to publish a new snapshot, the widget remains stuck in the post-transition day/night state indefinitely; schedule subsequent transitions or use a refresh strategy that can recompute them.
completion(
Timeline(
entries: entries,
policy: .never
)
lib/features/home/presentation/home_weather_controller.dart:175
- This callback is gated only by
_loadedCode, so it does not distinguish concurrent requests for the same region. A refresh can start request A, request B can finish and publish newer data, and then A can finish and overwrite the App Group with older weather; the same can happen when the user switches A → B → A. Track a request generation/latest-wins token before invokingonRealtimeLoadedso only the current request can publish.
final callback = onRealtimeLoaded;
if (callback != null) {
unawaited(callback(code, value));
lib/features/weather/domain/current_weather_widget_snapshot.dart:115
weather.data.weatheris the API's Traditional Chinese display label, and the Widget view renderssnapshot.weatherverbatim. As a result, an English-locale widget still displays values such as多雲時雨; persist/use the locale-neutral condition and add localized condition labels in the Widget extension instead of copying this raw label into the UI.
- Files reviewed: 41/41 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Fix(zh-Hant): 修正天氣請求競態造成舊資料覆蓋新資料 Fix(en-US): Prevent stale weather requests from overwriting newer data
Platform: ios Fix(zh-Hant): 啟動時清除尚未驗證地區的舊天氣小工具資料 Fix(en-US): Clear unverified weather widget snapshots on app startup
Platform: ios Fix(zh-Hant): 修正未校時與裝置時間偏移造成的小工具時間誤差 Fix(en-US): Calibrate widget timing and reject unsynced weather snapshots
Platform: ios Fix(zh-Hant): 天氣小工具改用本地化的語意天氣狀態 Fix(en-US): Show localized semantic weather conditions in iOS widgets
There was a problem hiding this comment.
🟡 Changes recommended
Three unresolved findings remain, including one critical request-generation race.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
ios/DPIPWidgets/CurrentWeatherWidgetSnapshot.swift:215
schemaVersion >= 4treats every future schema as if it were v4. If a later app writes a v5 snapshot with changed fields or semantics, an older extension will silently render it instead of failing closed, even though this decoder only has explicit compatibility for v2–v4. Validate the supported range and branch onschemaVersion == 4so unknown versions produce the no-data state.
if schemaVersion >= 4 {
calibratedTimeOffsetMilliseconds = try container.decode(
Int.self,
forKey: .calibratedTimeOffsetMilliseconds
)
} else {
calibratedTimeOffsetMilliseconds = try container.decodeIfPresent(
Int.self,
forKey: .calibratedTimeOffsetMilliseconds
) ?? 0
lib/features/home/presentation/home_weather_controller.dart:180
- The Widget publish callback is reached only after
_loadhas awaited the forecast and hour-trend futures above. A slow or unavailable secondary endpoint therefore delays the current-weather snapshot even when the realtime request has already succeeded, and the app may be backgrounded before the Widget is updated. Publish the current-weather snapshot from the realtime completion independently of the forecast/trend state.
final callback = onRealtimeLoaded;
if (callback != null) {
unawaited(callback(code, value));
- Files reviewed: 47/47 changed files
- Comments generated: 1
- Review effort level: Lite
| if (_regions.selectedCode != regionCode) { | ||
| return; | ||
| } |
這個 PR 做了什麼
新增 iOS 目前天氣 Widget 的基礎功能,讓 Widget 可顯示 DPIP 所選地區的目前天氣,並支援 stale 狀態、日夜圖示切換、Deep Link 與中英文介面。
此 PR 為 iOS Weather Widget 的第一階段實作,先建立 Current Weather Widget 與相關基礎架構;後續地點設定、獨立背景更新、Medium / Large Widget 與預報功能將以獨立 PR 逐步加入。
相關 issue
目前無。
怎麼驗
基本功能
tool/run.sh啟動 DPIP。地區同步
stale 狀態
Widget 資料超過目前設定的 stale threshold 後,確認:
日夜切換
確認目前天氣 Widget 可依所在地日出、日落時間切換圖示,例如:
sun.max.fillmoon.stars.fillcloud.sun.fillcloud.moon.fillWidgetKit timeline 會安排下一個日夜 transition,不需要重新開啟 App 才能完成已排定的圖示切換。
Deep Link
點擊 Widget 後,確認可透過:
dpip:///home開啟 DPIP 首頁。
本地化
確認 Widget Extension 支援:
包含:
原生 Widget 字串使用 Extension 專用 String Catalog,不使用 Flutter
AppLocalizations。自動測試與建置
已驗證:
flutter analyze:No issues foundRunner.app與DPIPWidgetsExtension.appex均成功產出git diff --check通過已知範圍
此 PR 只建立 Current Weather Widget 的第一層基礎。
以下功能會在後續獨立 PR 加入:
目前 Widget 的新天氣資料仍由 DPIP App 取得並寫入 App Group;App 未執行時,不會自行向天氣 API 取得新的觀測資料。
WidgetKit timeline 可在 App 未執行時處理已排定的 stale 狀態,以及 snapshot 內攜帶的下一次日夜 transition;若 App 長時間未再次發布 snapshot,後續日夜週期不在此 PR 的保證範圍內。此機制不能視為即時天氣更新或災防警報機制。
Reviewer 建議重點
建議優先確認:
檢查清單
tool/check/commits.sh origin/main..HEAD通過—— commit 訊息就是更新日誌,格式見 commit.md
mise exec -- flutter analyze與mise exec -- flutter test通過- Flutter UI 維持
AppLocalizations- Widget Extension 使用原生
Localizable.xcstrings