Created
December 8, 2024 14:24
-
-
Save lifeutilityapps/a15c8e34163a203ea188ab00ca96be02 to your computer and use it in GitHub Desktop.
A simple full screen cover view that allows the user to export their data.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ExportUserDataView.swift | |
// DownPay for iOS | |
// | |
// Created by Life Utility Apps on 12/6/24. | |
// | |
import SwiftUI | |
enum EAppUserDataExportFileType: String, Identifiable, CaseIterable { | |
case csv = "csv" | |
case json = "json" | |
var id: String { | |
return self.rawValue | |
} | |
} | |
struct ExportUserDataView: View { | |
@Environment(\.managedObjectContext) private var viewContext | |
@Environment(\.colorScheme) private var colorScheme | |
let onClose: () -> Void | |
let savingsObjects: [UserSavingsObject] | |
let debtObjects: [UserDebtObject] | |
let vehicleObjects: [AssetUserVehicleObject] | |
let realEstateObjects: [AssetUserRealEstateObject] | |
let educationObjects: [AssetUserEducationObject] | |
@State private var isLoading = true | |
@State private var activeExportFileType: EAppUserDataExportFileType? = nil | |
@ScaledMetric var spacerHeight = SCL.ui.screenHeight * 0.08 | |
var body: some View { | |
VStack(alignment: .leading, spacing: 0) { | |
VStack(alignment: .leading, spacing: SCL.ui.spacing) { | |
ScrollView { | |
VStack(alignment: .leading, spacing: SCL.ui.spacing) { | |
Rectangle().fill(.clear) | |
.frame(width: 10, height: spacerHeight) | |
AppLogo(size: 90, hideTitle: true, overrideIconVariant: colorScheme.isDarkMode ? .darkMode : .defaultIcon, cornerRadius: 16) | |
.shadow(color: SCL.colors.labelSecondaryColor.opacity(0.90), radius: 1) | |
.shadow(color: SCL.colors.labelSecondaryColor.opacity(0.2), radius: 15) | |
VStack(alignment: .leading, spacing: 0) { | |
Text("My App Data") //\(Global.Legal.appDisplayName) | |
.font(.system(size: 30)) | |
.fontWeight(.bold) | |
} | |
Text("Your data includes the following:") | |
.font(.callout) | |
.foregroundStyle(SCL.colors.labelSecondaryColor) | |
if(isLoading) { | |
HStack { | |
Spacer() | |
ProgressView() | |
Spacer() | |
} | |
.frame(height: spacerHeight * 2) | |
} else { | |
ListOfUserDataView(savingsObjects: savingsObjects, debtObjects: debtObjects, vehicleObjects: vehicleObjects, realEstateObjects: realEstateObjects, educationObjects: educationObjects, isActive: activeExportFileType != nil) | |
} | |
Spacer() | |
}.padding(SCL.ui.spacing * 5) | |
} | |
.scrollIndicators(.never) | |
Spacer() | |
if(!isLoading){ | |
ExportStackedButtons(activeExportFileType: $activeExportFileType, onActivate: { exportType in | |
initDataExport(exportType) | |
}) | |
.padding(.horizontal, SCL.ui.spacing * 5) | |
} | |
} | |
.overlay(alignment: .topTrailing, content: { | |
if(!isLoading && activeExportFileType == nil){ | |
Button { | |
onClose() | |
} label: { | |
StandardCircleIcon(iconName: SCL.icons.close, size: 45) | |
.opacity(0.25) | |
} | |
.background(.ultraThinMaterial) | |
.clipShape(Circle()) | |
.padding() | |
} | |
}) | |
.background(content: { | |
VStack { | |
Image("seamless-money-pattern-tall") | |
.resizable() | |
.aspectRatio(contentMode: .fill) | |
.frame(height: SCL.ui.screenHeight * 0.7) | |
.opacity(colorScheme.isDarkMode ? 0.35 : 0.25) | |
.grayscale(colorScheme.isDarkMode ? 1 : 0) | |
.mask(LinearGradient(gradient: Gradient(colors: [.black, .black.opacity(0.85), .clear]), startPoint: .top, endPoint: .bottom)) | |
Spacer() | |
} | |
.ignoresSafeArea(.all) | |
}) | |
} | |
.onAppear { | |
handleAppear() | |
} | |
.onDisappear { | |
handleDisappear() | |
} | |
} | |
func handleAppear() { | |
SCL.util.asyncAfter(delay: 0.5) { | |
isLoading = false | |
} | |
} | |
func handleDisappear(){ | |
} | |
func initDataExport(_ exportType: EAppUserDataExportFileType) { | |
SCL.util.asyncAfter(delay: 3) { | |
isLoading = true | |
activeExportFileType = nil | |
} | |
SCL.util.asyncAfter(delay: 4, useWithAnimation: false) { | |
onClose() | |
handleExportData(exportType) | |
} | |
} | |
func handleExportData(_ exportType: EAppUserDataExportFileType) { | |
SCL.util.asyncAfter(delay: 0.5, useWithAnimation: false) { | |
switch exportType { | |
case .csv: | |
print("TODO") | |
case .json: | |
exportCoreDataObjectsToJSON(from: viewContext) { url in | |
if let _url = url { | |
showShareSheet(with: [_url]) | |
} | |
} | |
} | |
} | |
} | |
} | |
struct ExportStackedButtons: View { | |
@Environment(\.colorScheme) private var colorScheme | |
@Binding var activeExportFileType: EAppUserDataExportFileType? | |
let onActivate: (EAppUserDataExportFileType) -> Void | |
var body: some View { | |
VStack(alignment: .leading, spacing: SCL.ui.spacing * 2) { | |
HStack { | |
Spacer() | |
Text("Choose an option") | |
.font(.caption) | |
.foregroundStyle(SCL.colors.labelSecondaryColor) | |
.opacity(activeExportFileType == nil ? 1 : 0) | |
Spacer() | |
} | |
ExportProgressButton(activeExportButton: activeExportFileType, isSelected: activeExportFileType == .csv, onClick: { | |
handleExport(.csv) | |
}, title: "Raw CSV", description: "Opens in most speadsheet software", imageName: "spreadsheet-icon", backgroundColor: colorScheme.isLightMode ? SCL.colors.backgroundColor : colorScheme.colorFormSection) | |
ExportProgressButton(activeExportButton: activeExportFileType, isSelected: activeExportFileType == .json, onClick: { | |
handleExport(.json) | |
}, title: "Complete App Backup", description: "Useful for migrating or safe keeping", imageName: "file-folder-icon", backgroundColor: colorScheme.isLightMode ? SCL.colors.backgroundColor : colorScheme.colorFormSection) | |
} | |
} | |
func handleExport(_ exportType: EAppUserDataExportFileType) { | |
onActivate(exportType) | |
withAnimation { | |
activeExportFileType = exportType | |
} | |
} | |
} | |
struct ExportProgressButton: View { | |
@Environment(\.colorScheme) private var colorScheme | |
let activeExportButton: EAppUserDataExportFileType? | |
var isSelected: Bool = false | |
let onClick: () -> Void | |
let title: String | |
var description: String = "" | |
var imageName: String = "" | |
var backgroundColor: Color = SCL.colors.backgroundFormSectionColor | |
var imageSize: CGFloat = 35 | |
@ScaledMetric var buttonHeight: CGFloat = 65 | |
let gradientColors: [Color] = [SCL.colors.green.opacity(0.5), SCL.colors.BrandColor.greenDeep] | |
var shouldBeDisabled: Bool { | |
var result = false | |
if(!isSelected && activeExportButton != nil) { | |
result = true | |
} | |
return result | |
} | |
var body: some View { | |
HStack { | |
if imageName.isNotEmpty { | |
if(isSelected) { | |
ProgressView() | |
.frame(width: imageSize, height: imageSize) | |
} else { | |
Image(imageName) | |
.resizable() | |
.aspectRatio(contentMode: .fit) | |
.frame(width: imageSize, height: imageSize) | |
.opacity(shouldBeDisabled ? 0.25 : 1) | |
} | |
} | |
VStack(alignment: .leading) { | |
Text(title) | |
.font(.headline) | |
.foregroundStyle(SCL.colors.labelColor) | |
Text(description) | |
.font(.caption) | |
.foregroundStyle(SCL.colors.labelSecondaryColor) | |
} | |
.opacity(shouldBeDisabled ? 0.25 : 1) | |
Spacer() | |
} | |
.padding() | |
.frame(height: buttonHeight) | |
.background( | |
RoundedRectangle(cornerRadius: 10) | |
.stroke(SCL.colors.labelSecondaryColor.opacity(0.5), lineWidth: 2) | |
.fill(backgroundColor) | |
.shadow(color: SCL.colors.labelColor.opacity(colorScheme.isLightMode ? 0.15 : 0.1), radius: colorScheme.isLightMode ? 17 : 12) | |
.overlay { | |
StandardProgressBar(percentage: isSelected ? 100 : 0, animationDuration: 4, colors: gradientColors, completedColors: gradientColors, height: buttonHeight, cornerRadius: 0, startAtZero: true) | |
.mask { | |
RoundedRectangle(cornerRadius: 10) | |
} | |
.opacity(isSelected ? 1 : 0) | |
} | |
) | |
.grayscale(shouldBeDisabled ? 1 : 0) | |
.opacity(shouldBeDisabled ? 0.75 : 1) | |
.onTapGesture { | |
if(!shouldBeDisabled){ | |
onClick() | |
} | |
} | |
} | |
} | |
struct ListOfUserDataView: View { | |
let savingsObjects: [UserSavingsObject] | |
let debtObjects: [UserDebtObject] | |
let vehicleObjects: [AssetUserVehicleObject] | |
let realEstateObjects: [AssetUserRealEstateObject] | |
let educationObjects: [AssetUserEducationObject] | |
let isActive: Bool | |
@State var animate1 = false | |
@State var animate2 = false | |
@State var animate3 = false | |
@State var animate4 = false | |
@State var animate5 = false | |
func beginAnimation() { | |
let delay = 0.3 | |
SCL.util.asyncAfter(delay: delay) { | |
animate1 = true | |
SCL.util.vibrateDevice() | |
} | |
SCL.util.asyncAfter(delay: delay * 2) { | |
animate2 = true | |
SCL.util.vibrateDevice() | |
} | |
SCL.util.asyncAfter(delay: delay * 3) { | |
animate3 = true | |
SCL.util.vibrateDevice() | |
} | |
SCL.util.asyncAfter(delay: delay * 4) { | |
animate4 = true | |
SCL.util.vibrateDevice() | |
} | |
SCL.util.asyncAfter(delay: delay * 5) { | |
animate5 = true | |
SCL.util.vibrateDevice() | |
} | |
} | |
var body: some View { | |
VStack(alignment: .leading) { | |
StandardColorChip(text: labelSavings, color: animate1 ? SCL.colors.BrandColor.greenDeep : SCL.colors.labelSecondaryColor, icon: animate1 ? SCL.icons.checkCircle : SCL.icons.cash, weight: animate1 ? .bold : .regular, isOpaque: true) | |
StandardColorChip(text: labelDebts, color: animate2 ? SCL.colors.BrandColor.greenDeep : SCL.colors.labelSecondaryColor, icon: animate2 ? SCL.icons.checkCircle : SCL.icons.debt, weight: animate2 ? .bold : .regular, isOpaque: true) | |
StandardColorChip(text: labelVehicles, color: animate3 ? SCL.colors.BrandColor.greenDeep : SCL.colors.labelSecondaryColor, icon: animate3 ? SCL.icons.checkCircle : SCL.icons.car, weight: animate3 ? .bold : .regular, isOpaque: true) | |
StandardColorChip(text: labelRealEstate, color: animate4 ? SCL.colors.BrandColor.greenDeep : SCL.colors.labelSecondaryColor, icon: animate4 ? SCL.icons.checkCircle : SCL.icons.houseFlag, weight: animate4 ? .bold : .regular, isOpaque: true) | |
StandardColorChip(text: labelEducation, color: animate5 ? SCL.colors.BrandColor.greenDeep : SCL.colors.labelSecondaryColor, icon: animate5 ? SCL.icons.checkCircle : SCL.icons.graduationCap, weight: animate5 ? .bold : .regular, isOpaque: true) | |
} | |
.onChange(of: isActive) { _, val in | |
if(val) { | |
beginAnimation() | |
} | |
} | |
} | |
} | |
extension ListOfUserDataView { | |
var labelSavings: String { | |
return "\(savingsObjects.count == 0 ? "No" : Int(savingsObjects.count).withCommas()) \(savingsObjects.isSingle ? SharedText.EntityName.downpayment : SharedText.EntityName.downpaymentPlural)" | |
} | |
var labelDebts: String { | |
return "\(debtObjects.count == 0 ? "No" : Int(debtObjects.count).withCommas()) \(debtObjects.isSingle ? SharedText.EntityName.debt : SharedText.EntityName.debtPlural)" | |
} | |
var labelVehicles: String { | |
return "\(vehicleObjects.count == 0 ? "No" : Int(vehicleObjects.count).withCommas()) \(vehicleObjects.isSingle ? SharedText.EntityName.assetVehicle : SharedText.EntityName.assetVehiclePlural)" | |
} | |
var labelRealEstate: String { | |
return "\(realEstateObjects.count == 0 ? "No" : Int(realEstateObjects.count).withCommas()) \(realEstateObjects.isSingle ? SharedText.EntityName.assetRealEstate : SharedText.EntityName.assetRealEstatePlural)" | |
} | |
var labelEducation: String { | |
return "\(educationObjects.count == 0 ? "No" : Int(educationObjects.count).withCommas()) \(educationObjects.isSingle ? SharedText.EntityName.assetEducation : SharedText.EntityName.assetEducationPlural)" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment