Fix launch crash: rename SyncRecord.entity to entityName
Build / Build (watchOS Simulator) (push) Canceled after 0s
Build / Build (watchOS Device) (push) Canceled after 0s

CoreData treats `entity` as the managed object's entity, so the predicate
`entity == "setting"` became a fetch of an entity named "setting" and the
app aborted at launch ("setting" is not a valid entity name). The model is
new in the unreleased contract v2 change, so no migration is needed.

Verified on the watchOS 27.2 simulator: launch, login to a local
BalanceServer v2, total balance 57 800 ₽ with a USD account converted at
the synced rate and a cross-currency transfer, per-account balances
39 800 ₽ and $200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wtandClaude Opus 5.5 committed 2026-11-01 01:53:36 +07:00
1 parent d0b023b1ba
commit 94177dbb0b
4 files changed
+12 -10

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ extension EnvironmentValues {
struct CurrencyContextProvider<Content: View>: View { struct CurrencyContextProvider<Content: View>: View {
@Query private var accounts: [FinanceAccount] @Query private var accounts: [FinanceAccount]
@Query private var rates: [ExchangeRate] @Query private var rates: [ExchangeRate]
@Query(filter: #Predicate<SyncRecord> { $0.entity == "setting" && $0.recordID == "baseCurrency" }) @Query(filter: #Predicate<SyncRecord> { $0.entityName == "setting" && $0.recordID == "baseCurrency" })
private var baseSetting: [SyncRecord] private var baseSetting: [SyncRecord]
@AppStorage("currencyCode") private var currencyCode = SupportedCurrency.RUB.rawValue @AppStorage("currencyCode") private var currencyCode = SupportedCurrency.RUB.rawValue
@ViewBuilder let content: Content @ViewBuilder let content: Content
+2 -2
View File
@@ -489,7 +489,7 @@ struct WatchSettingsView: View {
@StateObject private var account = ServerAccountStore.shared @StateObject private var account = ServerAccountStore.shared
@AppStorage("currencyCode") private var currencyCode = SupportedCurrency.RUB.rawValue @AppStorage("currencyCode") private var currencyCode = SupportedCurrency.RUB.rawValue
@AppStorage("appTheme") private var appTheme = AppTheme.system.rawValue @AppStorage("appTheme") private var appTheme = AppTheme.system.rawValue
@Query(filter: #Predicate<SyncRecord> { $0.entity == "setting" && $0.recordID == "baseCurrency" }) @Query(filter: #Predicate<SyncRecord> { $0.entityName == "setting" && $0.recordID == "baseCurrency" })
private var baseSetting: [SyncRecord] private var baseSetting: [SyncRecord]
private var currencyCodes: [String] { private var currencyCodes: [String] {
@@ -508,7 +508,7 @@ struct WatchSettingsView: View {
record.payload = payload record.payload = payload
record.syncUpdatedAt = .now record.syncUpdatedAt = .now
} else { } else {
modelContext.insert(SyncRecord(entity: "setting", recordID: SyncRecord.baseCurrencyKey, payload: payload, syncUpdatedAt: .now)) modelContext.insert(SyncRecord(entityName: "setting", recordID: SyncRecord.baseCurrencyKey, payload: payload, syncUpdatedAt: .now))
} }
}) })
} }
+5 -3
View File
@@ -70,13 +70,15 @@ final class ExchangeRate {
/// `attachment`, `setting`. Payload хранится целиком, поэтому все поля сохраняются. /// `attachment`, `setting`. Payload хранится целиком, поэтому все поля сохраняются.
@Model @Model
final class SyncRecord { final class SyncRecord {
var entity: String = "" /// Не `entity`: так называется встроенное свойство CoreData, и предикат
/// по нему ломает SQL-запрос SwiftData.
var entityName: String = ""
var recordID: String = "" var recordID: String = ""
var payload: Data = Data() var payload: Data = Data()
var syncUpdatedAt: Date = Date.now var syncUpdatedAt: Date = Date.now
init(entity: String, recordID: String, payload: Data, syncUpdatedAt: Date) { init(entityName: String, recordID: String, payload: Data, syncUpdatedAt: Date) {
self.entity = entity self.entityName = entityName
self.recordID = recordID self.recordID = recordID
self.payload = payload self.payload = payload
self.syncUpdatedAt = syncUpdatedAt self.syncUpdatedAt = syncUpdatedAt
+4 -4
View File
@@ -498,9 +498,9 @@ final class ServerAccountStore: ObservableObject {
private func pushSettings(context: ModelContext, after threshold: Date, through cutoff: Date, cursor: Int64) async throws { private func pushSettings(context: ModelContext, after threshold: Date, through cutoff: Date, cursor: Int64) async throws {
let lowerBound = threshold, upperBound = cutoff let lowerBound = threshold, upperBound = cutoff
let items = try context.fetch(FetchDescriptor<SyncRecord>(predicate: #Predicate { item in let items = try context.fetch(FetchDescriptor<SyncRecord>(predicate: #Predicate { item in
item.entity == "setting" && item.syncUpdatedAt >= lowerBound && item.syncUpdatedAt <= upperBound item.entityName == "setting" && item.syncUpdatedAt >= lowerBound && item.syncUpdatedAt <= upperBound
})) }))
try await sendPush(items.map { SyncChange(entity:$0.entity, id:$0.recordID, deleted:false, updatedAt:$0.syncUpdatedAt, payload:$0.payload) }, cursor:cursor) try await sendPush(items.map { SyncChange(entity:$0.entityName, id:$0.recordID, deleted:false, updatedAt:$0.syncUpdatedAt, payload:$0.payload) }, cursor:cursor)
} }
private func pushTombstones(through cutoff: Date, cursor: Int64) async throws { private func pushTombstones(through cutoff: Date, cursor: Int64) async throws {
@@ -660,12 +660,12 @@ final class ServerAccountStore: ObservableObject {
/// Регулярные операции часы не исполняют (§4). /// Регулярные операции часы не исполняют (§4).
private func applyRecord(_ change: SyncChange, context: ModelContext) throws { private func applyRecord(_ change: SyncChange, context: ModelContext) throws {
let entity = change.entity, recordID = change.id let entity = change.entity, recordID = change.id
let existing = try context.fetch(FetchDescriptor<SyncRecord>(predicate:#Predicate { $0.entity == entity && $0.recordID == recordID })).first let existing = try context.fetch(FetchDescriptor<SyncRecord>(predicate:#Predicate { $0.entityName == entity && $0.recordID == recordID })).first
if let existing, existing.syncUpdatedAt > change.updatedAt { return } if let existing, existing.syncUpdatedAt > change.updatedAt { return }
if change.deleted { if let existing { context.delete(existing) }; return } if change.deleted { if let existing { context.delete(existing) }; return }
guard let data = change.payload else { return } guard let data = change.payload else { return }
if let existing { existing.payload = data; existing.syncUpdatedAt = change.updatedAt } if let existing { existing.payload = data; existing.syncUpdatedAt = change.updatedAt }
else { context.insert(SyncRecord(entity:entity, recordID:recordID, payload:data, syncUpdatedAt:change.updatedAt)) } else { context.insert(SyncRecord(entityName:entity, recordID:recordID, payload:data, syncUpdatedAt:change.updatedAt)) }
} }
private func transaction(id: UUID, context: ModelContext) throws -> FinanceTransaction? { private func transaction(id: UUID, context: ModelContext) throws -> FinanceTransaction? {