Release 1.3.1 (build 20): iCloud sync fix + sources search/filter
iCloud sync: - Fix: deploy CloudKit schema to production so exports work - Fix: PredictionCache marked syncable=NO (binary attr caused partial failures) - Fix: forceExportToiCloud uses createdAt+1ms for guaranteed persistent history - Fix: auto-deduplication on CloudKit import (processRemoteChanges) - Add: NSPersistentHistoryTrackingKey always enabled regardless of CloudKit state - Add: cloudKitForceReload notification so repositories re-fetch on remote changes Sources UI: - Add: horizontal category filter chips in SourceListView - Add: search toggle button with animated TextField Other: - Add: ITSAppUsesNonExemptEncryption=NO in Info.plist (skip manual compliance) - Add: fastlane submit lane with run_precheck_before_submit=false - Update: release notes all locales for 1.3.1 - Update: fastlane API key via pass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,8 @@ struct SettingsView: View {
|
||||
@AppStorage("lockOnLaunch") private var lockOnLaunch = true
|
||||
@AppStorage("lockOnBackground") private var lockOnBackground = false
|
||||
|
||||
@ObservedObject private var cloudStack = CoreDataStack.shared
|
||||
|
||||
@State private var showingPinSetup = false
|
||||
@State private var showingPinChange = false
|
||||
@State private var showingBiometricAlert = false
|
||||
@@ -22,6 +24,8 @@ struct SettingsView: View {
|
||||
@State private var showingPinVerifyForFaceId = false
|
||||
@State private var showingRestartAlert = false
|
||||
@State private var didLoadCloudSync = false
|
||||
@State private var isForceUploading = false
|
||||
@State private var forceUploadResult: String?
|
||||
@State private var backupToRestore: BackupRecord?
|
||||
|
||||
init(iapService: IAPService) {
|
||||
@@ -342,6 +346,104 @@ struct SettingsView: View {
|
||||
viewModel.refreshBackups()
|
||||
}
|
||||
|
||||
if cloudSyncEnabled {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
// Local data counts
|
||||
HStack {
|
||||
Label(
|
||||
"\(cloudStack.localSourceCount) sources · \(cloudStack.localSnapshotCount) snapshots",
|
||||
systemImage: "internaldrive"
|
||||
)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Refresh") {
|
||||
CoreDataStack.shared.forceReload()
|
||||
}
|
||||
.font(.subheadline)
|
||||
.disabled(cloudStack.isSyncing)
|
||||
}
|
||||
|
||||
// Import / export status
|
||||
if cloudStack.isSyncing {
|
||||
Label("Syncing with iCloud...", systemImage: "arrow.triangle.2.circlepath.icloud")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
HStack(spacing: 12) {
|
||||
if let date = cloudStack.lastImportDate {
|
||||
Label("↓ \(date.formatted(.relative(presentation: .named, unitsStyle: .abbreviated)))", systemImage: "icloud.and.arrow.down")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Label("No import yet", systemImage: "icloud.and.arrow.down")
|
||||
.font(.caption)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
|
||||
if let date = cloudStack.lastExportDate {
|
||||
Label("↑ \(date.formatted(.relative(presentation: .named, unitsStyle: .abbreviated)))", systemImage: "icloud.and.arrow.up")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Label("No export yet", systemImage: "icloud.and.arrow.up")
|
||||
.font(.caption)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CloudKit error (if any)
|
||||
if let error = cloudStack.lastSyncError {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Label("CloudKit error", systemImage: "exclamationmark.icloud")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(.red)
|
||||
Text(error)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.red)
|
||||
.lineLimit(6)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
|
||||
// Force upload — use when data exists locally but hasn't reached iCloud
|
||||
if cloudStack.localSourceCount > 0 {
|
||||
if let result = forceUploadResult {
|
||||
Label(result, systemImage: "checkmark.icloud")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Button {
|
||||
isForceUploading = true
|
||||
CoreDataStack.shared.forceExportToiCloud { count in
|
||||
isForceUploading = false
|
||||
forceUploadResult = String(
|
||||
format: NSLocalizedString("save_n_snapshots", comment: ""),
|
||||
count
|
||||
)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
|
||||
forceUploadResult = nil
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if isForceUploading {
|
||||
Label("Uploading...", systemImage: "arrow.triangle.2.circlepath.icloud")
|
||||
} else {
|
||||
Label("Force Upload to iCloud", systemImage: "icloud.and.arrow.up")
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.appPrimary)
|
||||
.disabled(isForceUploading)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
Toggle(
|
||||
isOn: Binding(
|
||||
get: { viewModel.backupsEnabled },
|
||||
|
||||
Reference in New Issue
Block a user