From 5583257f28cd4fee32768b23ce8c59509931b6b6 Mon Sep 17 00:00:00 2001 From: Trey t Date: Fri, 11 Mar 2022 11:44:44 -0600 Subject: [PATCH] closed #113 - import / export --- .../xcschemes/xcschememanagement.plist | 2 +- Shared/views/SettingsView/SettingsView.swift | 133 +++++++++++++++++- 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/Feels.xcodeproj/xcuserdata/treyt.xcuserdatad/xcschemes/xcschememanagement.plist b/Feels.xcodeproj/xcuserdata/treyt.xcuserdatad/xcschemes/xcschememanagement.plist index 78042d3..b3eaeb2 100644 --- a/Feels.xcodeproj/xcuserdata/treyt.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Feels.xcodeproj/xcuserdata/treyt.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ Feels (macOS).xcscheme_^#shared#^_ orderHint - 3 + 2 FeelsWidgetExtension.xcscheme_^#shared#^_ diff --git a/Shared/views/SettingsView/SettingsView.swift b/Shared/views/SettingsView/SettingsView.swift index c5b9989..ec43061 100644 --- a/Shared/views/SettingsView/SettingsView.swift +++ b/Shared/views/SettingsView/SettingsView.swift @@ -7,12 +7,17 @@ import SwiftUI import CloudKitSyncMonitor +import UniformTypeIdentifiers struct SettingsView: View { @Environment(\.dismiss) var dismiss - @State private var showOnboarding = false + @State private var showingExporter = false + @State private var showingImporter = false + @State private var importContent = "" + @State private var showOnboarding = false + @State private var showSpecialThanks = false @State private var showWhyBGMode = false @ObservedObject var syncMonitor = SyncMonitor.shared @@ -44,6 +49,9 @@ struct SettingsView: View { if useCloudKit { cloudKitStatus } + + exportData + importData } Spacer() @@ -62,6 +70,57 @@ struct SettingsView: View { theme.currentTheme.bg .edgesIgnoringSafeArea(.all) ) + .fileExporter(isPresented: $showingExporter, + documents: [ + TextFile() + ], + contentType: .plainText, + onCompletion: { result in + switch result { + case .success(let url): + print("Saved to \(url)") + case .failure(let error): + print(error.localizedDescription) + } + }) + .fileImporter(isPresented: $showingImporter, allowedContentTypes: [.text], + allowsMultipleSelection: false) { result in + do { + guard let selectedFile: URL = try result.get().first else { return } + if selectedFile.startAccessingSecurityScopedResource() { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss +0000" + + guard let input = String(data: try Data(contentsOf: selectedFile), encoding: .utf8) else { return } + defer { selectedFile.stopAccessingSecurityScopedResource() } + + var rows = input.components(separatedBy: "\n") + rows.removeFirst() + for row in rows { + let columns = row.components(separatedBy: ",") + if columns.count != 7 { + continue + } + let moodEntry = MoodEntry(context: PersistenceController.shared.viewContext) + moodEntry.canDelete = Bool(columns[0])! + moodEntry.canEdit = Bool(columns[1])! + moodEntry.entryType = Int16(columns[2])! + moodEntry.forDate = dateFormatter.date(from: columns[3]) + moodEntry.moodValue = Int16(columns[4])! + moodEntry.timestamp = dateFormatter.date(from: columns[5]) + moodEntry.weekDay = Int16(columns[6])! + try! PersistenceController.shared.viewContext.save() + } + PersistenceController.shared.saveAndRunDataListerners() + } else { + // Handle denied access + } + } catch { + // Handle failure. + print("Unable to read file contents") + print(error.localizedDescription) + } + } } private var closeButtonView: some View { @@ -222,6 +281,34 @@ struct SettingsView: View { .cornerRadius(10, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } + private var exportData: some View { + ZStack { + theme.currentTheme.secondaryBGColor + Button(action: { + showingExporter.toggle() + }, label: { + Text("Export") + }) + .padding() + } + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(10, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) + } + + private var importData: some View { + ZStack { + theme.currentTheme.secondaryBGColor + Button(action: { + showingImporter.toggle() + }, label: { + Text("Import") + }) + .padding() + } + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(10, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) + } + private var randomIcons: some View { ZStack { theme.currentTheme.secondaryBGColor @@ -354,6 +441,50 @@ struct SettingsView: View { } } +struct TextFile: FileDocument { + // tell the system we support only plain text + static var readableContentTypes = [UTType.plainText] + + // by default our document is empty + var text = "" + + // a simple initializer that creates new, empty documents + init() { + let entries = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0), + endDate: Date(), + includedDays: []) + + var csvString = "canDelete,canEdit,entryType,forDate,moodValue,timestamp,weekDay\n" + for entry in entries { + let canDelete = entry.canDelete + let canEdit = entry.canEdit + let entryType = entry.entryType + let forDate = entry.forDate! + let moodValue = entry.moodValue + let timestamp = entry.timestamp! + let weekDay = entry.weekDay + + let dataString = "\(canDelete),\(canEdit),\(entryType),\(String(describing: forDate)),\(moodValue),\(String(describing:timestamp)),\(weekDay)\n" + print("DATA: \(dataString)") + csvString = csvString.appending(dataString) + } + text = csvString + } + + // this initializer loads data that has been saved previously + init(configuration: ReadConfiguration) throws { + if let data = configuration.file.regularFileContents { + text = String(decoding: data, as: UTF8.self) + } + } + + // this will be called when the system wants to write our data to disk + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + let data = Data(text.utf8) + return FileWrapper(regularFileWithContents: data) + } +} + struct SettingsView_Previews: PreviewProvider { static var previews: some View { SettingsView()