Add push notifications support and update AppDelegate

This commit is contained in:
“SamoilenkoVadym” 2025-03-03 16:44:46 +00:00
parent 49638e2519
commit 757342f057
4 changed files with 210 additions and 227 deletions

View file

@ -1,10 +1,4 @@
//
// Theme.swift
// Coinly
//
// Created by Vadym Samoilenko on 03/03/2025.
//
// Common/Theme/Theme.swift
import SwiftUI
@ -19,14 +13,19 @@ enum Theme {
// Акцентные цвета
static let primary = Color.accentColor
static let positive = Color("AccentGreen", bundle: nil) // Fallback to system green
static let negative = Color("AccentRed", bundle: nil) // Fallback to system red
static let positive = Color("AccentGreen", bundle: nil)
static let negative = Color("AccentRed", bundle: nil)
// Цвета для типов счетов
static let debitCard = Color.blue
static let credit = Color.purple
static let cash = Color.green
static let savings = Color.mint
static let debitCard = Color.blue
static let savings = Color.purple
static let investment = Color.orange
static let credit = Color.red
static let loan = Color.pink
static let deposit = Color.yellow
static let debtToMe = Color.mint
static let myDebt = Color.brown
// Цвета текста
static let primaryText = Color(.label)
@ -38,4 +37,4 @@ enum Theme {
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
}

View file

@ -3,49 +3,63 @@ import CoreData
struct AccountListView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject private var settingsViewModel: SettingsViewModel
@StateObject private var viewModel = AccountViewModel()
@State private var showingAddAccount = false
@State private var showingSettings = false
@State private var showArchived = false
@State private var animateChanges = false
var body: some View {
ScrollView {
VStack(spacing: 24) {
// Total Balance Card
if !viewModel.activeAccounts.isEmpty {
if viewModel.activeAccounts.isEmpty {
emptyStateView
.transition(.opacity.combined(with: .scale))
} else {
totalBalanceCard
}
// Account Groups
VStack(spacing: 24) {
// Basic Accounts
ForEach(AccountType.basicAccounts, id: \.self) { type in
accountGroup(for: type)
}
// Investment Accounts
if !viewModel.accounts(for: .investmentAccount).isEmpty {
accountGroup(for: .investmentAccount)
}
// Credit & Debt Accounts
if hasDebtAccounts {
VStack(alignment: .leading, spacing: 16) {
Text("CREDIT & DEBT")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(Theme.secondaryText)
.padding(.horizontal, 16)
ForEach(AccountType.debtAccounts, id: \.self) { type in
if !viewModel.accounts(for: type).isEmpty {
accountGroup(for: type)
.transition(.opacity.combined(with: .scale))
VStack(spacing: 24) {
ForEach(AccountType.basicAccounts, id: \.self) { type in
accountGroup(for: type)
}
if !viewModel.accounts(for: AccountType.investmentAccounts[0]).isEmpty {
VStack(alignment: .leading, spacing: 16) {
Text("INVESTMENTS")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(Theme.secondaryText)
.padding(.horizontal, 16)
ForEach(AccountType.investmentAccounts, id: \.self) { type in
if !viewModel.accounts(for: type).isEmpty {
accountGroup(for: type)
}
}
}
}
if hasDebtAccounts {
VStack(alignment: .leading, spacing: 16) {
Text("CREDIT & DEBT")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(Theme.secondaryText)
.padding(.horizontal, 16)
ForEach(AccountType.debtAccounts, id: \.self) { type in
if !viewModel.accounts(for: type).isEmpty {
accountGroup(for: type)
}
}
}
}
}
}
}
.animation(.spring(), value: animateChanges)
.padding(.top)
}
.background(Theme.background)
@ -53,13 +67,19 @@ struct AccountListView: View {
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack(spacing: 16) {
Button(action: { showingAddAccount = true }) {
Button(action: {
hapticFeedback()
showingAddAccount = true
}) {
Image(systemName: "plus.circle.fill")
.font(.title2)
.foregroundColor(Theme.primary)
}
Button(action: { showingSettings = true }) {
Button(action: {
hapticFeedback()
showingSettings = true
}) {
Image(systemName: "gear")
.font(.title2)
.foregroundColor(Theme.primary)
@ -70,107 +90,76 @@ struct AccountListView: View {
.sheet(isPresented: $showingAddAccount) {
AccountFormView()
.environment(\.managedObjectContext, viewContext)
.environmentObject(settingsViewModel)
}
.sheet(isPresented: $showingSettings) {
SettingsView()
.environmentObject(settingsViewModel)
}
.refreshable {
hapticFeedback()
await viewModel.loadAccounts()
withAnimation {
animateChanges.toggle()
}
}
.task {
await viewModel.loadAccounts()
animateChanges.toggle()
}
}
private var totalBalanceCard: some View {
VStack(spacing: 8) {
Text("Total Balance")
.font(.subheadline)
.foregroundColor(Theme.secondaryText)
ForEach(Array(viewModel.accountsByCurrency.keys), id: \.self) { currency in
VStack(spacing: 4) {
Text(viewModel.totalBalanceFormatted(currency: currency))
.font(.system(size: 34, weight: .medium, design: .rounded))
.foregroundColor(Theme.primaryText)
Text(currency.code)
.font(.caption)
.foregroundColor(Theme.secondaryText)
}
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
.background(
RoundedRectangle(cornerRadius: 24)
.fill(Theme.cardBackground)
.shadow(color: Theme.cardShadow, radius: 15, x: 0, y: 5)
)
.padding(.horizontal, 16)
}
private func accountGroup(for type: AccountType) -> some View {
let accounts = viewModel.accounts(for: type)
if accounts.isEmpty { return EmptyView().erasedToAnyView() }
return VStack(alignment: .leading, spacing: 16) {
// Group Header
HStack {
Image(systemName: type.icon)
.foregroundColor(accountTypeColor(for: type))
Text(type.localizedName.uppercased())
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(Theme.secondaryText)
}
.padding(.horizontal, 16)
// Account Cards
VStack(spacing: 12) {
ForEach(accounts) { account in
NavigationLink {
AccountDetailView(account: account)
.environment(\.managedObjectContext, viewContext)
} label: {
AccountRowView(account: account)
if accounts.isEmpty { return AnyView(EmptyView()) }
return AnyView(
VStack(alignment: .leading, spacing: 16) {
HStack {
Image(systemName: type.icon)
.foregroundColor(Color(type.color))
Text(type.localizedName.uppercased())
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(Theme.secondaryText)
}
.padding(.horizontal, 16)
VStack(spacing: 12) {
ForEach(accounts) { account in
NavigationLink {
AccountDetailView(account: account)
.environment(\.managedObjectContext, viewContext)
.environmentObject(settingsViewModel)
} label: {
AccountRowView(account: account)
.environmentObject(settingsViewModel)
}
.buttonStyle(PlainButtonStyle())
}
.buttonStyle(PlainButtonStyle())
}
}
}
.erasedToAnyView()
)
}
private func accountTypeColor(for type: AccountType) -> Color {
switch type {
case .debitCard: return Theme.debitCard
case .credit: return Theme.credit
case .cash: return Theme.cash
case .savings: return Theme.savings
default: return Theme.primary
}
}
private var hasDebtAccounts: Bool {
AccountType.debtAccounts.contains { !viewModel.accounts(for: $0).isEmpty }
}
}
extension View {
func erasedToAnyView() -> AnyView {
AnyView(self)
private var emptyStateView: some View {
Text("No accounts available")
.font(.title)
.foregroundColor(.gray)
}
private var totalBalanceCard: some View {
Text("Total Balance: $0.00")
.font(.title)
.foregroundColor(.primary)
}
private func hapticFeedback() {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
}
}
// MARK: - Preview
#if DEBUG
struct AccountListView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
AccountListView()
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
}
#endif

View file

@ -51,7 +51,6 @@ struct SettingsView: View {
_groupTransactionsByDay = State(initialValue: settings.display.groupTransactionsByDay)
_showRunningBalance = State(initialValue: settings.display.showRunningBalance)
}
var body: some View {
NavigationView {
List {
@ -125,7 +124,6 @@ struct SettingsView: View {
}
}
}
private var appearanceSection: some View {
Section {
ForEach(AppSettings.AppTheme.allCases, id: \.self) { theme in
@ -158,8 +156,8 @@ struct SettingsView: View {
.tag(currency)
}
}
.onChange(of: selectedCurrency) { newCurrency in
viewModel.updateDefaultCurrency(newCurrency)
.onChange(of: selectedCurrency) { oldValue, newValue in
viewModel.updateDefaultCurrency(newValue)
}
} header: {
Text("CURRENCY")
@ -169,51 +167,50 @@ struct SettingsView: View {
private var privacySection: some View {
Section {
Toggle("Hide Balances", isOn: $hideBalances)
.onChange(of: hideBalances) { newValue in
.onChange(of: hideBalances) { oldValue, newValue in
viewModel.updatePrivacySettings(hideBalances: newValue)
}
Toggle("Use Face ID", isOn: $useBiometrics)
.onChange(of: useBiometrics) { newValue in
.onChange(of: useBiometrics) { oldValue, newValue in
viewModel.updatePrivacySettings(useBiometrics: newValue)
}
Toggle("Require Passcode on Launch", isOn: $requirePasscodeOnLaunch)
.onChange(of: requirePasscodeOnLaunch) { newValue in
.onChange(of: requirePasscodeOnLaunch) { oldValue, newValue in
viewModel.updatePrivacySettings(requirePasscode: newValue)
}
} header: {
Text("PRIVACY & SECURITY")
}
}
private var notificationsSection: some View {
Section {
Toggle("Enable Notifications", isOn: $notificationsEnabled)
.onChange(of: notificationsEnabled) { newValue in
.onChange(of: notificationsEnabled) { oldValue, newValue in
viewModel.updateNotificationSettings(isEnabled: newValue)
}
if notificationsEnabled {
Toggle("Daily Balance Reminder", isOn: $dailyReminder)
.onChange(of: dailyReminder) { newValue in
.onChange(of: dailyReminder) { oldValue, newValue in
viewModel.updateNotificationSettings(dailyReminder: newValue)
}
if dailyReminder {
DatePicker("Reminder Time", selection: $dailyReminderTime, displayedComponents: .hourAndMinute)
.onChange(of: dailyReminderTime) { newValue in
.onChange(of: dailyReminderTime) { oldValue, newValue in
viewModel.updateNotificationSettings(reminderTime: newValue)
}
}
Toggle("Weekly Report", isOn: $weeklyReport)
.onChange(of: weeklyReport) { newValue in
.onChange(of: weeklyReport) { oldValue, newValue in
viewModel.updateNotificationSettings(weeklyReport: newValue)
}
Toggle("Transaction Alerts", isOn: $transactionAlerts)
.onChange(of: transactionAlerts) { newValue in
.onChange(of: transactionAlerts) { oldValue, newValue in
viewModel.updateNotificationSettings(transactionAlerts: newValue)
}
}
@ -225,24 +222,23 @@ struct SettingsView: View {
private var displaySection: some View {
Section {
Toggle("Show Cents", isOn: $showCents)
.onChange(of: showCents) { newValue in
.onChange(of: showCents) { oldValue, newValue in
viewModel.updateDisplaySettings(showCents: newValue)
}
Toggle("Group Transactions by Day", isOn: $groupTransactionsByDay)
.onChange(of: groupTransactionsByDay) { newValue in
.onChange(of: groupTransactionsByDay) { oldValue, newValue in
viewModel.updateDisplaySettings(groupByDay: newValue)
}
Toggle("Show Running Balance", isOn: $showRunningBalance)
.onChange(of: showRunningBalance) { newValue in
.onChange(of: showRunningBalance) { oldValue, newValue in
viewModel.updateDisplaySettings(showRunningBalance: newValue)
}
} header: {
Text("DISPLAY")
}
}
private var appInfoSection: some View {
Section {
HStack {
@ -296,96 +292,95 @@ struct SettingsView: View {
.contentShape(Rectangle())
}
}
@ViewBuilder
private var userProfileSection: some View {
if let email = viewModel.settings.userEmail {
VStack(alignment: .leading, spacing: 4) {
Text(email)
private var userProfileSection: some View {
if let email = viewModel.settings.userEmail {
VStack(alignment: .leading, spacing: 4) {
Text(email)
.foregroundColor(.primary)
Text("Signed in")
.font(.caption)
.foregroundColor(.secondary)
}
Button(role: .destructive) {
// Handle sign out
} label: {
Text("Sign Out")
}
}
}
private var authenticationSection: some View {
VStack(spacing: 12) {
NavigationLink {
SignInView()
} label: {
HStack {
Image(systemName: "person.fill")
Text("Sign In")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.accentColor)
.foregroundColor(.white)
.cornerRadius(10)
}
NavigationLink {
SignUpView()
} label: {
HStack {
Image(systemName: "person.badge.plus")
Text("Create Account")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.secondary.opacity(0.1))
.foregroundColor(.primary)
Text("Signed in")
.font(.caption)
.foregroundColor(.secondary)
}
Button(role: .destructive) {
// Handle sign out
} label: {
Text("Sign Out")
}
}
}
private var authenticationSection: some View {
VStack(spacing: 12) {
NavigationLink {
SignInView()
} label: {
HStack {
Image(systemName: "person.fill")
Text("Sign In")
.cornerRadius(10)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.accentColor)
.foregroundColor(.white)
.cornerRadius(10)
}
.padding(.vertical, 8)
}
// MARK: - Actions
private func checkForUpdates() {
Task {
await viewModel.checkForUpdates()
}
}
private func resetSettings() {
viewModel.resetSettings()
NavigationLink {
SignUpView()
} label: {
HStack {
Image(systemName: "person.badge.plus")
Text("Create Account")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.secondary.opacity(0.1))
.foregroundColor(.primary)
.cornerRadius(10)
}
}
.padding(.vertical, 8)
}
// MARK: - Actions
private func checkForUpdates() {
Task {
await viewModel.checkForUpdates()
// Update local state
selectedTheme = viewModel.settings.theme
selectedLanguage = viewModel.settings.language
selectedCurrency = viewModel.settings.defaultCurrency
// Privacy
hideBalances = viewModel.settings.privacy.hideBalances
useBiometrics = viewModel.settings.privacy.useBiometrics
requirePasscodeOnLaunch = viewModel.settings.privacy.requirePasscodeOnLaunch
// Notifications
notificationsEnabled = viewModel.settings.notifications.isEnabled
dailyReminder = viewModel.settings.notifications.dailyReminder
dailyReminderTime = viewModel.settings.notifications.dailyReminderTime
weeklyReport = viewModel.settings.notifications.weeklyReport
transactionAlerts = viewModel.settings.notifications.transactionAlerts
// Display
showCents = viewModel.settings.display.showCents
groupTransactionsByDay = viewModel.settings.display.groupTransactionsByDay
showRunningBalance = viewModel.settings.display.showRunningBalance
}
}
private func resetSettings() {
viewModel.resetSettings()
// Update local state
selectedTheme = viewModel.settings.theme
selectedLanguage = viewModel.settings.language
selectedCurrency = viewModel.settings.defaultCurrency
// Privacy
hideBalances = viewModel.settings.privacy.hideBalances
useBiometrics = viewModel.settings.privacy.useBiometrics
requirePasscodeOnLaunch = viewModel.settings.privacy.requirePasscodeOnLaunch
// Notifications
notificationsEnabled = viewModel.settings.notifications.isEnabled
dailyReminder = viewModel.settings.notifications.dailyReminder
dailyReminderTime = viewModel.settings.notifications.dailyReminderTime
weeklyReport = viewModel.settings.notifications.weeklyReport
transactionAlerts = viewModel.settings.notifications.transactionAlerts
// Display
showCents = viewModel.settings.display.showCents
groupTransactionsByDay = viewModel.settings.display.groupTransactionsByDay
showRunningBalance = viewModel.settings.display.showRunningBalance
}
}
// MARK: - Preview
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView()
// MARK: - Preview
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView()
}
}
}