Fix config data loss on startup and Codable forward-compatibility

- AppState.init: use _stocks = Published(wrappedValue:) to bypass didSet,
  preventing empty array from overwriting disk on decode failure
- AppState.saveStocks: guard against overwriting non-empty file with []
- Stock: explicit Codable extension using decodeIfPresent for optional fields,
  so missing fields in old JSON use defaults instead of failing the whole decode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
vgearen
2026-03-18 09:17:14 +08:00
co-authored by Claude Sonnet 4.6
parent 3227b4fcda
commit a119f89624
3 changed files with 23 additions and 2 deletions
+17 -1
View File
@@ -12,7 +12,7 @@ enum Market: String, Codable, CaseIterable {
}
}
struct Stock: Identifiable, Codable, Equatable {
struct Stock: Identifiable, Equatable {
var id: String // 股票代码,如 "sh600000"、"usr_aapl"、"hk00700"
var name: String
var market: Market
@@ -31,3 +31,19 @@ struct Stock: Identifiable, Codable, Equatable {
return quote.change * shares
}
}
// MARK: - Codable(容错:未知字段用默认值,避免新版本解码老数据时整条失败)
extension Stock: Codable {
enum CodingKeys: String, CodingKey {
case id, name, market, costPrice, holdingShares
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(String.self, forKey: .id)
name = try c.decode(String.self, forKey: .name)
market = try c.decode(Market.self, forKey: .market)
costPrice = try c.decodeIfPresent(Double.self, forKey: .costPrice)
holdingShares = try c.decodeIfPresent(Double.self, forKey: .holdingShares)
}
}
+5 -1
View File
@@ -1,6 +1,7 @@
import SwiftUI
import Combine
import AppKit
import os
@MainActor
final class AppState: ObservableObject {
@@ -40,6 +41,8 @@ final class AppState: ObservableObject {
}
private func saveStocks(_ stocks: [Stock]) {
// 防止把空数组覆盖掉磁盘上有内容的文件
if stocks.isEmpty, !Self.loadStocks().isEmpty { return }
guard let data = try? JSONEncoder().encode(stocks) else { return }
Self.backupIfNeeded()
try? data.write(to: Self.stocksFileURL, options: .atomic)
@@ -84,7 +87,8 @@ final class AppState: ObservableObject {
init() {
appLogger.info("AppState init start")
logToFile("AppState init start")
self.stocks = Self.loadStocks()
// 用 _stocks 直接赋值,绕过 didSet,避免加载失败时把空数组覆盖写回磁盘
_stocks = Published(wrappedValue: Self.loadStocks())
appLogger.info("AppState stocks loaded: \(self.stocks.count)")
logToFile("AppState stocks loaded: \(self.stocks.count)")
setupScheduler()
+1
View File
@@ -1,6 +1,7 @@
import SwiftUI
import ServiceManagement
import AppKit
import os
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {