Skip to content

Commit 3640b5d

Browse files
committed
Резервная копия
Добавил экран для работы с данными приложения, где можно: - создать резервную копию - восстановить данные из резервной копии - удалить все данные Сделал мелкий рефактор и удалил явные вызовы modelContext.save(), т.к. данные сохраняются автоматически при смене жизненного цикла приложения (если были изменения в modelContext)
1 parent 9cc93e7 commit 3640b5d

File tree

12 files changed

+313
-50
lines changed

12 files changed

+313
-50
lines changed

SwiftUI-Days.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@
531531
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
532532
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
533533
MACOSX_DEPLOYMENT_TARGET = 14.0;
534-
MARKETING_VERSION = 1.1;
534+
MARKETING_VERSION = 1.2;
535535
PRODUCT_BUNDLE_IDENTIFIER = "com.oleg991.SwiftUI-Days";
536536
PRODUCT_NAME = "$(TARGET_NAME)";
537537
SDKROOT = auto;
@@ -576,7 +576,7 @@
576576
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
577577
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
578578
MACOSX_DEPLOYMENT_TARGET = 14.0;
579-
MARKETING_VERSION = 1.1;
579+
MARKETING_VERSION = 1.2;
580580
PRODUCT_BUNDLE_IDENTIFIER = "com.oleg991.SwiftUI-Days";
581581
PRODUCT_NAME = "$(TARGET_NAME)";
582582
SDKROOT = auto;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// BackupFileDocument.swift
3+
// SwiftUI-Days
4+
//
5+
// Created by Oleg991 on 06.04.2025.
6+
//
7+
8+
import SwiftUI
9+
import UniformTypeIdentifiers
10+
11+
struct BackupFileDocument: FileDocument {
12+
static var readableContentTypes: [UTType] { [.json] }
13+
static var writableContentTypes: [UTType] { [.json] }
14+
static func makeBackupItem(with item: Item) -> BackupItem {
15+
.init(title: item.title, details: item.details, timestamp: item.timestamp)
16+
}
17+
18+
let items: [BackupItem]
19+
20+
init(items: [BackupItem]) {
21+
self.items = items
22+
}
23+
24+
init(configuration: ReadConfiguration) throws {
25+
guard let data = configuration.file.regularFileContents else {
26+
throw CocoaError(.fileReadCorruptFile)
27+
}
28+
items = try JSONDecoder().decode([BackupItem].self, from: data)
29+
}
30+
31+
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
32+
let data = try JSONEncoder().encode(items)
33+
return FileWrapper(regularFileWithContents: data)
34+
}
35+
}
36+
37+
extension BackupFileDocument {
38+
struct BackupItem: Codable {
39+
let title: String
40+
let details: String
41+
let timestamp: Date
42+
43+
var realItem: Item {
44+
.init(title: title, details: details, timestamp: timestamp)
45+
}
46+
}
47+
}

SwiftUI-Days/Screens/Detail/EditItemScreen.swift renamed to SwiftUI-Days/Screens/Main/Detail/EditItemScreen.swift

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,19 +97,14 @@ struct EditItemScreen: View {
9797
}
9898

9999
private func save() {
100-
if let oldItem {
101-
oldItem.title = title
102-
oldItem.details = details
103-
oldItem.timestamp = timestamp
104-
} else {
105-
let item = Item(title: title, details: details, timestamp: timestamp)
106-
modelContext.insert(item)
107-
}
108-
do {
109-
try modelContext.save()
110-
} catch {
111-
assertionFailure(error.localizedDescription)
100+
guard let oldItem else {
101+
let newItem = Item(title: title, details: details, timestamp: timestamp)
102+
modelContext.insert(newItem)
103+
return
112104
}
105+
oldItem.title = title
106+
oldItem.details = details
107+
oldItem.timestamp = timestamp
113108
}
114109
}
115110

SwiftUI-Days/Screens/Main/MainScreen+ListView.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,6 @@ extension MainScreen {
3636
.swipeActions {
3737
DaysDeleteButton {
3838
modelContext.delete(item)
39-
do {
40-
try modelContext.save()
41-
} catch {
42-
assertionFailure(error.localizedDescription)
43-
}
4439
}
4540
DaysEditButton { editItem = item }
4641
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//
2+
// AppDataScreen.swift
3+
// SwiftUI-Days
4+
//
5+
// Created by Oleg991 on 06.04.2025.
6+
//
7+
8+
import SwiftUI
9+
import SwiftData
10+
11+
struct AppDataScreen: View {
12+
@Environment(\.modelContext) private var modelContext
13+
@Query private var items: [Item]
14+
@State private var showDeleteDataConfirmation = false
15+
@State private var isCreatingBackup = false
16+
@State private var isRestoringFromBackup = false
17+
@State private var showResult = false
18+
@State private var operationResult: OperationResult?
19+
20+
var body: some View {
21+
VStack(spacing: 16) {
22+
Group {
23+
backupDataButton
24+
restoreDataButton
25+
if !items.isEmpty {
26+
removeAllDataButton
27+
}
28+
}
29+
.buttonStyle(.borderedProminent)
30+
.foregroundStyle(.buttonTint)
31+
}
32+
.animation(.default, value: items.isEmpty)
33+
.alert(
34+
operationResult?.title ?? "",
35+
isPresented: $showResult,
36+
presenting: operationResult,
37+
actions: { _ in
38+
Button("Ok") {}
39+
},
40+
message: { result in
41+
Text(result.message)
42+
}
43+
)
44+
.navigationTitle("App data")
45+
}
46+
47+
private var backupDataButton: some View {
48+
Button("Create a backup") {
49+
isCreatingBackup.toggle()
50+
}
51+
.accessibilityIdentifier("backupDataButton")
52+
.fileExporter(
53+
isPresented: $isCreatingBackup,
54+
document: BackupFileDocument(items: items.map(BackupFileDocument.makeBackupItem)),
55+
contentType: .json,
56+
defaultFilename: "Days backup"
57+
) { result in
58+
switch result {
59+
case .success:
60+
operationResult = .backupSuccess
61+
case let .failure(error):
62+
operationResult = .error(error.localizedDescription)
63+
}
64+
showResult = true
65+
}
66+
}
67+
68+
private var restoreDataButton: some View {
69+
Button("Restore from backup") {
70+
isRestoringFromBackup.toggle()
71+
}
72+
.accessibilityIdentifier("restoreDataButton")
73+
.fileImporter(
74+
isPresented: $isRestoringFromBackup,
75+
allowedContentTypes: [.json],
76+
allowsMultipleSelection: false
77+
) { result in
78+
switch result {
79+
case let .success(urls):
80+
if let url = urls.first,
81+
url.startAccessingSecurityScopedResource(),
82+
let data = try? Data(contentsOf: url),
83+
let importedItems = try? JSONDecoder().decode([BackupFileDocument.BackupItem].self, from: data) {
84+
defer { url.stopAccessingSecurityScopedResource() }
85+
let mappedRealItems = importedItems.map(\.realItem)
86+
mappedRealItems.forEach { modelContext.insert($0) }
87+
operationResult = .restoreSuccess
88+
} else {
89+
operationResult = .failedToRestore
90+
}
91+
case let .failure(error):
92+
operationResult = .error(error.localizedDescription)
93+
}
94+
showResult = true
95+
}
96+
}
97+
98+
private var removeAllDataButton: some View {
99+
Button("Delete all data", role: .destructive) {
100+
showDeleteDataConfirmation.toggle()
101+
}
102+
.accessibilityIdentifier("removeAllDataButton")
103+
.transition(.slide.combined(with: .scale).combined(with: .opacity))
104+
.confirmationDialog(
105+
"Do you want to delete all data permanently?",
106+
isPresented: $showDeleteDataConfirmation,
107+
titleVisibility: .visible
108+
) {
109+
Button("Delete", role: .destructive) {
110+
do {
111+
try modelContext.delete(model: Item.self)
112+
} catch {
113+
assertionFailure(error.localizedDescription)
114+
operationResult = .error(error.localizedDescription)
115+
showResult = true
116+
}
117+
}
118+
.accessibilityIdentifier("confirmRemoveAllDataButton")
119+
}
120+
}
121+
}
122+
123+
extension AppDataScreen {
124+
private enum OperationResult: Equatable {
125+
case backupSuccess
126+
case restoreSuccess
127+
case failedToRestore
128+
case error(String)
129+
130+
var title: LocalizedStringKey {
131+
switch self {
132+
case .backupSuccess, .restoreSuccess: "Done"
133+
case .failedToRestore, .error: "Error"
134+
}
135+
}
136+
137+
var message: LocalizedStringKey {
138+
switch self {
139+
case .backupSuccess: "Backup data saved"
140+
case .restoreSuccess: "Data restored from backup"
141+
case .failedToRestore: "Unable to recover data from the selected file"
142+
case let .error(message): .init(message)
143+
}
144+
}
145+
}
146+
}
147+
148+
#if DEBUG
149+
#Preview {
150+
NavigationStack {
151+
AppDataScreen()
152+
.environment(AppSettings())
153+
.modelContainer(PreviewModelContainer.make(with: Item.makeList()))
154+
}
155+
}
156+
#endif

0 commit comments

Comments
 (0)