Skip to content

Instantly share code, notes, and snippets.

@lifeutilityapps
Created December 8, 2024 14:24
Show Gist options
  • Save lifeutilityapps/a15c8e34163a203ea188ab00ca96be02 to your computer and use it in GitHub Desktop.
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.
//
// 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