Skip to content

feat(weather): add iOS current weather widget foundation - #576

Open
YinCheng0106 wants to merge 14 commits into
mainfrom
feat/ios-widget-current-weather
Open

YinCheng0106 wants to merge 14 commits into
mainfrom
feat/ios-widget-current-weather

Conversation

@YinCheng0106

@YinCheng0106 YinCheng0106 commented Sep 16, 2026

Copy link
Copy Markdown

這個 PR 做了什麼

新增 iOS 目前天氣 Widget 的基礎功能,讓 Widget 可顯示 DPIP 所選地區的目前天氣,並支援 stale 狀態、日夜圖示切換、Deep Link 與中英文介面。

此 PR 為 iOS Weather Widget 的第一階段實作,先建立 Current Weather Widget 與相關基礎架構;後續地點設定、獨立背景更新、Medium / Large Widget 與預報功能將以獨立 PR 逐步加入。

相關 issue

目前無。

怎麼驗

基本功能

  1. 使用 tool/run.sh 啟動 DPIP。
  2. 在 App 中選擇一個台灣地區。
  3. 將「目前天氣」Widget 加入 iOS 主畫面。
  4. 確認 Widget 可顯示:
    • 所選地區
    • 目前天氣
    • 溫度
    • 濕度
    • 雨量
    • 觀測時間

地區同步

  1. 在 DPIP 中切換地區。
  2. 等待新地區天氣資料載入。
  3. 確認 Widget 更新為新的地區及天氣資料。
  4. 確認切換期間不會將上一個地區的資料誤認為目前地區。

stale 狀態

Widget 資料超過目前設定的 stale threshold 後,確認:

  • Widget 仍保留最後一筆有效天氣資料。
  • 顯示「較舊 / Outdated」狀態。
  • 不會把舊資料表示成即時資料。

日夜切換

確認目前天氣 Widget 可依所在地日出、日落時間切換圖示,例如:

  • 晴天白天:sun.max.fill
  • 晴天夜晚:moon.stars.fill
  • 多雲白天:cloud.sun.fill
  • 多雲夜晚:cloud.moon.fill

WidgetKit timeline 會安排下一個日夜 transition,不需要重新開啟 App 才能完成已排定的圖示切換。

Deep Link

點擊 Widget 後,確認可透過:

dpip:///home

開啟 DPIP 首頁。

本地化

確認 Widget Extension 支援:

  • English
  • 繁體中文(zh-Hant)

包含:

  • Widget Gallery 名稱
  • Widget Gallery 描述
  • stale 狀態
  • 無資料狀態
  • 天氣狀態文字(例如 Clear / 晴、Rain / 雨)

原生 Widget 字串使用 Extension 專用 String Catalog,不使用 Flutter AppLocalizations

自動測試與建置

已驗證:

  • Dart tests:1,894 passed / 5 skipped
  • Weather tests:60 passed
  • Solar tests:8 passed
  • Home weather sky tests:14 passed
  • Native XCTest:20 passed
  • flutter analyze:No issues found
  • layering / storage / l10n / tooling 等 repository gates 通過
  • iOS Debug build 成功
  • Runner.appDPIPWidgetsExtension.appex 均成功產出
  • git diff --check 通過

已知範圍

此 PR 只建立 Current Weather Widget 的第一層基礎。

以下功能會在後續獨立 PR 加入:

  • Widget 自己選擇「所在地」或 DPIP 已加入的地點
  • 每個 Widget instance 使用不同地點
  • App 未執行時由 Widget Extension 自行取得最新天氣
  • systemMedium / systemLarge
  • 逐時與多日天氣預報
  • Widget onboarding / 使用教學

目前 Widget 的新天氣資料仍由 DPIP App 取得並寫入 App Group;App 未執行時,不會自行向天氣 API 取得新的觀測資料。

WidgetKit timeline 可在 App 未執行時處理已排定的 stale 狀態,以及 snapshot 內攜帶的下一次日夜 transition;若 App 長時間未再次發布 snapshot,後續日夜週期不在此 PR 的保證範圍內。此機制不能視為即時天氣更新或災防警報機制。

Reviewer 建議重點

建議優先確認:

  • Dart → App Group → Swift Widget 的資料流與 schema contract
  • Widget snapshot storage / MethodChannel 邊界
  • Region 切換時舊 snapshot 的處理
  • stale 與 day/night timeline composition
  • v4 + v2/v3 backwards compatibility
  • Widget Extension target membership
  • Native localization resource 是否只進入 Widget Extension
  • 後續擴充 configurable Widget 與 self-refresh 時,現有架構是否有不必要耦合

檢查清單

  • tool/check/commits.sh origin/main..HEAD 通過
    —— commit 訊息就是更新日誌,格式見 commit.md
  • 一個 commit 一件事(這條 gate 驗不了,靠自己和 review)
  • mise exec -- flutter analyzemise exec -- flutter test 通過
  • 新的使用者可見字串皆有本地化
    - Flutter UI 維持 AppLocalizations
    - Widget Extension 使用原生 Localizable.xcstrings
  • 有 UI 變更的話:深色模式與文字顯示已確認;Widget UI 使用原生 SwiftUI layout

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

New(zh-Hant): iOS 天氣小工具支援繁體中文與英文介面
New(en-US): iOS weather widgets support Traditional Chinese and English
@YinCheng0106 YinCheng0106 self-assigned this Sep 16, 2026
@YinCheng0106 YinCheng0106 added the ios Issues or pull requests related to the iOS platform. label Sep 16, 2026
@YinCheng0106
YinCheng0106 changed the base branch from feat/ios-weather-widget to main September 16, 2026 15:11
@YinCheng0106
YinCheng0106 marked this pull request as draft September 16, 2026 15:26
@YinCheng0106
YinCheng0106 marked this pull request as ready for review September 16, 2026 15:26
Copilot AI lite review requested due to automatic review settings September 16, 2026 15:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.swift schedules only one future day/night transition.
  • Moderate (2 votes): ios/DPIPWidgets/DPIPWidgets.swift uses uncalibrated device dates for server observation timestamps.
  • Moderate (1 vote): ios/DPIPWidgets/DPIPWidgets.swift can remain in the post-transition state indefinitely.
  • Moderate (3 votes): lib/features/home/presentation/home_weather_controller.dart may retain a previous-region snapshot after relaunch.
  • Moderate (1 vote): lib/features/home/presentation/home_weather_controller.dart lacks latest-request-wins protection.
  • Nit (2 votes): lib/features/weather/domain/current_weather_widget_snapshot.dart duplicates weather-code classification.
  • Moderate (1 vote): lib/features/weather/domain/current_weather_widget_snapshot.dart persists 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 .never timeline. 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 invoking onRealtimeLoaded so 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.weather is the API's Traditional Chinese display label, and the Widget view renders snapshot.weather verbatim. 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.

Comment thread ios/DPIPWidgets/DPIPWidgets.swift Outdated
Comment thread lib/features/home/presentation/home_weather_controller.dart Outdated
Comment thread lib/features/weather/domain/current_weather_widget_snapshot.dart Outdated
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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 >= 4 treats 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 on schemaVersion == 4 so 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 _load has 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

Comment on lines +81 to +83
if (_regions.selectedCode != regionCode) {
return;
}
@lowrt lowrt added this to the v26.3 milestone Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ios Issues or pull requests related to the iOS platform. size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants