Redesign dashboard with Apple-style UI improvements, better account cards and spending analysis

This commit is contained in:
“SamoilenkoVadym” 2025-03-02 20:09:58 +00:00
parent c3f0a87841
commit 78c47819d0
6 changed files with 223 additions and 145 deletions

View file

@ -26,11 +26,18 @@ struct ContentView: View {
}
}
.environmentObject(settings)
.preferredColorScheme(settings.isDarkMode ? .dark : .light)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
Group {
ContentView()
.preferredColorScheme(.light)
ContentView()
.preferredColorScheme(.dark)
}
}
}

View file

@ -20,6 +20,7 @@ struct AccountsListView: View {
Section(type.rawValue) {
ForEach(store.getAccounts(of: type)) { account in
AccountCardView(account: account)
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
}
}
}
@ -42,13 +43,20 @@ struct AccountsListView: View {
}
}
}
.background(Color(uiColor: .systemGroupedBackground))
}
}
}
struct AccountsListView_Previews: PreviewProvider {
static var previews: some View {
AccountsListView(store: AccountsStore())
.environmentObject(AppSettings.shared)
Group {
AccountsListView(store: AccountsStore())
.preferredColorScheme(.light)
AccountsListView(store: AccountsStore())
.preferredColorScheme(.dark)
}
.environmentObject(AppSettings.shared)
}
}
}

View file

@ -4,6 +4,7 @@ struct DashboardView: View {
@ObservedObject var store: TransactionsStore
@ObservedObject var accountsStore: AccountsStore
@EnvironmentObject private var settings: AppSettings
@State private var showingAccountsList = false
private var totalBalance: Double {
store.transactions.reduce(0) { total, transaction in
@ -42,129 +43,191 @@ struct DashboardView: View {
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
// Balance Card
VStack(spacing: 8) {
Text("Total Balance")
.font(.subheadline)
.foregroundColor(.gray)
Text(totalBalance.formatAsCurrency())
.font(.title)
.fontWeight(.bold)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
// Accounts Summary
AccountsSummaryView(accountsStore: accountsStore)
// Income/Expense Summary
HStack(spacing: 16) {
// Income Card
VStack(spacing: 8) {
Text("Income")
VStack(spacing: 24) {
// Header Stats
HStack(spacing: 20) {
// Monthly Balance
VStack(alignment: .leading, spacing: 8) {
Text("This Month")
.font(.subheadline)
.foregroundColor(.gray)
Text(income.formatAsCurrency())
.font(.headline)
.foregroundColor(.green)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
// Expense Card
VStack(spacing: 8) {
Text("Expenses")
.font(.subheadline)
.foregroundColor(.gray)
Text(expenses.formatAsCurrency())
.font(.headline)
.foregroundColor(.red)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
// Expenses by Category
if !categoryExpenses.isEmpty {
VStack(alignment: .leading, spacing: 16) {
Text("Expenses by Category")
.font(.headline)
.foregroundColor(Color(uiColor: .secondaryLabel))
Text(totalBalance.formatAsCurrency())
.font(.system(size: 28, weight: .semibold))
// Pie Chart
PieChartView(slices: categoryExpenses)
.frame(height: 200)
.padding(.vertical)
// Category Legend
VStack(spacing: 12) {
ForEach(categoryExpenses) { slice in
HStack {
Circle()
.fill(Color(slice.category.color))
.frame(width: 12, height: 12)
Text(slice.category.name)
Spacer()
Text(slice.amount.formatAsCurrency())
.foregroundColor(.secondary)
Text(String(format: "(%.1f%%)", slice.percentage * 100))
.foregroundColor(.secondary)
}
HStack(spacing: 16) {
// Income indicator
HStack(spacing: 4) {
Circle()
.fill(Color.green)
.frame(width: 8, height: 8)
Text("\(income.formatAsCurrency())")
.font(.caption)
.foregroundColor(.green)
}
// Expense indicator
HStack(spacing: 4) {
Circle()
.fill(Color.red)
.frame(width: 8, height: 8)
Text("\(expenses.formatAsCurrency())")
.font(.caption)
.foregroundColor(.red)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding()
.background(Color(uiColor: .secondarySystemBackground))
.cornerRadius(16)
// Recent Transactions
VStack(alignment: .leading, spacing: 12) {
Text("Recent Transactions")
.font(.headline)
// Accounts Section
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("Accounts")
.font(.title2)
.fontWeight(.bold)
Spacer()
Button("See All") {
showingAccountsList = true
}
.foregroundColor(.accentColor)
}
ForEach(Array(store.transactions.prefix(3))) { transaction in
TransactionRowView(transaction: transaction)
if transaction.id != store.transactions.prefix(3).last?.id {
Divider()
ForEach(Array(accountsStore.accounts.prefix(3))) { account in
Button(action: {
// Show account details
}) {
HStack(spacing: 16) {
// Account Icon
Image(systemName: account.type.icon)
.font(.title2)
.foregroundColor(.white)
.frame(width: 40, height: 40)
.background(accountColor(for: account.type))
.cornerRadius(12)
VStack(alignment: .leading, spacing: 4) {
Text(account.name)
.font(.headline)
.foregroundColor(Color(uiColor: .label))
Text(account.type.rawValue)
.font(.caption)
.foregroundColor(Color(uiColor: .secondaryLabel))
}
Spacer()
VStack(alignment: .trailing, spacing: 4) {
Text(account.balance.formatAsCurrency())
.font(.headline)
.foregroundColor(Color(uiColor: .label))
if let availableCredit = account.availableCredit {
Text("Available: \(availableCredit.formatAsCurrency())")
.font(.caption)
.foregroundColor(Color(uiColor: .secondaryLabel))
}
}
}
.padding()
.background(Color(uiColor: .secondarySystemBackground))
.cornerRadius(16)
}
}
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
.padding(.horizontal)
// Expenses Analysis
VStack(alignment: .leading, spacing: 16) {
Text("Spending Analysis")
.font(.title2)
.fontWeight(.bold)
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
ForEach(categoryExpenses) { slice in
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: slice.category.icon)
.foregroundColor(.white)
.frame(width: 32, height: 32)
.background(Color(slice.category.color))
.cornerRadius(8)
VStack(alignment: .leading) {
Text(slice.category.name)
.font(.headline)
Text(slice.amount.formatAsCurrency())
.font(.subheadline)
.foregroundColor(Color(uiColor: .secondaryLabel))
}
}
// Progress bar
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.fill(Color(uiColor: .systemFill))
Rectangle()
.fill(Color(slice.category.color))
.frame(width: geometry.size.width * slice.percentage)
}
}
.frame(height: 8)
.cornerRadius(4)
Text(String(format: "%.1f%%", slice.percentage * 100))
.font(.caption)
.foregroundColor(Color(uiColor: .secondaryLabel))
}
.frame(width: 160)
.padding()
.background(Color(uiColor: .secondarySystemBackground))
.cornerRadius(16)
}
}
.padding(.horizontal)
}
}
}
.padding()
.padding(.vertical)
}
.navigationTitle("Dashboard")
.background(Color(.systemGroupedBackground))
.background(Color(uiColor: .systemBackground))
.sheet(isPresented: $showingAccountsList) {
AccountsListView(store: accountsStore)
}
}
}
private func accountColor(for type: AccountType) -> Color {
switch type {
case .wallet: return .blue
case .bankAccount: return .green
case .creditCard: return .purple
case .deposit: return .orange
case .debt: return .red
}
}
}
struct DashboardView_Previews: PreviewProvider {
static var previews: some View {
DashboardView(
store: TransactionsStore(),
accountsStore: AccountsStore()
)
Group {
DashboardView(
store: TransactionsStore(),
accountsStore: AccountsStore()
)
.preferredColorScheme(.light)
DashboardView(
store: TransactionsStore(),
accountsStore: AccountsStore()
)
.preferredColorScheme(.dark)
}
.environmentObject(AppSettings.shared)
}
}

View file

@ -1,4 +1,4 @@
import Foundation
import SwiftUI
class AppSettings: ObservableObject {
@Published var currency: Currency = .usd {
@ -8,7 +8,11 @@ class AppSettings: ObservableObject {
}
}
}
@Published var isDarkMode: Bool = false
@AppStorage("isDarkMode") var isDarkMode: Bool = false {
didSet {
applyTheme()
}
}
@Published var notificationsEnabled: Bool = true
@Published private(set) var exchangeRates: [String: Double] = [:]
@Published private(set) var lastRatesUpdate: Date?
@ -30,13 +34,21 @@ class AppSettings: ObservableObject {
static let shared = AppSettings()
private init() {
// Инициализируем базовые курсы (1 USD = ...)
exchangeRates = [
"USD": 1.0, // 1 USD = 1 USD
"EUR": 0.92, // 1 USD = 0.92 EUR
"GBP": 0.79 // 1 USD = 0.79 GBP
"USD": 1.0,
"EUR": 0.92,
"GBP": 0.79
]
fetchExchangeRates()
applyTheme()
}
private func applyTheme() {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
windowScene.windows.forEach { window in
window.overrideUserInterfaceStyle = isDarkMode ? .dark : .light
}
}
}
func convert(_ amount: Double, from sourceCurrency: Currency, to targetCurrency: Currency) -> Double {
@ -47,13 +59,11 @@ class AppSettings: ObservableObject {
return amount
}
// Сначала конвертируем в USD, затем в целевую валюту
if sourceCurrency == .usd {
return amount * targetRate
} else if targetCurrency == .usd {
return amount / sourceRate
} else {
// Конвертация через USD
let amountInUSD = amount / sourceRate
return amountInUSD * targetRate
}
@ -73,7 +83,7 @@ class AppSettings: ObservableObject {
let response = try JSONDecoder().decode(ExchangeRatesResponse.self, from: data)
DispatchQueue.main.async {
var rates = response.rates
rates["USD"] = 1.0 // Базовая валюта всегда 1.0
rates["USD"] = 1.0
self?.exchangeRates = rates
self?.lastRatesUpdate = Date()

View file

@ -1,11 +1,3 @@
//
// AccountCardView.swift
// Coinly
//
// Created by Vadym Samoilenko on 02/03/2025.
//
import SwiftUI
struct AccountCardView: View {
@ -41,7 +33,7 @@ struct AccountCardView: View {
Text(account.currency.symbol)
.font(.subheadline)
.foregroundColor(.gray)
.foregroundColor(Color(uiColor: .secondaryLabel))
}
// Balance
@ -53,7 +45,7 @@ struct AccountCardView: View {
if let availableCredit = account.availableCredit {
Text("Available: \(availableCredit.formatAsCurrency())")
.font(.caption)
.foregroundColor(.gray)
.foregroundColor(Color(uiColor: .secondaryLabel))
}
if account.isOverdue {
@ -63,7 +55,7 @@ struct AccountCardView: View {
}
}
.padding()
.background(Color(.systemBackground))
.background(Color(uiColor: .systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
@ -76,8 +68,8 @@ struct AccountCardView_Previews: PreviewProvider {
AccountCardView(account: AccountModel.sampleData[2])
}
.padding()
.background(Color(.systemGroupedBackground))
.background(Color(uiColor: .systemGroupedBackground))
.environmentObject(AppSettings.shared)
.previewLayout(.sizeThatFits)
}
}
}

View file

@ -1,11 +1,3 @@
//
// AccountsSummaryView.swift
// Coinly
//
// Created by Vadym Samoilenko on 02/03/2025.
//
import SwiftUI
struct AccountsSummaryView: View {
@ -23,7 +15,7 @@ struct AccountsSummaryView: View {
VStack(spacing: 8) {
Text("Total Assets")
.font(.subheadline)
.foregroundColor(.gray)
.foregroundColor(Color(uiColor: .secondaryLabel))
Text(totalBalance.formatAsCurrency())
.font(.title2)
@ -47,7 +39,7 @@ struct AccountsSummaryView: View {
}
}
.padding()
.background(Color(.systemBackground))
.background(Color(uiColor: .systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
.sheet(isPresented: $showingAccountsList) {
@ -58,10 +50,16 @@ struct AccountsSummaryView: View {
struct AccountsSummaryView_Previews: PreviewProvider {
static var previews: some View {
AccountsSummaryView(accountsStore: AccountsStore())
.environmentObject(AppSettings.shared)
.padding()
.background(Color(.systemGroupedBackground))
.previewLayout(.sizeThatFits)
Group {
AccountsSummaryView(accountsStore: AccountsStore())
.preferredColorScheme(.light)
AccountsSummaryView(accountsStore: AccountsStore())
.preferredColorScheme(.dark)
}
.environmentObject(AppSettings.shared)
.padding()
.background(Color(uiColor: .systemGroupedBackground))
.previewLayout(.sizeThatFits)
}
}
}