From 94177dbb0bc07aa0c27f74ca4fafc25d868a18b3 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Sun, 1 Nov 2026 01:53:36 +0700 Subject: [PATCH] Fix launch crash: rename SyncRecord.entity to entityName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- BalanceWatch/App/WatchAccountsView.swift | 2 +- BalanceWatch/App/WatchFeatures.swift | 4 ++-- BalanceWatch/Models/V2Records.swift | 8 +++++--- BalanceWatch/Shared/ServerSync.swift | 8 ++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/BalanceWatch/App/WatchAccountsView.swift b/BalanceWatch/App/WatchAccountsView.swift index 992fcd2..5af6157 100644 --- a/BalanceWatch/App/WatchAccountsView.swift +++ b/BalanceWatch/App/WatchAccountsView.swift @@ -20,7 +20,7 @@ extension EnvironmentValues { struct CurrencyContextProvider: View { @Query private var accounts: [FinanceAccount] @Query private var rates: [ExchangeRate] - @Query(filter: #Predicate { $0.entity == "setting" && $0.recordID == "baseCurrency" }) + @Query(filter: #Predicate { $0.entityName == "setting" && $0.recordID == "baseCurrency" }) private var baseSetting: [SyncRecord] @AppStorage("currencyCode") private var currencyCode = SupportedCurrency.RUB.rawValue @ViewBuilder let content: Content diff --git a/BalanceWatch/App/WatchFeatures.swift b/BalanceWatch/App/WatchFeatures.swift index abc4ed7..f6389f6 100644 --- a/BalanceWatch/App/WatchFeatures.swift +++ b/BalanceWatch/App/WatchFeatures.swift @@ -489,7 +489,7 @@ struct WatchSettingsView: View { @StateObject private var account = ServerAccountStore.shared @AppStorage("currencyCode") private var currencyCode = SupportedCurrency.RUB.rawValue @AppStorage("appTheme") private var appTheme = AppTheme.system.rawValue - @Query(filter: #Predicate { $0.entity == "setting" && $0.recordID == "baseCurrency" }) + @Query(filter: #Predicate { $0.entityName == "setting" && $0.recordID == "baseCurrency" }) private var baseSetting: [SyncRecord] private var currencyCodes: [String] { @@ -508,7 +508,7 @@ struct WatchSettingsView: View { record.payload = payload record.syncUpdatedAt = .now } 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)) } }) } diff --git a/BalanceWatch/Models/V2Records.swift b/BalanceWatch/Models/V2Records.swift index dc7ae05..d6232ca 100644 --- a/BalanceWatch/Models/V2Records.swift +++ b/BalanceWatch/Models/V2Records.swift @@ -70,13 +70,15 @@ final class ExchangeRate { /// `attachment`, `setting`. Payload хранится целиком, поэтому все поля сохраняются. @Model final class SyncRecord { - var entity: String = "" + /// Не `entity`: так называется встроенное свойство CoreData, и предикат + /// по нему ломает SQL-запрос SwiftData. + var entityName: String = "" var recordID: String = "" var payload: Data = Data() var syncUpdatedAt: Date = Date.now - init(entity: String, recordID: String, payload: Data, syncUpdatedAt: Date) { - self.entity = entity + init(entityName: String, recordID: String, payload: Data, syncUpdatedAt: Date) { + self.entityName = entityName self.recordID = recordID self.payload = payload self.syncUpdatedAt = syncUpdatedAt diff --git a/BalanceWatch/Shared/ServerSync.swift b/BalanceWatch/Shared/ServerSync.swift index 424e59f..1355c8a 100644 --- a/BalanceWatch/Shared/ServerSync.swift +++ b/BalanceWatch/Shared/ServerSync.swift @@ -498,9 +498,9 @@ final class ServerAccountStore: ObservableObject { private func pushSettings(context: ModelContext, after threshold: Date, through cutoff: Date, cursor: Int64) async throws { let lowerBound = threshold, upperBound = cutoff let items = try context.fetch(FetchDescriptor(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 { @@ -660,12 +660,12 @@ final class ServerAccountStore: ObservableObject { /// Регулярные операции часы не исполняют (§4). private func applyRecord(_ change: SyncChange, context: ModelContext) throws { let entity = change.entity, recordID = change.id - let existing = try context.fetch(FetchDescriptor(predicate:#Predicate { $0.entity == entity && $0.recordID == recordID })).first + let existing = try context.fetch(FetchDescriptor(predicate:#Predicate { $0.entityName == entity && $0.recordID == recordID })).first if let existing, existing.syncUpdatedAt > change.updatedAt { return } if change.deleted { if let existing { context.delete(existing) }; return } guard let data = change.payload else { return } 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? {