()
18 |
19 | // Функция проверяет, есть ли в категории транзакции с выбранным типом (доход или расход)
20 | func hasTransactions(type: CategoryType) -> Bool {
21 | for transaction in transactions {
22 | if transaction.type == type {
23 | return true
24 | }
25 | }
26 | return false
27 | }
28 |
29 | // Функция которая вычисляет сумму всех транзакций определенного типа (категории) из списка транзакций.
30 | func categoryAmount(type: CategoryType) -> Float {
31 | var totalAmount: Float = 0
32 | for transaction in transactions {
33 | if transaction.type == type {
34 | totalAmount += transaction.amount
35 | }
36 | }
37 | return totalAmount
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/iWallet/View/Templates/IconPickerView.swift:
--------------------------------------------------------------------------------
1 | // IconPickerView.swift
2 |
3 | import SwiftUI
4 |
5 | struct IconPickerView: View {
6 | @Binding var selectedImage: String
7 |
8 | var body: some View {
9 | ScrollView(.horizontal, showsIndicators: false) {
10 | HStack(alignment: .center) {
11 | ForEach(Icons.allIcons, id: \.self) { image in
12 | HStack {
13 | VStack {
14 | Image(systemName: image)
15 | .foregroundColor(Color(Colors.mainText))
16 | .font(.system(size: 20))
17 | .frame(width: 40, height: 40)
18 | .background {
19 | RoundedRectangle(cornerRadius: 10, style: .circular)
20 | .strokeBorder(Color(Colors.mainText))
21 | }
22 | }
23 | }
24 | .opacity(image == selectedImage ? 1.0 : 0.5)
25 | .scaleEffect(image == selectedImage ? 1.1 : 1.0)
26 | .onTapGesture {
27 | selectedImage = image
28 | }
29 | }
30 | }
31 | .padding(2)
32 | }
33 | }
34 | }
35 |
36 | struct IconPickerView_Previews: PreviewProvider {
37 | static var previews: some View {
38 | IconPickerView(selectedImage: .constant("folder.circle"))
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/iWallet/View/Templates/BalanceView.swift:
--------------------------------------------------------------------------------
1 | // BalanceView.swift
2 |
3 | import SwiftUI
4 |
5 | struct BalanceView: View {
6 | @EnvironmentObject var appVM: AppViewModel
7 |
8 | let amount: Float
9 | let curren: String
10 | let type: String
11 | let icon: String
12 | let iconBG: Color
13 |
14 | var body: some View {
15 | VStack {
16 | VStack(alignment: .leading) {
17 | HStack {
18 | Image(systemName: icon)
19 | .foregroundColor(Color(Colors.colorBlack))
20 | .frame(width: 30, height: 30)
21 | .background(iconBG)
22 | .cornerRadius(7.5)
23 | Spacer()
24 | Text("\(amount.formattedWithSeparatorAndCurrency(roundingNumbers: appVM.roundingNumbers)) \(curren)")
25 | .font(.headline)
26 | .fontWeight(.bold)
27 | .foregroundColor(Color(Colors.mainText))
28 | }
29 |
30 | HStack {
31 | Text(type)
32 | .foregroundColor(.gray).textCase(.uppercase)
33 | .font(.subheadline).dynamicTypeSize(.small)
34 | } .padding(.top, 5)
35 | }
36 | .padding(10)
37 | .padding(.vertical, 5)
38 | .background(Color(Colors.colorBalanceBG))
39 | .cornerRadius(10)
40 | .frame(maxWidth: .infinity, maxHeight: .infinity)
41 | }
42 | }
43 | }
44 |
45 | struct BalanceView_Previews: PreviewProvider {
46 | static var previews: some View {
47 | BalanceView(amount: 1000, curren: "$", type: "Income", icon: "plus", iconBG: .blue)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/iWallet/Resources/Colors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Colors.swift
3 | // iWallet
4 | //
5 | // Created by Vladislav Novoshinskiy on 26.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Colors {
11 |
12 | // основные цвета
13 | static let colorBalanceBG: String = "colorBalanceBG"
14 | static let mainText: String = "colorBalanceText"
15 | static let mainBG: String = "colorBG"
16 | static let colorBlack: String = "colorBlack"
17 | static let colorPickerBG: String = "colorPickerBG"
18 |
19 | // дополнительные цвета для иконок кол-во 21
20 | static let colorBlue: String = "colorBlue"
21 | static let colorBlue1: String = "colorBlue1"
22 | static let colorBlue2: String = "colorBlue2"
23 | static let colorBrown: String = "colorBrown"
24 | static let colorBrown1: String = "colorBrown1"
25 | static let colorBrown2: String = "colorBrown2"
26 | static let colorGray: String = "colorGray"
27 | static let colorGray1: String = "colorGray1"
28 | static let colorGray2: String = "colorGray2"
29 | static let colorGreen: String = "colorGreen"
30 | static let colorGreen1: String = "colorGreen1"
31 | static let colorGreen2: String = "colorGreen2"
32 | static let colorPurple: String = "colorPurple"
33 | static let colorPurple1: String = "colorPurple1"
34 | static let colorPurple2: String = "colorPurple2"
35 | static let colorRed: String = "colorRed"
36 | static let colorRed1: String = "colorRed1"
37 | static let colorRed2: String = "colorRed2"
38 | static let colorYellow: String = "colorYellow"
39 | static let colorYellow1: String = "colorYellow1"
40 | static let colorYellow2: String = "colorYellow2"
41 |
42 | // все дополнительные цвета в массиве кол-во 21
43 | static let allColors: [String] =
44 | [
45 | colorBlue, colorBlue1, colorBlue2, colorRed, colorRed1, colorRed2, colorYellow, colorYellow1, colorYellow2, colorPurple, colorPurple1, colorPurple2, colorBrown, colorBrown1, colorBrown2, colorGray, colorGray1, colorGray2
46 | ]
47 | }
48 |
49 |
50 |
--------------------------------------------------------------------------------
/iWallet/Model/CurrencyModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CurrencyModel.swift
3 | // iWallet
4 | //
5 | // Created by Vladislav Novoshinskiy on 24.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum Currency: String, CaseIterable, Identifiable, Hashable {
11 | case usd = "USD"
12 | case eur = "EUR"
13 | case jpy = "JPY"
14 | case gbp = "GBP"
15 | case aud = "AUD"
16 | case cad = "CAD"
17 | case chf = "CHF"
18 | case cny = "CNY"
19 | case rub = "RUB"
20 | case inr = "INR"
21 | case brl = "BRL"
22 | case zar = "ZAR"
23 | case TRY = "TRY"
24 | case mxn = "MXN"
25 | case idr = "IDR"
26 | case hkd = "HKD"
27 | case sgd = "SGD"
28 | case twd = "TWD"
29 | case krw = "KRW"
30 | case thb = "THB"
31 | case myr = "MYR"
32 | case php = "PHP"
33 | case vnd = "VND"
34 | case kes = "KES"
35 | case egp = "EGP"
36 | case byn = "BYN"
37 | case uah = "UAH"
38 | case kzt = "KZT"
39 | case tmt = "TMT"
40 | case czk = "CZK"
41 |
42 | var id: String {
43 | return self.rawValue
44 | }
45 |
46 | var symbol: String {
47 | let currencySymbols: [Currency: String] = [
48 | .usd: "$",
49 | .eur: "€",
50 | .jpy: "¥",
51 | .gbp: "£",
52 | .aud: "$",
53 | .cad: "$",
54 | .chf: "CHF",
55 | .cny: "¥",
56 | .rub: "₽",
57 | .inr: "₹",
58 | .brl: "R$",
59 | .zar: "R",
60 | .TRY: "₺",
61 | .mxn: "$",
62 | .idr: "Rp",
63 | .hkd: "HK$",
64 | .sgd: "S$",
65 | .twd: "NT$",
66 | .krw: "₩",
67 | .thb: "฿",
68 | .myr: "RM",
69 | .php: "₱",
70 | .vnd: "₫",
71 | .kes: "KSh",
72 | .egp: "£",
73 | .byn: "Br",
74 | .uah: "₴",
75 | .kzt: "₸",
76 | .tmt: "m",
77 | .czk: "Kč"
78 | ]
79 | return currencySymbols[self] ?? ""
80 | }
81 |
82 | static var sortedCases: [Currency] {
83 | return allCases.sorted { $0.rawValue < $1.rawValue }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/iWallet/View/Category/CategoryItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIView.swift
3 | // iWallet
4 | //
5 | // Created by Vladislav Novoshinskiy on 26.05.2023.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CategoryItemView: View {
11 |
12 | @EnvironmentObject var appVM: AppViewModel
13 |
14 | let categoryColor: String
15 | let categoryIcon: String
16 | let categoryName: String
17 | let totalAmount: Float
18 | let currencySymbol: String
19 |
20 | var body: some View {
21 | VStack(alignment: .leading, spacing: 0) {
22 | HStack {
23 | HStack {
24 | Divider()
25 | .foregroundColor(Color(categoryColor))
26 | .frame(width: 5, height: 50)
27 | .background(Color(categoryColor))
28 | }
29 |
30 | Image(systemName: categoryIcon)
31 | .font(.system(size: 15))
32 | .foregroundColor(.black)
33 | .frame(width: 30, height: 30)
34 | .background(Color(categoryColor))
35 | .cornerRadius(7.5)
36 | .padding(0)
37 |
38 | Text(categoryName)
39 | .font(.headline)
40 | .fontWeight(.light)
41 | .foregroundColor(Color(Colors.mainText))
42 |
43 | Spacer()
44 |
45 | Text("\(totalAmount.formattedWithSeparatorAndCurrency(roundingNumbers: appVM.roundingNumbers)) \(currencySymbol)")
46 | .font(.headline).bold()
47 | .foregroundColor(Color(Colors.mainText))
48 |
49 | Image(systemName: "chevron.forward")
50 | .foregroundColor(Color(Colors.mainText))
51 | .opacity(0.5)
52 |
53 | }
54 | .padding()
55 | .frame(maxWidth: .infinity, maxHeight: 50)
56 | .background(Color(Colors.colorBalanceBG))
57 |
58 | Divider()
59 | }
60 | }
61 | }
62 |
63 | struct CategoryItemView_Previews: PreviewProvider {
64 | static var previews: some View {
65 | CategoryItemView(categoryColor: Colors.colorBlue, categoryIcon: "plus", categoryName: "Food", totalAmount: 100, currencySymbol: "$")
66 | }
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ✨ iWallet - Expenses and Income ✨
2 |
3 | 
4 |
5 |
6 | It is a personal finance management assessment tool that will help you keep track of your income and expenses, keep an eye on your budget, and achieve your financial goals. Install iWallet and enjoy complete control over your finances!
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## You can
21 | * Calculate the average expense per day, taking into account all financial transactions, for more accurate planning and control of your budget.
22 |
23 | * Data security and privacy, since all data is stored on your device and you have full control over access to it.
24 |
25 | ## Create categories
26 | * One of the main features of iWallet is the ability to create custom categories for expenses and income.
27 |
28 | * You can choose a unique icon and color for each category to help you better represent your financial activities. In addition, the application allows you to analyze data and identify areas where you can reduce costs.
29 |
30 | ## Recording transactions
31 | * iWallet is a reliable financial management assistant: track all income and expenses with high accuracy (including date, category and notes) and control your finances with convenience.
32 |
33 | * Be aware of your financial flows and make informed decisions about your money.
34 |
35 | 
36 |
37 | ## Our team
38 | * Our team is working on the development of iWallet — an open source project that allows users to manage their finances more efficiently. We believe that everyone should have access to a quality tool to keep track of their income and expenses.
39 |
40 | * We strive to create an application that is convenient and easy to use, but at the same time functional and powerful. We are constantly working to improve iWallet and add new features so that our users can get the most out of it.
41 |
42 | * Our team consists of beginner developers, designers and a huge amount of motivation. We work with open source to ensure that our project is transparent and accessible to everyone. We also welcome contributions from the community and encourage participation in iWallet development.
43 |
44 | ## iWalletTeam
45 | > ### " Our goal is to make iWallet the best financial accounting tool available to everyone. We believe that every person deserves financial stability and we do our best to help them achieve this goal. "
46 |
47 | 
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/iWallet/View/Category/CategoryView.swift:
--------------------------------------------------------------------------------
1 | // AddCategoryView.swift
2 |
3 | import SwiftUI
4 | import RealmSwift
5 |
6 | struct CategoryView: View {
7 | @EnvironmentObject var appVM: AppViewModel
8 | @EnvironmentObject var categoryVM: CategoryViewModel
9 | @EnvironmentObject var transactionVM: TransactionViewModel
10 | @ObservedResults(Category.self) var categories
11 | @Environment(\.dismiss) var dismiss
12 |
13 | @State var selectedType: CategoryType = .expense
14 |
15 | var body: some View {
16 | NavigationStack {
17 | VStack {
18 | List {
19 | if categoryVM.filteredCategory(category: categories, type: selectedType).isEmpty {
20 | previewCardCategory()
21 | } else {
22 | ForEach(categoryVM.filteredCategory(category: categories, type: selectedType), id: \.self) { category in
23 | HStack {
24 | Image(systemName: category.icon)
25 | .font(.system(size: 15))
26 | .foregroundColor(Color(.black))
27 | .frame(width: 30, height: 30)
28 | .background(Color(category.color))
29 | .cornerRadius(7.5)
30 | Text(category.name)
31 | .foregroundColor(Color("colorBalanceText"))
32 | }
33 | }
34 | .onDelete(perform: { indexSet in
35 | categoryVM.deleteCategories(category: categories, at: indexSet, type: selectedType, transaction: transactionVM)
36 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
37 | })
38 | }
39 | }
40 | .scrollContentBackground(.hidden)
41 | .background(Color("colorBG"))
42 | }
43 | .toolbar {
44 | ToolbarItem(placement: .navigationBarLeading) {
45 | Button {
46 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
47 | dismiss()
48 | } label: {
49 | Text("Back")
50 | }
51 | }
52 | ToolbarItem(placement: .navigationBarTrailing) {
53 | NavigationLink(destination: AddCategoryView(selectedType: selectedType), label: {
54 | Text("New")
55 | })
56 | }
57 | ToolbarItem(placement: .principal) {
58 | Picker("Type", selection: $selectedType) {
59 | ForEach(CategoryType.allCases, id: \.self) { type in
60 | Text(type.localizedName())
61 | }
62 | } .pickerStyle(.segmented)
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
69 | struct CategoryView_Previews: PreviewProvider {
70 | static var previews: some View {
71 | CategoryView()
72 | .environmentObject(AppViewModel())
73 | .environmentObject(TransactionViewModel())
74 | .environmentObject(CategoryViewModel())
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/iWallet/View/Templates/PreviewCardView.swift:
--------------------------------------------------------------------------------
1 | // PreviewCardView.swift
2 |
3 | import SwiftUI
4 |
5 | @ViewBuilder
6 | func previewHomeTransaction() -> some View {
7 | VStack {
8 | VStack(alignment: .center) {
9 | Spacer(minLength: 20)
10 | Image("icon")
11 | .resizable()
12 | .frame(width: 25, height: 25)
13 | Spacer()
14 | Text("iWallet")
15 | .foregroundColor(.gray).bold()
16 | .font(.title)
17 | Spacer(minLength: 20)
18 |
19 | Text("The list of transactions is currently empty,")
20 | .foregroundColor(.gray)
21 | .font(.system(size: 12))
22 | Text("please add transaction.")
23 | .foregroundColor(.gray)
24 | .font(.system(size: 12))
25 | Spacer(minLength: 20)
26 | }
27 | .frame(maxWidth: .infinity, maxHeight: 300)
28 | .background(Color(Colors.colorBalanceBG))
29 | }
30 | }
31 |
32 | @ViewBuilder
33 | func previewCardTransaction() -> some View {
34 | VStack {
35 | VStack(alignment: .center) {
36 | Spacer(minLength: 20)
37 | Image("icon")
38 | .resizable()
39 | .frame(width: 25, height: 25)
40 | Spacer()
41 | Text("iWallet")
42 | .foregroundColor(.gray).bold()
43 | .font(.title)
44 | Spacer(minLength: 20)
45 |
46 | Text("The list of transactions is currently empty,")
47 | .foregroundColor(.gray)
48 | .font(.system(size: 12))
49 | Text("please add transaction.")
50 | .foregroundColor(.gray)
51 | .font(.system(size: 12))
52 | Spacer(minLength: 20)
53 | }
54 | .frame(maxWidth: .infinity, maxHeight: 300)
55 | }
56 | }
57 |
58 | @ViewBuilder
59 | func previewCardCategory() -> some View {
60 | VStack {
61 | VStack(alignment: .center) {
62 | Spacer(minLength: 20)
63 | Image("icon")
64 | .resizable()
65 | .frame(width: 25, height: 25)
66 | Spacer()
67 | Text("iWallet")
68 | .foregroundColor(.gray).bold()
69 | .font(.title)
70 | Spacer(minLength: 20)
71 |
72 | Text("The list of categories is currently empty,")
73 | .foregroundColor(.gray)
74 | .font(.system(size: 12))
75 | Text("please add category.")
76 | .foregroundColor(.gray)
77 | .font(.system(size: 12))
78 | Spacer(minLength: 20)
79 | }
80 | .frame(maxWidth: .infinity, maxHeight: 300)
81 | }
82 | }
83 |
84 | func previewNoCategory() -> some View {
85 | VStack {
86 | VStack(alignment: .center) {
87 | Spacer(minLength: 20)
88 | Image("icon")
89 | .resizable()
90 | .frame(width: 25, height: 25)
91 | Spacer()
92 | Text("iWallet")
93 | .foregroundColor(.gray).bold()
94 | .font(.title)
95 | Spacer(minLength: 20)
96 |
97 | Text("There are currently no categories.")
98 | .foregroundColor(.gray)
99 | .font(.system(size: 12))
100 | Text("Please add.")
101 | .foregroundColor(.gray)
102 | .font(.system(size: 12))
103 | Spacer(minLength: 20)
104 | }
105 | .frame(maxWidth: .infinity, maxHeight: 300)
106 | .background(Color(Colors.colorBalanceBG))
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/iWallet/Localizable/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | iWallet
4 |
5 | Created by Владислав Новошинский on 13.04.2023.
6 |
7 | */
8 |
9 | // HomeView
10 | "Balance" = "Balance";
11 | "Expense average" = "Expense average";
12 | "Income" = "Income";
13 | "Expense" = "Expense";
14 | "Settings" = "Settings";
15 |
16 | // AddTransaction
17 | "Enter amount:" = "Enter amount:";
18 | "Note" = "Note";
19 | "Enter note:" = "Enter note:";
20 | "Category type" = "Category type";
21 | "Category" = "Category";
22 | "Purpose:" = "Purpose:";
23 | "Date" = "Date";
24 | "Enter date:" = "Enter date:";
25 | "Cancel" = "Cancel";
26 | "Add" = "Add";
27 | "Please enter amount" = "Please enter amount";
28 | "Please select a category" = "Please select a category";
29 | "Okay" = "Okay";
30 | "Select a category" = "Select a category";
31 | "Category:" = "Category:";
32 | "Addendum" = "Addendum";
33 |
34 | // TransactionView
35 | "Transactions" = "Transactions";
36 | "Back" = "Back";
37 |
38 | // AddCategoryView
39 | "Back" = "Back";
40 | "New" = "New";
41 |
42 | // AddCategory
43 | "Select type:" = "Select type:";
44 | "Name" = "Name";
45 | "Enter Name" = "Enter Name";
46 | "Choose an icon:" = "Choose an icon:";
47 | "Choose color:" = "Choose color:";
48 | "Create a category" = "Create a category";
49 | "Back" = "Back";
50 | "Add" = "Add";
51 |
52 | // SettingView
53 | "Back" = "Back";
54 | "Settings" = "Settings";
55 | "Transactions" = "Transactions";
56 | "Categories" = "Categories";
57 | "Data" = "Data";
58 | "Web-site" = "Web-site";
59 | "Community" = "Community";
60 | "Write to the developer" = "Write to the developer";
61 | "Feedback" = "Feedback";
62 | "Application" = "Application";
63 | "Currency" = "Currency";
64 | "Vibration" = "Vibration";
65 | "https://idevnva.com/" = "https://idevnva.com/iwallet";
66 | "https://t.me/idevnva" = "https://t.me/idevnva";
67 | "App version: 1.1.5" = "App version: 1.1.5";
68 | "Rounding numbers" = "Rounding numbers";
69 |
70 | //PreviewCard
71 | "Welcome" = "Welcome!";
72 | "The list of transactions is currently empty," = "The list of transactions is currently empty,";
73 | "please add transaction." = "please add transaction.";
74 | "Welcome" = "Welcome";
75 | "The list of categories is currently empty," = "The list of categories is currently empty,";
76 | "please add category." = "please add category.";
77 | "There are currently no categories." = "There are currently no categories.";
78 | "Please add." = "Please add.";
79 |
80 | // DefaultCategories
81 | // Все категории для расхода
82 | "Car" = "Car";
83 | "Bank" = "Bank";
84 | "Business services" = "Business services";
85 | "Charity" = "Charity";
86 | "State" = "State";
87 | "Children" = "Children";
88 | "House" = "House";
89 | "Pets" = "Pets";
90 | "Eating out" = "Eating out";
91 | "Health" = "Health";
92 | "Beauty" = "Beauty";
93 | "Mobile connection" = "Mobile connection";
94 | "Education" = "Education";
95 | "Clothing and footwear" = "Clothing and footwear";
96 | "Present" = "Present";
97 | "Food" = "Food";
98 | "Trips" = "Trips";
99 | "Entertainment" = "Entertainment";
100 | "Technique" = "Technique";
101 | "Transport" = "Transport";
102 |
103 | // Все категории для дохода
104 | "Rent" = "Rent";
105 | "Exchange" = "Exchange";
106 | "Dividends" = "Dividends";
107 | "Wage" = "Wage";
108 | "Present" = "Present";
109 | "Part time job" = "Part time job";
110 | "Interest on accounts" = "Interest on accounts";
111 |
112 | // WelcomeView
113 | "iWallet" = "iWallet";
114 | "Currency" = "Currency";
115 | "Initial application setup" = "Initial application setup:";
116 | "Basic categories" = "Basic categories";
117 | "Note: Enabling this feature will create base categories for expenses and income." = "Note: Enabling this feature will create base categories for expenses and income.";
118 | "Continue" = "Continue";
119 |
120 | // PickerCategoryView
121 | "Categories" = "Categories";
122 | "Create category" = "Create category";
123 |
124 | // EditTransactionView
125 | "Editing" = "Editing";
126 | "Edit" = "Edit";
127 |
--------------------------------------------------------------------------------
/iWallet/Localizable/ru.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | iWallet
4 |
5 | Created by Владислав Новошинский on 13.04.2023.
6 |
7 | */
8 |
9 | // HomeView
10 | "Balance" = "Баланс";
11 | "Expense average" = "Средний расход";
12 | "Income" = "Доход";
13 | "Expense" = "Расход";
14 | "Settings" = "Настройки";
15 |
16 | // AddTransaction
17 | "Enter amount:" = "Введите сумму:";
18 | "Note" = "Заметка";
19 | "Enter note:" = "Введите заметку:";
20 | "Category type" = "Тип категории";
21 | "Category" = "Категория";
22 | "Purpose:" = "Назначание:";
23 | "Date" = "Дата";
24 | "Enter date:" = "Введите дату:";
25 | "Cancel" = "Отменить";
26 | "Add" = "Добавить";
27 | "Please enter amount" = "Пожалуйста введите сумму";
28 | "Please select a category" = "Пожалуйста, выберите категорию";
29 | "Okay" = "Хорошо";
30 | "Select a category" = "Выберите категорию";
31 | "Category:" = "Категория:";
32 | "Addendum" = "Добавление";
33 |
34 | // TransactionView
35 | "Transactions" = "Транзакции";
36 | "Back" = "Назад";
37 |
38 | // AddCategoryView
39 | "Back" = "Назад";
40 | "New" = "Новая";
41 |
42 | // AddCategory
43 | "Select type:" = "Выберите тип:";
44 | "Name" = "Название";
45 | "Enter Name" = "Введите название";
46 | "Choose an icon:" = "Выберите иконку:";
47 | "Choose color:" = "Выберите цвет:";
48 | "Create a category" = "Создание категории";
49 | "Back" = "Назад";
50 | "Add" = "Добавить";
51 |
52 | // SettingView
53 | "Back" = "Назад";
54 | "Settings" = "Настройки";
55 | "Transactions" = "Транзакции";
56 | "Categories" = "Категории";
57 | "Data" = "Данные";
58 | "Web-site" = "Веб-сайт";
59 | "Community" = "Сообщество";
60 | "Write to the developer" = "Написать разработчику";
61 | "Feedback" = "Обратная звязь";
62 | "Application" = "Приложение";
63 | "Currency" = "Валюта";
64 | "Vibration" = "Вибрация";
65 | "https://idevnva.com/" = "https://idevnva.com/iwallet";
66 | "https://t.me/idevnva" = "https://t.me/idevnva";
67 | "App version: 1.1.5" = "Версия приложения: 1.1.5";
68 | "Rounding numbers" = "Округление чисел";
69 |
70 | //PreviewCard
71 | "Welcome" = "Добро пожаловать!";
72 | "The list of transactions is currently empty," = "Список транзакций в настоящее время пуст,";
73 | "please add transaction." = "пожалуйста добавьте транзакцию.";
74 | "Welcome" = "Добро пожаловать!";
75 | "The list of categories is currently empty," = "Список категорий в настоящее время пуст,";
76 | "please add category." = "пожалуйста добавьте категорию.";
77 | "There are currently no categories." = "В настоящее время нет категорий.";
78 | "Please add." = "Пожалуйста, добавьте.";
79 |
80 | // DefaultCategories
81 | // Все категории для расхода
82 | "Car" = "Автомобиль";
83 | "Bank" = "Банк";
84 | "Business services" = "Бизнес услуги";
85 | "Charity" = "Благотворительность";
86 | "State" = "Государство";
87 | "Children" = "Дети";
88 | "House" = "Дом";
89 | "Pets" = "Домашние животные";
90 | "Eating out" = "Еда вне дома";
91 | "Health" = "Здоровье";
92 | "Beauty" = "Красота";
93 | "Mobile connection" = "Мобильная связь";
94 | "Education" = "Образование";
95 | "Clothing and footwear" = "Одежда и обувь";
96 | "Present" = "Подарки";
97 | "Food" = "Продукты питания";
98 | "Trips" = "Путешествие";
99 | "Entertainment" = "Развлечение";
100 | "Technique" = "Техника";
101 | "Transport" = "Транспорт";
102 |
103 | // Все категории для дохода
104 | "Rent" = "Аренда";
105 | "Exchange" = "Биржа";
106 | "Dividends" = "Дивиденды";
107 | "Wage" = "Заработная плата";
108 | "Present" = "Подарки";
109 | "Part time job" = "Подработка";
110 | "Interest on accounts" = "Проценты по счетам";
111 |
112 | // WelcomeView
113 | "iWallet" = "iWallet";
114 | "Currency" = "Валюта";
115 | "Initial application setup" = "Первоначальная настройка приложения:";
116 | "Basic categories" = "Базовые категории";
117 | "Note: Enabling this feature will create base categories for expenses and income." = "Примечание: При включении этой функции будут созданы базовые категории расходов и доходов.";
118 | "Continue" = "Продолжить";
119 |
120 | // PickerCategoryView
121 | "Categories" = "Категории";
122 | "Create category" = "Создать категорию";
123 |
124 | // EditTransactionView
125 | "Editing" = "Редактирование";
126 | "Edit" = "Изменить";
127 |
--------------------------------------------------------------------------------
/iWallet/View/Category/PickerCategoryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PickerCategoryView.swift
3 | // iWallet
4 | //
5 | // Created by Vladislav Novoshinskiy on 03.06.2023.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct PickerCategoryView: View {
12 | @ObservedResults(Category.self) var categories
13 | @Environment(\.dismiss) var dismiss
14 |
15 | @Binding var selected: Category
16 | @State var selectedType: CategoryType = .expense
17 |
18 | var body: some View {
19 | NavigationStack {
20 | ScrollView(.vertical, showsIndicators: false) {
21 | if categories.filter({ $0.type == selectedType }).isEmpty {
22 | VStack {
23 | VStack {
24 | previewNoCategory()
25 | }
26 | .cornerRadius(10)
27 | .padding(.horizontal)
28 |
29 | HStack {
30 | NavigationLink(destination: AddCategoryView(selectedType: selectedType), label: {
31 | HStack {
32 | Text("Create category")
33 | }
34 | })
35 | }
36 | .padding()
37 | .frame(maxWidth: .infinity, maxHeight: 50)
38 | .background(Color(Colors.colorBalanceBG))
39 | .cornerRadius(10)
40 | .padding(.horizontal)
41 | }
42 | } else {
43 | VStack(alignment: .leading, spacing: 0) {
44 | ForEach(categories.filter { $0.type == selectedType }, id: \.name) { category in
45 | Button {
46 | selected = category
47 | dismiss()
48 | } label: {
49 | HStack {
50 | HStack {
51 | Divider()
52 | .foregroundColor(Color(category.color))
53 | .frame(width: 5, height: 50)
54 | .background(Color(category.color))
55 | }
56 | Image(systemName: category.icon)
57 | .font(.system(size: 15))
58 | .foregroundColor(.black)
59 | .frame(width: 30, height: 30)
60 | .background(Color(category.color))
61 | .cornerRadius(7.5)
62 | .padding(0)
63 |
64 | Text(category.name)
65 | .font(.headline)
66 | .fontWeight(.light)
67 | .foregroundColor(Color(Colors.mainText))
68 |
69 | Spacer()
70 | }
71 | }
72 | .padding()
73 | .frame(maxWidth: .infinity, maxHeight: 50)
74 | .background(Color(Colors.colorBalanceBG))
75 |
76 | Divider()
77 | }
78 | }
79 | .cornerRadius(10)
80 | .padding()
81 | }
82 | }
83 | .background(Color(Colors.mainBG))
84 | .navigationTitle("Categories")
85 | .navigationBarTitleDisplayMode(.inline)
86 | }
87 | }
88 | }
89 |
90 | struct PickerCategoryView_Previews: PreviewProvider {
91 | static var previews: some View {
92 | PickerCategoryView(selected: .constant(Category(value: ["name": NSLocalizedString("Interest on accounts", comment: "Interest on accounts"), "icon": "percent", "color": "colorYellow", "type": CategoryType.income] as [String : Any])))
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/iWallet/ViewModel/CategoryViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CategoryViewModel.swift
3 | // iWallet
4 | //
5 | // Created by Vladislav Novoshinskiy on 24.05.2023.
6 | //
7 |
8 | import Foundation
9 | import RealmSwift
10 |
11 | final class CategoryViewModel: ObservableObject {
12 | @Published var categories: [Category] = []
13 |
14 | init() {
15 | loadData()
16 | }
17 |
18 | // Метод для загрузки категорий
19 | func loadData() {
20 | guard let realm = try? Realm() else {
21 | print("Ошибка: loadData")
22 | return
23 | }
24 | let categoriesResult = realm.objects(Category.self)
25 | categories = Array(categoriesResult)
26 | }
27 |
28 | // Добавления категорий по умолчанию
29 | func createDefaultCategories() {
30 | guard let realm = try? Realm() else {
31 | print("Ошибка: Не удалось создать категории по умолчанию Realm")
32 | return
33 | }
34 |
35 | let defaultCategory = defaultCategoriesModel
36 |
37 | try! realm.write {
38 | for category in defaultCategory {
39 | realm.add(category)
40 | }
41 | }
42 | }
43 |
44 | // Метод сохранения категории
45 | func saveCategory(name: String, icon: String, color: String, type: CategoryType) {
46 | guard let realm = try? Realm() else {
47 | print("Ошибка: Не удалось создать экземпляр Realm")
48 | return
49 | }
50 | let newCategory = Category()
51 | newCategory.name = name
52 | newCategory.icon = icon
53 | newCategory.color = color
54 | newCategory.type = type
55 | do {
56 | try realm.write {
57 | realm.add(newCategory)
58 | }
59 | // отладочное сообщение
60 | return print("Категория сохранена: \(newCategory)")
61 | } catch {
62 | // отладочное сообщение
63 | return print("Ошибка сохранения категории: \(error)")
64 | }
65 | }
66 |
67 | // Метод для удаления категории
68 | func deleteCategory(id: ObjectId) {
69 | do {
70 | let realm = try Realm()
71 | if let category = realm.object(ofType: Category.self, forPrimaryKey: id) {
72 | try realm.write {
73 | // Удаление всех транзакций, связанных с категорией
74 | for transaction in category.transactions {
75 | realm.delete(transaction)
76 | }
77 |
78 | // Удаление категории
79 | realm.delete(category)
80 | }
81 | loadData()
82 | }
83 | } catch {
84 | print("Error deleting category: \(error)")
85 | }
86 | }
87 |
88 | // MARK: HomeView
89 | // Функция фильтрует категории из массива categories, сохраняя только те, сумма транзакций которых для заданного типа type (доход или расход) больше 0
90 | func filteredCategories(categories: [Category], type: CategoryType) -> [Category] {
91 | var result: [Category] = []
92 | for category in categories {
93 | if category.categoryAmount(type: type) > 0 {
94 | result.append(category)
95 | }
96 | }
97 | return result
98 | }
99 |
100 | // Функция которая фильтрует список категорий, чтобы найти только те, которые имеют транзакции определенного типа.
101 | func categoriesWithTransaction(categories: Results, type: CategoryType) -> [Category] {
102 | var result: [Category] = []
103 | for category in categories {
104 | if category.hasTransactions(type: type) {
105 | result.append(category)
106 | }
107 | }
108 | return result
109 | }
110 |
111 | // MARK: CategoryView
112 | func filteredCategory(category: Results, type: CategoryType) -> [Category] {
113 | return category.filter { $0.type == type
114 | }
115 | }
116 |
117 | func deleteCategories(category: Results, at offsets: IndexSet, type: CategoryType, transaction: TransactionViewModel) {
118 | let filtered = filteredCategory(category: category, type: type)
119 | offsets.forEach { index in
120 | deleteCategory(id: filtered[index].id)
121 | loadData()
122 | transaction.loadData()
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/iWallet/Model/DefaultCategoriesModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCategoriesModel.swift
3 | // iWallet
4 | //
5 | // Created by Vladislav Novoshinskiy on 24.05.2023.
6 | //
7 |
8 | import Foundation
9 | import RealmSwift
10 |
11 | let defaultCategoriesModel = {
12 | [
13 | // Все категории для расхода
14 | Category(value: ["name": NSLocalizedString("Car", comment: "Car"), "icon": "car", "color": "colorBlue", "type": CategoryType.expense] as [String : Any]),
15 | Category(value: ["name": NSLocalizedString("Bank", comment: "Bank"), "icon": "creditcard", "color": "colorBlue1", "type": CategoryType.expense] as [String : Any]),
16 | Category(value: ["name": NSLocalizedString("Business services", comment: "Business services"), "icon": "person.2", "color": "colorBlue2", "type": CategoryType.expense] as [String : Any]),
17 | Category(value: ["name": NSLocalizedString("Charity", comment: "Charity"), "icon": "figure.roll", "color": "colorGreen", "type": CategoryType.expense] as [String : Any]),
18 | Category(value: ["name": NSLocalizedString("State", comment: "State"), "icon": "network.badge.shield.half.filled", "color": "colorGreen1", "type": CategoryType.expense] as [String : Any]),
19 | Category(value: ["name": NSLocalizedString("Children", comment: "Children"), "icon": "figure.2.and.child.holdinghands", "color": "colorGreen2", "type": CategoryType.expense] as [String : Any]),
20 | Category(value: ["name": NSLocalizedString("House", comment: "House"), "icon": "house", "color": "colorYellow", "type": CategoryType.expense] as [String : Any]),
21 | Category(value: ["name": NSLocalizedString("Pets", comment: "Pets"), "icon": "fish", "color": "colorYellow1", "type": CategoryType.expense] as [String : Any]),
22 | Category(value: ["name": NSLocalizedString("Eating out", comment: "Eating out"), "icon": "popcorn", "color": "colorYellow2", "type": CategoryType.expense] as [String : Any]),
23 | Category(value: ["name": NSLocalizedString("Health", comment: "Health"), "icon": "heart", "color": "colorRed", "type": CategoryType.expense] as [String : Any]),
24 | Category(value: ["name": NSLocalizedString("Beauty", comment: "Beauty"), "icon": "fleuron", "color": "colorRed1", "type": CategoryType.expense] as [String : Any]),
25 | Category(value: ["name": NSLocalizedString("Mobile connection", comment: "Mobile connection"), "icon": "wifi", "color": "colorRed2", "type": CategoryType.expense] as [String : Any]),
26 | Category(value: ["name": NSLocalizedString("Education", comment: "Education"), "icon": "book", "color": "colorBrown", "type": CategoryType.expense] as [String : Any]),
27 | Category(value: ["name": NSLocalizedString("Clothing and footwear", comment: "Clothing and footwear"), "icon": "backpack", "color": "colorBrown1", "type": CategoryType.expense] as [String : Any]),
28 | Category(value: ["name": NSLocalizedString("Present", comment: "Present"), "icon": "gift", "color": "colorBrown2", "type": CategoryType.expense] as [String : Any]),
29 | Category(value: ["name": NSLocalizedString("Food", comment: "Food"), "icon": "cart", "color": "colorPurple", "type": CategoryType.expense] as [String : Any]),
30 | Category(value: ["name": NSLocalizedString("Trips", comment: "Trips"), "icon": "airplane", "color": "colorPurple1", "type": CategoryType.expense] as [String : Any]),
31 | Category(value: ["name": NSLocalizedString("Entertainment", comment: "Entertainment"), "icon": "music.mic", "color": "colorPurple2", "type": CategoryType.expense] as [String : Any]),
32 | Category(value: ["name": NSLocalizedString("Technique", comment: "Technique"), "icon": "display", "color": "colorGray", "type": CategoryType.expense] as [String : Any]),
33 | Category(value: ["name": NSLocalizedString("Transport", comment: "Transport"), "icon": "bus.fill", "color": "colorGray1", "type": CategoryType.expense] as [String : Any]),
34 |
35 | // Все категории для дохода
36 | Category(value: ["name": NSLocalizedString("Rent", comment: "Rent"), "icon": "key", "color": "colorBlue", "type": CategoryType.income] as [String : Any]),
37 | Category(value: ["name": NSLocalizedString("Exchange", comment: "Exchange"), "icon": "arrow.triangle.2.circlepath", "color": "colorBlue1", "type": CategoryType.income] as [String : Any]),
38 | Category(value: ["name": NSLocalizedString("Dividends", comment: "Dividends"), "icon": "chart.xyaxis.line", "color": "colorBlue2", "type": CategoryType.income] as [String : Any]),
39 | Category(value: ["name": NSLocalizedString("Wage", comment: "Wage"), "icon": "dollarsign", "color": "colorGreen", "type": CategoryType.income] as [String : Any]),
40 | Category(value: ["name": NSLocalizedString("Present", comment: "Present"), "icon": "shippingbox.circle", "color": "colorGreen1", "type": CategoryType.income] as [String : Any]),
41 | Category(value: ["name": NSLocalizedString("Part time job", comment: "Part time job"), "icon": "person.fill.checkmark", "color": "colorGreen2", "type": CategoryType.income] as [String : Any]),
42 | Category(value: ["name": NSLocalizedString("Interest on accounts", comment: "Interest on accounts"), "icon": "percent", "color": "colorYellow", "type": CategoryType.income] as [String : Any])
43 | ]
44 | }()
45 |
--------------------------------------------------------------------------------
/iWallet/View/Welcome/WelcomeView.swift:
--------------------------------------------------------------------------------
1 | // WelcomeView.swift
2 |
3 | import SwiftUI
4 | import RealmSwift
5 |
6 | struct WelcomeView: View {
7 | @EnvironmentObject var appVM: AppViewModel
8 | @EnvironmentObject var categoryVM: CategoryViewModel
9 |
10 | @State private var selectedCurrency: Currency = .usd
11 | @State private var createCategories: Bool = true
12 |
13 | var body: some View {
14 | NavigationStack {
15 | ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
16 | ScrollView(.vertical, showsIndicators: false) {
17 | VStack(alignment: .center) {
18 | VStack {
19 | Spacer(minLength: 100)
20 | Image("icon")
21 | .resizable()
22 | .frame(width: 100, height: 100)
23 | Spacer(minLength: 20)
24 | Text("iWallet")
25 | .foregroundColor(.gray).bold()
26 | .font(.largeTitle)
27 | Spacer(minLength: 50)
28 | }
29 | Section {
30 | HStack {
31 | Text(selectedCurrency.symbol)
32 | .foregroundColor(Color("colorBlack"))
33 | .frame(width: 30, height: 30)
34 | .background(Color("colorBrown1"))
35 | .cornerRadius(7.5)
36 | Text("Currency")
37 | Spacer()
38 | Picker("Currency", selection: $selectedCurrency) {
39 | ForEach(Currency.sortedCases, id: \.self) { currency in
40 | Text(currency.rawValue)
41 | .tag(currency)
42 | }
43 | }
44 | }
45 | .padding()
46 | .frame(maxWidth: .infinity, maxHeight: 50)
47 | .background(Color(Colors.colorBalanceBG))
48 | .cornerRadius(12.5)
49 | } header: {
50 | HStack {
51 | Text("Initial application setup").font(.caption).fontWeight(.light).padding(.leading).textCase(.uppercase)
52 | Spacer()
53 | }
54 | }
55 |
56 | HStack {
57 | Text(appVM.roundingNumbers ? "0" : "0.0")
58 | .foregroundColor(Color("colorBlack"))
59 | .frame(width: 30, height: 30)
60 | .background(Color(Colors.colorGreen))
61 | .cornerRadius(7.5)
62 | Toggle("Rounding numbers", isOn: $appVM.roundingNumbers)
63 | }
64 | .padding()
65 | .frame(maxWidth: .infinity, maxHeight: 50)
66 | .background(Color(Colors.colorBalanceBG))
67 | .cornerRadius(12.5)
68 |
69 | VStack(alignment: .leading) {
70 | HStack {
71 | Toggle("Basic categories", isOn: $createCategories)
72 | .toggleStyle(SwitchToggleStyle(tint: Color.green))
73 | }
74 | HStack {
75 | Image(systemName: "exclamationmark.shield")
76 | Text("Note: Enabling this feature will create base categories for expenses and income.")
77 | }
78 | .font(.subheadline)
79 | .fontWeight(.ultraLight)
80 | }
81 | .padding()
82 | .frame(maxWidth: .infinity, maxHeight: 200)
83 | .background(Color(Colors.colorBalanceBG))
84 | .cornerRadius(12.5)
85 | }
86 | }
87 | VStack {
88 | Button {
89 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
90 | appVM.hasRunBefore = true
91 | appVM.currencySymbol = selectedCurrency.symbol
92 | if createCategories {
93 | categoryVM.createDefaultCategories()
94 | }
95 | } label: {
96 | HStack(alignment: .center) {
97 | Text("Continue")
98 | .frame(maxWidth: .infinity, maxHeight: 20)
99 | .padding()
100 | .background(Color.accentColor)
101 | .foregroundColor(.white).bold()
102 | .cornerRadius(15)
103 | }
104 | }
105 | }
106 | }
107 | .padding(15)
108 | .background(Color(Colors.mainBG))
109 | }
110 | .onChange(of: selectedCurrency) { newCurrency in
111 | // Сохраняем символ валюты при изменении выбора
112 | appVM.currencySymbol = newCurrency.symbol
113 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
114 | }
115 | }
116 | }
117 |
118 | struct WelcomeView_Previews: PreviewProvider {
119 | static var previews: some View {
120 | WelcomeView()
121 | }
122 | }
123 |
124 |
--------------------------------------------------------------------------------
/iWallet/View/Transaction/TransactionView.swift:
--------------------------------------------------------------------------------
1 | // TransactionView.swift
2 |
3 | import SwiftUI
4 | import RealmSwift
5 |
6 | struct TransactionView: View {
7 | @EnvironmentObject var appVM: AppViewModel
8 | @EnvironmentObject var transactionVM: TransactionViewModel
9 | @Environment(\.dismiss) var dismiss
10 |
11 | @ObservedResults(TransactionItem.self) var transactions
12 | @ObservedResults(Category.self) var categories
13 |
14 | var body: some View {
15 | NavigationStack {
16 | List {
17 | if transactions.isEmpty {
18 | previewCardTransaction()
19 | } else {
20 | let groupedTransactions = transactionVM.transactionsByDate(Array(transactions))
21 | ForEach(groupedTransactions.keys.sorted(by: { $0 > $1 }), id: \.self) { date in
22 | Section(header: Text(date, style: .date).bold()) {
23 | let sortedTransactions = transactionVM.sortTransactionsByDate(transactions: groupedTransactions[date]!)
24 |
25 | ForEach(sortedTransactions, id: \.self) { transaction in
26 | if let category = transactionVM.filterCategories(categories: Array(categories), transaction: transaction) {
27 | NavigationLink(destination: EditTransactionView(selectedTransaction: transaction)) {
28 | transactionRow(transaction: transaction, category: category)
29 | }
30 | }
31 | }
32 | .onDelete(perform: { indexSet in
33 | transactionVM.deleteTransaction(at: indexSet, from: sortedTransactions)
34 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
35 | })
36 | }
37 | }
38 | }
39 | }
40 | .scrollContentBackground(.hidden)
41 | .background(Color("colorBG"))
42 | .navigationBarTitle("Transactions", displayMode: .inline)
43 | .toolbar {
44 | ToolbarItem(placement: .navigationBarLeading) {
45 | Button {
46 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
47 | dismiss()
48 | } label: {
49 | Text("Back")
50 | }
51 | }
52 |
53 | ToolbarItem(placement: .navigationBarTrailing) {
54 | if transactions.isEmpty {
55 | EditButton().disabled(true)
56 | } else {
57 | EditButton()
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
64 | // Метод для оптимизации отображения списка
65 | @ViewBuilder
66 | private func transactionRow(transaction: TransactionItem, category: Category) -> some View {
67 | VStack(alignment: .leading, spacing: 0) {
68 | HStack {
69 | HStack {
70 | Divider()
71 | .foregroundColor(Color(category.color))
72 | .frame(width: 5, height: 72)
73 | .background(Color(category.color))
74 | } .padding(.trailing, 3)
75 |
76 | VStack(alignment: .leading) {
77 | HStack {
78 | if transaction.type == CategoryType.expense {
79 | Text("-\(transaction.amount.formattedWithSeparatorAndCurrency(roundingNumbers: appVM.roundingNumbers)) \(appVM.currencySymbol)")
80 | .font(.title3).bold()
81 | } else {
82 | Text("\(transaction.amount.formattedWithSeparatorAndCurrency(roundingNumbers: appVM.roundingNumbers)) \(appVM.currencySymbol)")
83 | .font(.title3).bold()
84 | }
85 | Spacer()
86 | HStack {
87 | Text(category.name)
88 | .foregroundColor(Color("colorBalanceText")).textCase(.uppercase)
89 | .font(.caption)
90 | .multilineTextAlignment(.trailing)
91 | .dynamicTypeSize(.small)
92 | .padding(0)
93 | Image(systemName: category.icon)
94 | .font(.caption).dynamicTypeSize(.small)
95 | .foregroundColor(.black)
96 | .frame(width: 20, height: 20)
97 | .background(Color(category.color))
98 | .cornerRadius(5)
99 | .padding(0)
100 | } .padding(0)
101 | }
102 | HStack {
103 | Text(transaction.note)
104 | .foregroundColor(Color(.gray)).textCase(.uppercase)
105 | .font(.subheadline).dynamicTypeSize(.small)
106 | Spacer()
107 | Text(category.type.localizedName())
108 | .foregroundColor(Color(.gray)).textCase(.uppercase)
109 | .font(.subheadline).dynamicTypeSize(.small)
110 | }
111 | } .padding(.leading, 10)
112 | }
113 | }
114 | .padding(.vertical, 5)
115 | .frame(height: 50)
116 | }
117 | }
118 |
119 | struct TransactionView_Previews: PreviewProvider {
120 | static var previews: some View {
121 | TransactionView()
122 | .environmentObject(AppViewModel())
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/iWallet/View/Transaction/TransactionCategoryView.swift:
--------------------------------------------------------------------------------
1 | // TransactionCategoryView.swift
2 |
3 | import SwiftUI
4 | import RealmSwift
5 |
6 | struct TransactionCategoryView: View {
7 | @EnvironmentObject var appVM: AppViewModel
8 | @EnvironmentObject var transactionVM: TransactionViewModel
9 | @ObservedResults(TransactionItem.self) var transactions
10 |
11 | @State var selectedCategory: Category
12 |
13 | var filteredTransactions: [TransactionItem] {
14 | transactionVM.filterTransaction(category: selectedCategory, transactions: Array(transactions))
15 | }
16 |
17 | var body: some View {
18 | List {
19 | if transactions.isEmpty {
20 | previewCardTransaction()
21 | } else {
22 | let groupedTransactions = transactionVM.transactionsByDate(Array(filteredTransactions))
23 | ForEach(groupedTransactions.keys.sorted(by: { $0 > $1 }), id: \.self) { date in
24 | Section(header: Text(date, style: .date).bold()) {
25 | let sortedTransactions = transactionVM.sortTransactionsByDate(transactions: groupedTransactions[date]!)
26 |
27 | ForEach(sortedTransactions, id: \.self) { transaction in
28 | NavigationLink(destination: EditTransactionView(selectedTransaction: transaction)) {
29 | VStack(alignment: .leading, spacing: 0) {
30 | HStack {
31 | HStack {
32 | Divider()
33 | .foregroundColor(Color(selectedCategory.color))
34 | .frame(width: 5, height: 72)
35 | .background(Color(selectedCategory.color))
36 | } .padding(.trailing, 3)
37 |
38 | VStack(alignment: .leading) {
39 | HStack {
40 | if transaction.type == CategoryType.expense {
41 | Text("-\(transaction.amount.formattedWithSeparatorAndCurrency(roundingNumbers: appVM.roundingNumbers)) \(appVM.currencySymbol)")
42 | .font(.title3).bold()
43 | } else {
44 | Text("\(transaction.amount.formattedWithSeparatorAndCurrency(roundingNumbers: appVM.roundingNumbers)) \(appVM.currencySymbol)")
45 | .font(.title3).bold()
46 | }
47 | Spacer()
48 | HStack {
49 | Text(selectedCategory.name)
50 | .foregroundColor(Color("colorBalanceText")).textCase(.uppercase)
51 | .font(.caption)
52 | .multilineTextAlignment(.trailing)
53 | .dynamicTypeSize(.small)
54 | .padding(0)
55 | Image(systemName: selectedCategory.icon)
56 | .font(.caption).dynamicTypeSize(.small)
57 | .foregroundColor(.black)
58 | .frame(width: 20, height: 20)
59 | .background(Color(selectedCategory.color))
60 | .cornerRadius(5)
61 | .padding(0)
62 | } .padding(0)
63 | }
64 | HStack {
65 | Text(transaction.note)
66 | .foregroundColor(Color(.gray)).textCase(.uppercase)
67 | .font(.subheadline).dynamicTypeSize(.small)
68 | Spacer()
69 | Text(selectedCategory.type.localizedName())
70 | .foregroundColor(Color(.gray)).textCase(.uppercase)
71 | .font(.subheadline).dynamicTypeSize(.small)
72 | }
73 | } .padding(.leading, 10)
74 | }
75 | }
76 | .padding(.vertical, 5)
77 | .frame(height: 50)
78 | }
79 | }
80 | .onDelete(perform: { indexSet in
81 | transactionVM.deleteTransaction(at: indexSet, from: sortedTransactions)
82 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
83 | })
84 | }
85 | }
86 | }
87 | }
88 | .navigationTitle(selectedCategory.name)
89 | .background(Color("colorBG"))
90 | .scrollContentBackground(.hidden)
91 | .toolbar {
92 | ToolbarItem(placement: .navigationBarTrailing) {
93 | if transactions.isEmpty {
94 | EditButton().disabled(true)
95 | } else {
96 | EditButton()
97 | }
98 | }
99 | }
100 | }
101 | }
102 |
103 | struct TransactionCategoryView_Previews: PreviewProvider {
104 | static var previews: some View {
105 | TransactionCategoryView(selectedCategory: Category())
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/iWallet/View/Category/AddCategoryView.swift:
--------------------------------------------------------------------------------
1 | // AddCategoryView.swift
2 |
3 | import SwiftUI
4 |
5 | struct AddCategoryView: View {
6 | @EnvironmentObject var appVM: AppViewModel
7 | @EnvironmentObject var categoryVM: CategoryViewModel
8 | @Environment(\.dismiss) var dismiss
9 |
10 | @FocusState private var nameIsFocused: Bool
11 |
12 | @State var selectedType: CategoryType = .expense
13 | @State private var name: String = ""
14 | @State private var selectedImage: String = "folder.circle"
15 | @State private var selectedColor: String = "colorBlue"
16 | @State private var showAlert: Bool = false
17 |
18 | var body: some View {
19 | NavigationStack {
20 | VStack {
21 | ScrollView(.vertical, showsIndicators: false) {
22 | VStack(alignment: .leading) {
23 | HStack(alignment: .center) {
24 | Spacer()
25 | Image(systemName: selectedImage)
26 | .foregroundColor(Color(.black))
27 | .font(.system(size: 50))
28 | .frame(width: 100, height: 100)
29 | .background(Color(selectedColor))
30 | .cornerRadius(25)
31 | Spacer()
32 | } .padding(.bottom, 15)
33 |
34 | Section {
35 | VStack(alignment: .leading) {
36 | Picker("Type", selection: $selectedType) {
37 | ForEach(CategoryType.allCases, id: \.self) { type in
38 | Text(type.localizedName())
39 | }
40 | }
41 | .pickerStyle(.segmented)
42 | .padding()
43 | .frame(maxWidth: .infinity, maxHeight: .infinity)
44 | .background(Color("colorBalanceBG"))
45 | .cornerRadius(10)
46 | .padding(.bottom, 15)
47 | }
48 | } header: {
49 | Text("Select type:")
50 | .font(.caption).textCase(.uppercase)
51 | .padding(.leading, 10)
52 | }
53 |
54 | Section {
55 | VStack(alignment: .leading) {
56 | TextField("Name", text: $name)
57 | .padding()
58 | .frame(maxWidth: .infinity, maxHeight: .infinity)
59 | .background(Color("colorBalanceBG"))
60 | .cornerRadius(10)
61 | .padding(.bottom, 15)
62 | .focused($nameIsFocused)
63 | }
64 | } header: {
65 | Text("Enter Name")
66 | .font(.caption).textCase(.uppercase)
67 | .padding(.leading, 10)
68 | }
69 | .onTapGesture {
70 | nameIsFocused.toggle()
71 | }
72 |
73 | Section {
74 | IconPickerView(selectedImage: $selectedImage)
75 | .foregroundColor(Color(.black))
76 | .padding()
77 | .frame(maxWidth: .infinity, maxHeight: .infinity)
78 | .background(Color("colorBalanceBG"))
79 | .cornerRadius(10)
80 | .padding(.bottom, 15)
81 | } header: {
82 | Text("Choose an icon:")
83 | .font(.caption).textCase(.uppercase)
84 | .padding(.leading, 10)
85 | }
86 | Section {
87 | ColorPickerView(selectedColor: $selectedColor)
88 | .padding(5)
89 | .frame(maxWidth: .infinity, maxHeight: .infinity)
90 | .background(Color("colorBalanceBG"))
91 | .cornerRadius(10)
92 |
93 | } header: {
94 | Text("Choose color:")
95 | .font(.caption).textCase(.uppercase)
96 | .padding(.leading, 10)
97 | }
98 | }
99 | .padding(.horizontal, 15)
100 | .padding(.top, 20)
101 | }
102 | }
103 | .background(Color("colorBG"))
104 | .navigationBarTitle("Create a category", displayMode: .inline)
105 | .scrollDismissesKeyboard(.immediately)
106 | .toolbar {
107 | ToolbarItem(placement: .navigationBarTrailing) {
108 | Button {
109 | if name.isEmpty {
110 | showAlert.toggle()
111 | } else {
112 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
113 | categoryVM.saveCategory(name: name, icon: selectedImage, color: selectedColor, type: selectedType)
114 | dismiss()
115 | }
116 | } label: {
117 | if name.isEmpty {
118 | Text("Add")
119 | .foregroundColor(.gray)
120 | } else {
121 | Text("Add")
122 | }
123 | }
124 | }
125 | }
126 | .alert("Пожалуйста введите название категории", isPresented: $showAlert) {
127 | Button("Okay", role: .cancel) {
128 | }
129 | }
130 | }
131 | }
132 | }
133 |
134 | struct AddCategoryView_Previews: PreviewProvider {
135 | static var previews: some View {
136 | AddCategoryView()
137 | .environmentObject(AppViewModel())
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/iWallet/View/Home/HomeView.swift:
--------------------------------------------------------------------------------
1 | // ContentView.swift
2 |
3 | import SwiftUI
4 | import RealmSwift
5 |
6 | struct HomeView: View {
7 | @EnvironmentObject var appVM: AppViewModel
8 | @EnvironmentObject var categoryVM: CategoryViewModel
9 | @EnvironmentObject var transactionVM: TransactionViewModel
10 | @ObservedResults(Category.self) var categories
11 |
12 | @State private var showAddTransaction: Bool = false
13 | @State private var selectedCategoryType: CategoryType = .expense
14 |
15 | private let adaptive =
16 | [
17 | GridItem(.adaptive(minimum: 165))
18 | ]
19 |
20 | var body: some View {
21 | NavigationStack {
22 | ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
23 | ScrollView(.vertical, showsIndicators: false) {
24 | LazyVGrid(columns: adaptive) {
25 |
26 | BalanceView(amount: transactionVM.balance(), curren: appVM.currencySymbol, type: NSLocalizedString("Balance", comment: "Balance"), icon: "equal.circle", iconBG: Color(Colors.colorBlue))
27 |
28 | BalanceView(amount: transactionVM.averageDailyExpense(), curren: appVM.currencySymbol, type: NSLocalizedString("Expense average", comment: "Expense average"), icon: "plusminus.circle", iconBG: Color(Colors.colorYellow))
29 |
30 | BalanceView(amount: transactionVM.totalIncomes(), curren: appVM.currencySymbol, type: NSLocalizedString("Income", comment: "Income"), icon: "plus.circle", iconBG: Color(Colors.colorGreen))
31 |
32 | BalanceView(amount: transactionVM.totalExpenses(), curren: appVM.currencySymbol, type: NSLocalizedString("Expense", comment: "Expense"), icon: "minus.circle", iconBG: Color(Colors.colorRed))
33 |
34 | }
35 | .padding(.horizontal)
36 | .padding(.top)
37 |
38 | Picker("Тип", selection: $selectedCategoryType) {
39 | ForEach(CategoryType.allCases, id: \.self) { type in
40 | Text(type.localizedName())
41 | }
42 | }
43 | .pickerStyle(SegmentedPickerStyle())
44 | .padding(10)
45 | .frame(maxWidth: .infinity, maxHeight: .infinity)
46 | .background(Color(Colors.colorBalanceBG))
47 | .cornerRadius(10)
48 | .padding(.horizontal, 15)
49 |
50 | VStack(spacing: 0) {
51 | // создаем массив транзакций по категориями
52 | let categoriesWithTransactionsArray = categoryVM.categoriesWithTransaction(categories: categories, type: selectedCategoryType)
53 |
54 | // фильтруем категории по типу
55 | var filteredCategoriesArray = categoryVM.filteredCategories(categories: categoriesWithTransactionsArray, type: selectedCategoryType)
56 |
57 | // сортируем категории по сумме
58 | let _: () = filteredCategoriesArray.sort(by: { $0.categoryAmount(type: selectedCategoryType) > $1.categoryAmount(type: selectedCategoryType)})
59 |
60 | if filteredCategoriesArray.isEmpty {
61 | previewHomeTransaction()
62 | } else {
63 |
64 | ForEach(filteredCategoriesArray, id: \.self) { category in
65 | let totalAmount = category.categoryAmount(type: selectedCategoryType)
66 | NavigationLink(destination: TransactionCategoryView(selectedCategory: category)) {
67 |
68 | CategoryItemView(categoryColor: category.color, categoryIcon: category.icon, categoryName: category.name, totalAmount: totalAmount, currencySymbol: appVM.currencySymbol)
69 | }
70 | }
71 | }
72 | }
73 | .cornerRadius(10)
74 | .padding(.horizontal, 15)
75 | .padding(.bottom, 100)
76 | }
77 |
78 |
79 | HStack {
80 | Button {
81 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
82 | showAddTransaction.toggle()
83 | } label: {
84 | ZStack {
85 | Circle()
86 | .frame(width: 55, height: 55)
87 | .foregroundColor(Color("colorBalanceText"))
88 | Image(systemName: "plus")
89 | .foregroundColor(Color("colorBG"))
90 | .font(.system(size: 30))
91 | }
92 | }
93 | }
94 | .padding(.all, 25)
95 | .toolbar {
96 | ToolbarItem(placement: .navigationBarTrailing) {
97 | Button {
98 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
99 | } label: {
100 | NavigationLink(destination: SettingView(), label: {
101 | HStack {
102 | Text("Settings")
103 | }
104 | })
105 | }
106 | }
107 | ToolbarItem(placement: .navigationBarLeading) {
108 | Text("iWallet")
109 | .font(.title)
110 | .fontWeight(.bold)
111 | .foregroundColor(Color("colorBalanceText"))
112 | }
113 | }
114 | }
115 | .background(Color("colorBG"))
116 | }
117 | .sheet(isPresented: $showAddTransaction) {
118 | AddTransactionView(selectedCategory: Category(), selectedType: selectedCategoryType)
119 | }
120 | }
121 | }
122 |
123 | struct HomeView_Previews: PreviewProvider {
124 | static var previews: some View {
125 | HomeView()
126 | .environmentObject(AppViewModel())
127 | .environmentObject(TransactionViewModel())
128 | .environmentObject(CategoryViewModel())
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/iWallet/Resources/Icons.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Icons.swift
3 | // iWallet
4 | //
5 | // Created by Vladislav Novoshinskiy on 29.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Icons {
11 |
12 | // все иконки создания категории
13 | static let car: String = "car"
14 | static let box_truck: String = "box.truck"
15 | static let car_2: String = "car.2"
16 | static let creditcard: String = "creditcard"
17 | static let banknote: String = "banknote"
18 | static let giftcard: String = "giftcard"
19 | static let person_2: String = "person.2"
20 | static let person_bust: String = "person.bust"
21 | static let figure_wave: String = "figure.wave"
22 | static let figure_roll: String = "figure.roll"
23 | static let circle_hexagonpath: String = "circle.hexagonpath"
24 | static let captions_bubble: String = "captions.bubble"
25 | static let network_badge_shield_half_filled: String = "network.badge.shield.half.filled"
26 | static let icloud: String = "icloud"
27 | static let bonjour: String = "bonjour"
28 | static let figure_2_and_child_holdinghands: String = "figure.2.and.child.holdinghands"
29 | static let figure_and_child_holdinghands: String = "figure.and.child.holdinghands"
30 | static let face_smiling: String = "face.smiling"
31 | static let house: String = "house"
32 | static let sofa: String = "sofa"
33 | static let fireplace: String = "fireplace"
34 | static let fish: String = "fish"
35 | static let hare: String = "hare"
36 | static let pawprint: String = "pawprint"
37 | static let popcorn: String = "popcorn"
38 | static let balloon_2: String = "balloon.2"
39 | static let figure_walk: String = "figure.walk"
40 | static let heart: String = "heart"
41 | static let pill: String = "pill"
42 | static let cross: String = "cross"
43 | static let fleuron: String = "fleuron"
44 | static let wand_and_stars_inverse: String = "wand.and.stars.inverse"
45 | static let pencil_tip: String = "pencil.tip"
46 | static let wifi: String = "wifi"
47 | static let antenna_radiowaves_left_and_right: String = "antenna.radiowaves.left.and.right"
48 | static let network: String = "network"
49 | static let book: String = "book"
50 | static let graduationcap: String = "graduationcap"
51 | static let pencil_and_ruler: String = "pencil.and.ruler"
52 | static let backpack: String = "backpack"
53 | static let tshirt: String = "tshirt"
54 | static let tag: String = "tag"
55 | static let gift: String = "gift"
56 | static let shippingbox: String = "shippingbox"
57 | static let party_popper_fill: String = "party.popper.fill"
58 | static let cart: String = "cart"
59 | static let carrot: String = "carrot"
60 | static let airpods_chargingcase: String = "airpods.chargingcase"
61 | static let airplane: String = "airplane"
62 | static let sailboat: String = "sailboat"
63 | static let road_lanes: String = "road.lanes"
64 | static let music_mic: String = "music.mic"
65 | static let theatermasks: String = "theatermasks"
66 | static let birthday_cake: String = "birthday.cake"
67 | static let display: String = "display"
68 | static let applewatch_watchface: String = "applewatch.watchface"
69 | static let gamecontroller: String = "gamecontroller"
70 | static let bus_fill: String = "bus.fill"
71 | static let cablecar: String = "cablecar"
72 | static let steeringwheel: String = "steeringwheel"
73 | static let key: String = "key"
74 | static let door_right_hand_closed: String = "door.right.hand.closed"
75 | static let house_and_flag: String = "house.and.flag"
76 | static let arrow_triangle_2_circlepath: String = "arrow.triangle.2.circlepath"
77 | static let dollarsign_arrow_circlepath: String = "dollarsign.arrow.circlepath"
78 | static let hourglass: String = "hourglass"
79 | static let chart_xyaxis_line: String = "chart.xyaxis.line"
80 | static let creditcard_and_123: String = "creditcard.and.123"
81 | static let chart_bar: String = "chart.bar"
82 | static let dollarsign: String = "dollarsign"
83 | static let rublesign: String = "rublesign"
84 | static let eurosign: String = "eurosign"
85 | static let shippingbox_circle: String = "shippingbox.circle"
86 | static let timelapse: String = "timelapse"
87 | static let camera_metering_matrix: String = "camera.metering.matrix"
88 | static let person_fill_checkmark: String = "person.fill.checkmark"
89 | static let person_crop_square_filled_and_at_rectangle_fill: String = "person.crop.square.filled.and.at.rectangle.fill"
90 | static let hand_thumbsup: String = "hand.thumbsup"
91 | static let percent: String = "percent"
92 | static let sum: String = "sum"
93 | static let number: String = "number"
94 |
95 | static let allIcons: [String] =
96 | [
97 | car,
98 | box_truck,
99 | car_2,
100 | creditcard,
101 | banknote,
102 | giftcard,
103 | person_2,
104 | person_bust,
105 | figure_wave,
106 | figure_roll,
107 | circle_hexagonpath,
108 | captions_bubble,
109 | network_badge_shield_half_filled,
110 | icloud,
111 | bonjour,
112 | figure_2_and_child_holdinghands,
113 | figure_and_child_holdinghands,
114 | face_smiling,
115 | house,
116 | sofa,
117 | fireplace,
118 | fish,
119 | hare,
120 | pawprint,
121 | popcorn,
122 | balloon_2,
123 | figure_walk,
124 | heart,
125 | pill,
126 | cross,
127 | fleuron,
128 | wand_and_stars_inverse,
129 | pencil_tip,
130 | wifi,
131 | antenna_radiowaves_left_and_right,
132 | network,
133 | book,
134 | graduationcap,
135 | pencil_and_ruler,
136 | backpack,
137 | tshirt,
138 | tag,
139 | gift,
140 | shippingbox,
141 | party_popper_fill,
142 | cart,
143 | carrot,
144 | airpods_chargingcase,
145 | airplane,
146 | sailboat,
147 | road_lanes,
148 | music_mic,
149 | theatermasks,
150 | birthday_cake,
151 | display,
152 | applewatch_watchface,
153 | gamecontroller,
154 | bus_fill,
155 | cablecar,
156 | steeringwheel,
157 | key,
158 | door_right_hand_closed,
159 | house_and_flag,
160 | arrow_triangle_2_circlepath,
161 | dollarsign_arrow_circlepath,
162 | hourglass,
163 | chart_xyaxis_line,
164 | creditcard_and_123,
165 | chart_bar,
166 | dollarsign,
167 | rublesign,
168 | eurosign,
169 | shippingbox_circle,
170 | timelapse,
171 | camera_metering_matrix,
172 | person_fill_checkmark,
173 | person_crop_square_filled_and_at_rectangle_fill,
174 | hand_thumbsup,
175 | percent,
176 | sum,
177 | number
178 | ]
179 | }
180 |
--------------------------------------------------------------------------------
/iWallet/View/Transaction/EditTransactionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EditTransactionView.swift
3 | // iWallet
4 | //
5 | // Created by Vladislav Novoshinskiy on 13.06.2023.
6 | //
7 |
8 | import SwiftUI
9 | import RealmSwift
10 |
11 | struct EditTransactionView: View {
12 | @EnvironmentObject private var appVM: AppViewModel
13 | @EnvironmentObject private var transactionVM: TransactionViewModel
14 | @EnvironmentObject private var categoryVM: CategoryViewModel
15 | @Environment(\.dismiss) private var dismiss
16 |
17 | @State var selectedTransaction: TransactionItem
18 | @State private var alertAmount: Bool = false
19 |
20 | @FocusState private var amountIsFocused: Bool
21 | @FocusState private var noteIsFocused: Bool
22 |
23 | let formatter: NumberFormatter = {
24 | let formatter = NumberFormatter()
25 | formatter.numberStyle = .decimal
26 | if AppViewModel().roundingNumbers == true {
27 | formatter.maximumFractionDigits = 0
28 | } else {
29 | formatter.maximumFractionDigits = 2
30 | }
31 | return formatter
32 | }()
33 |
34 | var body: some View {
35 | ScrollView(.vertical, showsIndicators: false) {
36 | VStack(alignment: .leading) {
37 | Section {
38 | TextField(selectedTransaction.type == .expense ? "-100 \(appVM.currencySymbol)" : "+100 \(appVM.currencySymbol)", value: $selectedTransaction.amount, formatter: formatter)
39 | .font(.title3)
40 | .keyboardType(appVM.roundingNumbers ? .numberPad : .decimalPad)
41 | .padding()
42 | .frame(maxWidth: .infinity, maxHeight: .infinity)
43 | .background(Color("colorBalanceBG"))
44 | .cornerRadius(10)
45 | .padding(.bottom, 15)
46 | .focused($amountIsFocused)
47 | } header: {
48 | Text("Enter amount:")
49 | .font(.caption).textCase(.uppercase)
50 | .padding(.leading, 10)
51 | }
52 | .onTapGesture {
53 | amountIsFocused.toggle()
54 | }
55 |
56 | Section {
57 | TextField("Note", text: $selectedTransaction.note)
58 | .padding()
59 | .frame(maxWidth: .infinity, maxHeight: .infinity)
60 | .background(Color("colorBalanceBG"))
61 | .cornerRadius(10)
62 | .focused($noteIsFocused)
63 |
64 | ScrollView(.horizontal, showsIndicators: false) {
65 | HStack {
66 | // находит категорию в которую была записана транзакция
67 | let categoryResult = categorySearch(categories: categoryVM.categories)
68 |
69 | // находит все заметки в той категории в которую была записанна транзакция
70 | let filterTransaction = transactionVM.filterTransactionsNote(category: categoryResult, transactions: transactionVM.transactions)
71 |
72 | ForEach(filterTransaction.reversed(), id: \.self) { notes in
73 | Button {
74 | selectedTransaction.note = notes.note
75 | } label: {
76 | Text(String(notes.note.prefix(20)))
77 | .font(Font.caption)
78 | .foregroundColor(Color(.systemGray2))
79 | .padding(.vertical, 5)
80 | .padding(.horizontal, 10)
81 | .background(
82 | RoundedRectangle(cornerRadius: 7, style: .continuous)
83 | .strokeBorder(Color(.systemGray2))
84 | )
85 | .padding(.bottom, 10)
86 | }
87 | }
88 | }
89 | .padding(.horizontal, 10)
90 | }
91 | } header: {
92 | Text("Enter note:")
93 | .font(.caption).textCase(.uppercase)
94 | .padding(.leading, 10)
95 | }
96 | .onTapGesture {
97 | noteIsFocused.toggle()
98 | }
99 |
100 | Section {
101 | HStack {
102 | DatePicker("Date", selection: $selectedTransaction.date, displayedComponents: .date)
103 | }
104 | .pickerStyle(SegmentedPickerStyle())
105 | .padding()
106 | .frame(maxWidth: .infinity, maxHeight: .infinity)
107 | .background(Color("colorBalanceBG"))
108 | .cornerRadius(10)
109 | } header: {
110 | Text("Enter date:")
111 | .font(.caption).textCase(.uppercase)
112 | .padding(.leading, 10)
113 | .padding(.top, 10)
114 | }
115 | }
116 | .padding(.horizontal, 15)
117 | .padding(.top, 20)
118 | }
119 | .navigationBarTitle("Editing", displayMode: .inline)
120 | .scrollDismissesKeyboard(.immediately)
121 | .background(Color("colorBG"))
122 | .toolbar {
123 | ToolbarItem(placement: .navigationBarTrailing) {
124 | Button {
125 | if selectedTransaction.amount.formattedWithSeparatorAndCurrency(roundingNumbers: appVM.roundingNumbers).count == 0 {
126 | alertAmount = true
127 | } else {
128 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
129 | dismiss()
130 | }
131 | } label: {
132 | Text("Edit")
133 | }
134 | }
135 | }
136 | .alert("Please enter amount", isPresented: $alertAmount) {
137 | Button("Okay", role: .cancel) { }
138 | }
139 | }
140 |
141 | // находит категорию в которую была записана транзакция
142 | func categorySearch(categories: [Category]) -> Category {
143 | var result = Category()
144 |
145 | for category in categories {
146 | if category.id == selectedTransaction.categoryId {
147 | if category.hasTransactions(type: selectedTransaction.type) {
148 | result = category
149 | }
150 | }
151 | }
152 | return result
153 | }
154 | }
155 |
156 | struct EditTransactionView_Previews: PreviewProvider {
157 | static var previews: some View {
158 | EditTransactionView(selectedTransaction: TransactionItem())
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/iWallet/ViewModel/TransactionViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransactionViewModel.swift
3 | // iWallet
4 | //
5 | // Created by Vladislav Novoshinskiy on 25.05.2023.
6 | //
7 |
8 | import Foundation
9 | import RealmSwift
10 |
11 | final class TransactionViewModel: ObservableObject {
12 | @Published var transactions: [TransactionItem] = []
13 |
14 | init() {
15 | loadData()
16 | }
17 |
18 | // Метод для загрузки транзакций
19 | func loadData() {
20 | guard let realm = try? Realm() else {
21 | print("Ошибка: loadData")
22 | return
23 | }
24 | let transactionsResult = realm.objects(TransactionItem.self)
25 | transactions = Array(transactionsResult)
26 | }
27 |
28 | // Метод сохранения транзакции
29 | func saveTransaction(amount: Float, date: Date, note: String, type: CategoryType, category: Category) {
30 | guard let realm = try? Realm() else {
31 | print("Ошибка: Не удалось создать экземпляр Realm")
32 | return
33 | }
34 | if let newCategory = realm.object(ofType: Category.self, forPrimaryKey: category.id) {
35 | let newTransaction = TransactionItem()
36 | newTransaction.categoryId = newCategory.id
37 | newTransaction.amount = amount
38 | newTransaction.date = date
39 | newTransaction.note = note
40 | newTransaction.type = type
41 | do {
42 | try realm.write {
43 | newCategory.transactions.append(newTransaction)
44 | }
45 |
46 | // Обновляю список транзакций после сохранения
47 | transactions.append(newTransaction)
48 |
49 | // Отладочное сообщение
50 | print("Транзакция сохранена: \(newTransaction)")
51 | } catch {
52 | // Отладочное сообщение
53 | print("Ошибка сохранения транзакции: \(error)")
54 | }
55 | } else {
56 | // Отладочное сообщение, если категория не найдена
57 | print("Ошибка: категория не найдена")
58 | }
59 | }
60 |
61 | // Метод для удаления транзакций
62 | private func deleteTransaction(withId id: ObjectId) {
63 | do {
64 | let realm = try Realm()
65 |
66 | if let transaction = realm.object(ofType: TransactionItem.self, forPrimaryKey: id) {
67 | try realm.write {
68 | if let category = transaction.category.first {
69 | if let index = category.transactions.firstIndex(of: transaction) {
70 | category.transactions.remove(at: index)
71 | }
72 | }
73 | realm.delete(transaction)
74 | }
75 | loadData()
76 | } else {
77 | print("Транзакция с ID \(id) не найдена")
78 | }
79 | } catch let error {
80 | print("Ошибка удаления транзакции: \(error)")
81 | }
82 | }
83 |
84 | // Метод удаления транзакций
85 | func deleteTransaction(at offsets: IndexSet, from sortedTransactions: [TransactionItem]) {
86 | offsets.forEach { index in
87 | let transaction = sortedTransactions[index]
88 | deleteTransaction(withId: transaction.id)
89 | }
90 | }
91 |
92 | // MARK: TransactionView
93 | // Метод для группировки транзакций по дате
94 | func transactionsByDate(_ transactions: [TransactionItem]) -> [Date: [TransactionItem]] {
95 | var groupedTransactions: [Date: [TransactionItem]] = [:]
96 |
97 | let dateFormatter = DateFormatter()
98 | dateFormatter.dateStyle = .medium
99 | dateFormatter.timeStyle = .none
100 |
101 | for transaction in transactions {
102 | let dateString = dateFormatter.string(from: transaction.date)
103 | if let date = dateFormatter.date(from: dateString) {
104 | if groupedTransactions[date] == nil {
105 | groupedTransactions[date] = []
106 | }
107 | groupedTransactions[date]?.append(transaction)
108 | }
109 | }
110 |
111 | return groupedTransactions
112 | }
113 |
114 | // Метод фильтрации категорий
115 | func filterCategories(categories: [Category], transaction: TransactionItem) -> Category? {
116 | for category in categories {
117 | if category.id == transaction.categoryId {
118 | return category
119 | }
120 | }
121 | return nil
122 | }
123 |
124 | // метод сортировки транзакций по дате
125 | func sortTransactionsByDate(transactions: [TransactionItem]) -> [TransactionItem] {
126 | return transactions.sorted(by: { $0.date > $1.date })
127 | }
128 |
129 | // MARK: TransactionCategoryView
130 | // метод фильтрации транзакции с выбранной категорией
131 | func filterTransaction(category: Category, transactions: [TransactionItem]) -> [TransactionItem] {
132 | var groupedTransaction: [TransactionItem] = []
133 |
134 | for transaction in transactions {
135 | if category.id == transaction.categoryId {
136 | groupedTransaction.append(transaction)
137 | }
138 | }
139 | return groupedTransaction
140 | }
141 |
142 | // MARK: AddTransactionView
143 | // метод фильтрации транзакций для отбора уникальных заметок
144 | func filterTransactionsNote(category: Category, transactions: [TransactionItem]) -> [TransactionItem] {
145 | var groupedTransaction: [TransactionItem] = []
146 | var uniqueNotes: [String] = []
147 |
148 | for transaction in transactions.prefix(20) {
149 | if category.id == transaction.categoryId {
150 | if transaction.note.count != 0 {
151 | if !uniqueNotes.contains(transaction.note.description) {
152 | uniqueNotes.append(transaction.note.description)
153 | groupedTransaction.append(transaction)
154 | }
155 | }
156 | }
157 | }
158 | return groupedTransaction
159 | }
160 |
161 | // MARK: HomeView
162 | // Считает расход
163 | func totalExpenses() -> Float {
164 | var expenses: Float = 0
165 | for transaction in transactions {
166 | if transaction.type == .expense {
167 | expenses += transaction.amount
168 | }
169 | }
170 | return expenses
171 | }
172 |
173 | // Считает доход
174 | func totalIncomes() -> Float {
175 | var icncome: Float = 0
176 | for transaction in transactions {
177 | if transaction.type == .income {
178 | icncome += transaction.amount
179 | }
180 | }
181 | return icncome
182 | }
183 |
184 | // Считает балланс
185 | func balance() -> Float {
186 | return totalIncomes() - totalExpenses()
187 | }
188 |
189 | // Расчет среднего расхода за день, сначала нахожу общее количество дней c транзакциями, а затем разделим общую сумму расходных транзакций на количество дней
190 | func averageDailyExpense() -> Float {
191 | let expenseTransactions = transactions.filter { $0.type == .expense }
192 | guard !expenseTransactions.isEmpty else {
193 | return 0
194 | }
195 |
196 | let uniqueExpenseDates = Set(expenseTransactions.map { transaction -> Date in
197 | let calendar = Calendar.current
198 | let components = calendar.dateComponents([.year, .month, .day], from: transaction.date)
199 | return calendar.date(from: components) ?? transaction.date
200 | })
201 |
202 | let daysWithTransactions = uniqueExpenseDates.count
203 |
204 | let totalExpenseAmount = totalExpenses()
205 |
206 | return totalExpenseAmount / Float(daysWithTransactions)
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/iWallet/View/Setting/SettingView.swift:
--------------------------------------------------------------------------------
1 | // SettingView.swift
2 |
3 | import SwiftUI
4 |
5 | struct SettingView: View {
6 | @EnvironmentObject var appVM: AppViewModel
7 | @Environment(\.dismiss) var dismiss
8 | @Environment(\.openURL) var openURL
9 |
10 | @State private var selectedCurrency: Currency = .usd
11 | @State private var showCategory: Bool = false
12 | @State private var showTransactionView: Bool = false
13 |
14 | var body: some View {
15 | NavigationStack {
16 | VStack {
17 | List {
18 | Section {
19 | Button {
20 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
21 | showTransactionView.toggle()
22 | } label: {
23 | HStack {
24 | Image(systemName: "clock.circle")
25 | .foregroundColor(Color("colorBlack"))
26 | .frame(width: 30, height: 30)
27 | .background(Color("colorYellow"))
28 | .cornerRadius(7.5)
29 | Text("Transactions")
30 | .foregroundColor(Color("colorBalanceText"))
31 | Spacer()
32 | Image(systemName: "chevron.forward")
33 | .foregroundColor(Color("colorBalanceText"))
34 | .opacity(0.5)
35 | }
36 | }
37 |
38 | Button {
39 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
40 | showCategory.toggle()
41 | } label: {
42 | HStack {
43 | Image(systemName: "list.bullet.circle")
44 | .foregroundColor(Color("colorBlack"))
45 | .frame(width: 30, height: 30)
46 | .background(Color("colorBlue"))
47 | .cornerRadius(7.5)
48 | Text("Categories")
49 | .foregroundColor(Color("colorBalanceText"))
50 | Spacer()
51 | Image(systemName: "chevron.forward")
52 | .foregroundColor(Color("colorBalanceText"))
53 | .opacity(0.5)
54 | }
55 | }
56 | } header: {
57 | Text("Data")
58 | }
59 |
60 | Section(header: Text("Application")) {
61 | HStack {
62 | Text(selectedCurrency.symbol)
63 | .foregroundColor(Color("colorBlack"))
64 | .frame(width: 30, height: 30)
65 | .background(Color("colorBrown1"))
66 | .cornerRadius(7.5)
67 | Picker("Currency", selection: $selectedCurrency) {
68 | ForEach(Currency.sortedCases, id: \.self) { currency in
69 | Text(currency.rawValue)
70 | .tag(currency)
71 | }
72 | }
73 | }
74 | HStack {
75 | Image(systemName: appVM.selectedFeedbackHaptic ? "iphone.radiowaves.left.and.right.circle" : "iphone.slash.circle")
76 | .foregroundColor(Color("colorBlack"))
77 | .frame(width: 30, height: 30)
78 | .background(Color("colorPurple2"))
79 | .cornerRadius(7.5)
80 | Toggle("Vibration", isOn: $appVM.selectedFeedbackHaptic)
81 | }
82 |
83 | HStack {
84 | Text(appVM.roundingNumbers ? "0" : "0.0")
85 | .foregroundColor(Color("colorBlack"))
86 | .frame(width: 30, height: 30)
87 | .background(Color(Colors.colorGreen))
88 | .cornerRadius(7.5)
89 | Toggle("Rounding numbers", isOn: $appVM.roundingNumbers)
90 | }
91 | }
92 |
93 | Section {
94 | Button {
95 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
96 | openURL(URL(string: NSLocalizedString("https://idevnva.com/", comment: "https://idevnva.com/"))!)
97 | } label: {
98 | HStack {
99 | Image(systemName: "network")
100 | .foregroundColor(Color("colorBlack"))
101 | .frame(width: 30, height: 30)
102 | .background(Color("colorRed"))
103 | .cornerRadius(7.5)
104 | Text("Web-site")
105 | .foregroundColor(Color("colorBalanceText"))
106 | Spacer()
107 | Image(systemName: "chevron.forward")
108 | .foregroundColor(Color("colorBalanceText"))
109 | .opacity(0.5)
110 | }
111 | }
112 |
113 | Button {
114 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
115 | openURL(URL(string: NSLocalizedString("https://t.me/idevnva", comment: "https://t.me/idevnva"))!)
116 | } label: {
117 | HStack {
118 | Image(systemName: "envelope.circle")
119 | .foregroundColor(Color("colorBlack"))
120 | .frame(width: 30, height: 30)
121 | .background(Color("colorGray1"))
122 | .cornerRadius(7.5)
123 | Text("Write to the developer")
124 | .foregroundColor(Color("colorBalanceText"))
125 | Spacer()
126 | Image(systemName: "chevron.forward")
127 | .foregroundColor(Color("colorBalanceText"))
128 | .opacity(0.5)
129 | }
130 | }
131 | } header: {
132 | Text("Feedback")
133 | }
134 | }
135 | VStack {
136 | Image(systemName: "exclamationmark.shield")
137 | Text("App version: 1.1.7")
138 | }
139 | .font(.caption2).bold()
140 | .padding()
141 | }
142 | .navigationTitle("Settings")
143 | .scrollContentBackground(.hidden)
144 | .background(Color("colorBG"))
145 | .onAppear {
146 | // Устанавливаем selectedCurrency на основе сохраненного символа валюты
147 | if let currency = Currency.allCases.first(where: { $0.symbol == appVM.currencySymbol }) {
148 | selectedCurrency = currency
149 | }
150 | }
151 | .onChange(of: selectedCurrency) { newCurrency in
152 | // Сохраняем символ валюты при изменении выбора
153 | appVM.currencySymbol = newCurrency.symbol
154 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
155 | }
156 | .fullScreenCover(isPresented: $showCategory) {
157 | CategoryView()
158 | }
159 | .sheet(isPresented: $showTransactionView) {
160 | TransactionView()
161 | }
162 | }
163 | }
164 | }
165 |
166 |
167 | struct SettingView_Previews: PreviewProvider {
168 | static var previews: some View {
169 | SettingView()
170 | .environmentObject(AppViewModel())
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/iWallet/View/Transaction/AddTransactionView.swift:
--------------------------------------------------------------------------------
1 | // AddTransactionView.swift
2 |
3 | import SwiftUI
4 | import RealmSwift
5 |
6 | struct AddTransactionView: View {
7 | @EnvironmentObject var appVM: AppViewModel
8 | @EnvironmentObject var transactionVM: TransactionViewModel
9 | @Environment(\.dismiss) var dismiss
10 |
11 | @FocusState private var amountIsFocused: Bool
12 | @FocusState private var noteIsFocused: Bool
13 | @FocusState private var keyIsFocused: Bool
14 |
15 | @State var selectedCategory: Category
16 | @State var amount: Float = 0
17 | @State var date: Date = Date()
18 | @State var note: String = ""
19 | @State var selectedType: CategoryType = .expense
20 | @State var alertAmount: Bool = false
21 | @State var alertCategory: Bool = false
22 |
23 | private let formatter: NumberFormatter = {
24 | let formatter = NumberFormatter()
25 | formatter.numberStyle = .decimal
26 | formatter.maximumFractionDigits = 2
27 | formatter.zeroSymbol = ""
28 | return formatter
29 | }()
30 |
31 | var body: some View {
32 | NavigationStack {
33 | ScrollView(.vertical, showsIndicators: false) {
34 | VStack(alignment: .leading) {
35 | Section {
36 | TextField(selectedType == .expense ? "-100 \(appVM.currencySymbol)" : "+100 \(appVM.currencySymbol)", value: $amount, formatter: formatter)
37 | .font(.title3)
38 | .keyboardType(appVM.roundingNumbers ? .numberPad : .decimalPad)
39 | .padding()
40 | .frame(maxWidth: .infinity, maxHeight: .infinity)
41 | .background(Color("colorBalanceBG"))
42 | .cornerRadius(10)
43 | .padding(.bottom, 15)
44 | .focused($amountIsFocused)
45 | } header: {
46 | Text("Enter amount:")
47 | .font(.caption).textCase(.uppercase)
48 | .padding(.leading, 10)
49 | }
50 | .onTapGesture {
51 | amountIsFocused.toggle()
52 | }
53 |
54 | Section {
55 | TextField("Note", text: $note)
56 | .padding()
57 | .frame(maxWidth: .infinity, maxHeight: .infinity)
58 | .background(Color("colorBalanceBG"))
59 | .cornerRadius(10)
60 | .focused($noteIsFocused)
61 |
62 | ScrollView(.horizontal, showsIndicators: false) {
63 | HStack {
64 | let filterTransaction = transactionVM.filterTransactionsNote(category: selectedCategory, transactions: transactionVM.transactions)
65 | ForEach(filterTransaction.reversed(), id: \.self) { notes in
66 | Button {
67 | note = notes.note
68 | } label: {
69 | Text(String(notes.note.prefix(20)))
70 | .font(Font.caption)
71 | .foregroundColor(Color(.systemGray2))
72 | .padding(.vertical, 5)
73 | .padding(.horizontal, 10)
74 | .background(
75 | RoundedRectangle(cornerRadius: 7, style: .continuous)
76 | .strokeBorder(Color(.systemGray2))
77 | )
78 | .padding(.bottom, 10)
79 | }
80 | }
81 | }
82 | .padding(.horizontal, 10)
83 | }
84 | } header: {
85 | Text("Enter note:")
86 | .font(.caption).textCase(.uppercase)
87 | .padding(.leading, 10)
88 | }
89 | .onTapGesture {
90 | noteIsFocused.toggle()
91 | }
92 |
93 | Section {
94 | Picker("Category type", selection: $selectedType) {
95 | ForEach(CategoryType.allCases, id: \.self) { type in
96 | Text(type.localizedName())
97 | }
98 | }
99 | .pickerStyle(SegmentedPickerStyle())
100 | .padding(10)
101 | .frame(maxWidth: .infinity, maxHeight: .infinity)
102 | .background(Color("colorBalanceBG"))
103 | .cornerRadius(10)
104 | .onChange(of: selectedType) { _ in
105 | withAnimation() {
106 | selectedCategory = Category()
107 | }
108 |
109 | }
110 |
111 | HStack {
112 | NavigationLink(destination: PickerCategoryView(selected: $selectedCategory, selectedType: selectedType), label: {
113 | if selectedCategory.name.isEmpty {
114 | HStack {
115 | Spacer()
116 | Text("?")
117 | .font(.system(size: 15))
118 | .frame(width: 30, height: 30)
119 | .background {
120 | RoundedRectangle(cornerRadius: 10, style: .circular)
121 | .strokeBorder(Color(Colors.mainText))
122 | }
123 |
124 | Text("Select a category")
125 | Spacer()
126 | }
127 | .font(Font.subheadline)
128 | .padding(10)
129 | .frame(maxWidth: .infinity)
130 | .background(Color(Colors.colorPickerBG))
131 | .cornerRadius(10)
132 | } else {
133 | HStack {
134 | Text("Category:")
135 | .font(.headline)
136 | Spacer()
137 | Image(systemName: selectedCategory.icon)
138 | .font(.system(size: 15))
139 | .foregroundColor(.black)
140 | .frame(width: 30, height: 30)
141 | .background(Color(selectedCategory.color))
142 | .cornerRadius(7.5)
143 | Text(selectedCategory.name)
144 | .font(.headline)
145 | .fontWeight(.light)
146 | }
147 | .padding(5)
148 | }
149 | })
150 | .foregroundColor(Color(Colors.mainText))
151 | .padding(10)
152 | .background(Color(Colors.colorBalanceBG))
153 | .cornerRadius(10)
154 | }
155 | } header: {
156 | Text("Purpose:")
157 | .font(.caption).textCase(.uppercase)
158 | .padding(.leading, 10)
159 | }
160 |
161 | Section {
162 | HStack {
163 | DatePicker("Date", selection: $date, displayedComponents: .date)
164 | }
165 | .pickerStyle(SegmentedPickerStyle())
166 | .padding()
167 | .frame(maxWidth: .infinity, maxHeight: .infinity)
168 | .background(Color("colorBalanceBG"))
169 | .cornerRadius(10)
170 | } header: {
171 | Text("Enter date:")
172 | .font(.caption).textCase(.uppercase)
173 | .padding(.leading, 10)
174 | .padding(.top, 10)
175 | }
176 | }
177 | .padding(.horizontal, 15)
178 | .padding(.top, 20)
179 |
180 | }
181 | .scrollDismissesKeyboard(.immediately)
182 | .background(Color("colorBG"))
183 | .navigationBarTitle("Addendum", displayMode: .inline)
184 | .toolbar {
185 | ToolbarItem(placement: .navigationBarLeading) {
186 | Button {
187 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
188 | dismiss()
189 | } label: {
190 | Text("Cancel")
191 | }
192 | }
193 | ToolbarItem(placement: .navigationBarTrailing) {
194 | Button {
195 | if amount == 0 {
196 | alertAmount = true
197 | } else if selectedCategory.name == "" {
198 | alertCategory = true
199 | } else if selectedType != selectedCategory.type {
200 | alertCategory = true
201 | selectedCategory = Category()
202 | } else {
203 | playFeedbackHaptic(appVM.selectedFeedbackHaptic)
204 | transactionVM.saveTransaction(amount: amount, date: date, note: note, type: selectedType, category: selectedCategory)
205 | dismiss()
206 | }
207 | } label: {
208 | Text("Add")
209 | }
210 | .alert("Please select a category", isPresented: $alertCategory) {
211 | Button("Okay", role: .cancel) { }
212 | }
213 | .alert("Please enter amount", isPresented: $alertAmount) {
214 | Button("Okay", role: .cancel) { }
215 | }
216 | }
217 | }
218 | }
219 | }
220 | }
221 |
222 | struct AddTransactionView_Previews: PreviewProvider {
223 | static var previews: some View {
224 | AddTransactionView(selectedCategory: Category())
225 | .environmentObject(AppViewModel())
226 | .environmentObject(TransactionViewModel())
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/iWallet.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 0812FA482A39085A00764C0E /* EditTransactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0812FA472A39085A00764C0E /* EditTransactionView.swift */; };
11 | 08558B8629D35EA40047F959 /* iWalletApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08558B8529D35EA40047F959 /* iWalletApp.swift */; };
12 | 08558B8829D35EA40047F959 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08558B8729D35EA40047F959 /* HomeView.swift */; };
13 | 08558B8A29D35EA40047F959 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 08558B8929D35EA40047F959 /* Assets.xcassets */; };
14 | 08558B8D29D35EA40047F959 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 08558B8C29D35EA40047F959 /* Preview Assets.xcassets */; };
15 | 08558B9829D35F890047F959 /* BalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08558B9729D35F890047F959 /* BalanceView.swift */; };
16 | 08558B9A29D36D6D0047F959 /* SettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08558B9929D36D6D0047F959 /* SettingView.swift */; };
17 | 08558B9C29D37DDC0047F959 /* CategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08558B9B29D37DDC0047F959 /* CategoryView.swift */; };
18 | 08558B9E29D380920047F959 /* AddCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08558B9D29D380920047F959 /* AddCategoryView.swift */; };
19 | 08558BA029D381F10047F959 /* ColorPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08558B9F29D381F10047F959 /* ColorPickerView.swift */; };
20 | 08558BA229D388080047F959 /* IconPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08558BA129D388080047F959 /* IconPickerView.swift */; };
21 | 08558BA629D394950047F959 /* AppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08558BA529D394950047F959 /* AppViewModel.swift */; };
22 | 08558BA929D394F40047F959 /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 08558BA829D394F40047F959 /* Realm */; };
23 | 08558BAB29D394F40047F959 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 08558BAA29D394F40047F959 /* RealmSwift */; };
24 | 086459892A201BB0006C28AB /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086459882A201BB0006C28AB /* Colors.swift */; };
25 | 0864598E2A20269C006C28AB /* CategoryItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0864598D2A20269C006C28AB /* CategoryItemView.swift */; };
26 | 08645FDC2A1E80460059BDCB /* CategoryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08645FDB2A1E80460059BDCB /* CategoryModel.swift */; };
27 | 08645FDE2A1E808E0059BDCB /* CategoryTypeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08645FDD2A1E808E0059BDCB /* CategoryTypeModel.swift */; };
28 | 08645FE02A1E80CF0059BDCB /* TransactionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08645FDF2A1E80CF0059BDCB /* TransactionModel.swift */; };
29 | 08645FE32A1E817D0059BDCB /* ExtensionFloat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08645FE22A1E817D0059BDCB /* ExtensionFloat.swift */; };
30 | 08645FE52A1E81E60059BDCB /* CurrencyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08645FE42A1E81E60059BDCB /* CurrencyModel.swift */; };
31 | 08645FE72A1E824B0059BDCB /* DefaultCategoriesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08645FE62A1E824B0059BDCB /* DefaultCategoriesModel.swift */; };
32 | 08645FE92A1E835D0059BDCB /* PlayFeedbackHaptic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08645FE82A1E835D0059BDCB /* PlayFeedbackHaptic.swift */; };
33 | 08645FEE2A1EA7280059BDCB /* CategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08645FED2A1EA7280059BDCB /* CategoryViewModel.swift */; };
34 | 08645FF02A1FBD030059BDCB /* TransactionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08645FEF2A1FBD030059BDCB /* TransactionViewModel.swift */; };
35 | 086A5A2029E34B0000D044F7 /* PreviewCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086A5A1F29E34B0000D044F7 /* PreviewCardView.swift */; };
36 | 0880C66D29E8682B00E088DB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0880C66F29E8682B00E088DB /* Localizable.strings */; };
37 | 0880C67529E8AB9200E088DB /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0880C67429E8AB9200E088DB /* WelcomeView.swift */; };
38 | 08A2C2752A24C77400B8FA6D /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A2C2742A24C77400B8FA6D /* Icons.swift */; };
39 | 08AE95FF29E3236600F8AFA7 /* TransactionCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AE95FE29E3236600F8AFA7 /* TransactionCategoryView.swift */; };
40 | 08D38C5029D465640078C6C4 /* AddTransactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D38C4F29D465640078C6C4 /* AddTransactionView.swift */; };
41 | 08D38C5429D48FE00078C6C4 /* TransactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D38C5329D48FE00078C6C4 /* TransactionView.swift */; };
42 | 08F15F1A2A2A95890009FC0A /* PickerCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F15F192A2A95890009FC0A /* PickerCategoryView.swift */; };
43 | /* End PBXBuildFile section */
44 |
45 | /* Begin PBXFileReference section */
46 | 0812FA472A39085A00764C0E /* EditTransactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTransactionView.swift; sourceTree = ""; };
47 | 08558B8229D35EA40047F959 /* iWallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iWallet.app; sourceTree = BUILT_PRODUCTS_DIR; };
48 | 08558B8529D35EA40047F959 /* iWalletApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iWalletApp.swift; sourceTree = ""; };
49 | 08558B8729D35EA40047F959 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; };
50 | 08558B8929D35EA40047F959 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
51 | 08558B8C29D35EA40047F959 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
52 | 08558B9729D35F890047F959 /* BalanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceView.swift; sourceTree = ""; };
53 | 08558B9929D36D6D0047F959 /* SettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingView.swift; sourceTree = ""; };
54 | 08558B9B29D37DDC0047F959 /* CategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryView.swift; sourceTree = ""; };
55 | 08558B9D29D380920047F959 /* AddCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCategoryView.swift; sourceTree = ""; };
56 | 08558B9F29D381F10047F959 /* ColorPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerView.swift; sourceTree = ""; };
57 | 08558BA129D388080047F959 /* IconPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconPickerView.swift; sourceTree = ""; };
58 | 08558BA529D394950047F959 /* AppViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewModel.swift; sourceTree = ""; };
59 | 086459882A201BB0006C28AB /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; };
60 | 0864598D2A20269C006C28AB /* CategoryItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryItemView.swift; sourceTree = ""; };
61 | 08645FDB2A1E80460059BDCB /* CategoryModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryModel.swift; sourceTree = ""; };
62 | 08645FDD2A1E808E0059BDCB /* CategoryTypeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryTypeModel.swift; sourceTree = ""; };
63 | 08645FDF2A1E80CF0059BDCB /* TransactionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionModel.swift; sourceTree = ""; };
64 | 08645FE22A1E817D0059BDCB /* ExtensionFloat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionFloat.swift; sourceTree = ""; };
65 | 08645FE42A1E81E60059BDCB /* CurrencyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyModel.swift; sourceTree = ""; };
66 | 08645FE62A1E824B0059BDCB /* DefaultCategoriesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultCategoriesModel.swift; sourceTree = ""; };
67 | 08645FE82A1E835D0059BDCB /* PlayFeedbackHaptic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayFeedbackHaptic.swift; sourceTree = ""; };
68 | 08645FED2A1EA7280059BDCB /* CategoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryViewModel.swift; sourceTree = ""; };
69 | 08645FEF2A1FBD030059BDCB /* TransactionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionViewModel.swift; sourceTree = ""; };
70 | 086A5A1F29E34B0000D044F7 /* PreviewCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewCardView.swift; sourceTree = ""; };
71 | 0880C66E29E8682B00E088DB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
72 | 0880C67129E86A6500E088DB /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; };
73 | 0880C67429E8AB9200E088DB /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; };
74 | 08A2C2742A24C77400B8FA6D /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = ""; };
75 | 08AE95FE29E3236600F8AFA7 /* TransactionCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionCategoryView.swift; sourceTree = ""; };
76 | 08D38C4F29D465640078C6C4 /* AddTransactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTransactionView.swift; sourceTree = ""; };
77 | 08D38C5329D48FE00078C6C4 /* TransactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionView.swift; sourceTree = ""; };
78 | 08F15F192A2A95890009FC0A /* PickerCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerCategoryView.swift; sourceTree = ""; };
79 | /* End PBXFileReference section */
80 |
81 | /* Begin PBXFrameworksBuildPhase section */
82 | 08558B7F29D35EA40047F959 /* Frameworks */ = {
83 | isa = PBXFrameworksBuildPhase;
84 | buildActionMask = 2147483647;
85 | files = (
86 | 08558BAB29D394F40047F959 /* RealmSwift in Frameworks */,
87 | 08558BA929D394F40047F959 /* Realm in Frameworks */,
88 | );
89 | runOnlyForDeploymentPostprocessing = 0;
90 | };
91 | /* End PBXFrameworksBuildPhase section */
92 |
93 | /* Begin PBXGroup section */
94 | 08558B7929D35EA40047F959 = {
95 | isa = PBXGroup;
96 | children = (
97 | 08558B8429D35EA40047F959 /* iWallet */,
98 | 08558B8329D35EA40047F959 /* Products */,
99 | );
100 | sourceTree = "";
101 | };
102 | 08558B8329D35EA40047F959 /* Products */ = {
103 | isa = PBXGroup;
104 | children = (
105 | 08558B8229D35EA40047F959 /* iWallet.app */,
106 | );
107 | name = Products;
108 | sourceTree = "";
109 | };
110 | 08558B8429D35EA40047F959 /* iWallet */ = {
111 | isa = PBXGroup;
112 | children = (
113 | 08645FEC2A1E84AF0059BDCB /* App */,
114 | 08558B9529D35EC70047F959 /* View */,
115 | 08D1C19D29E0A5E700B58CDE /* ViewModel */,
116 | 08558B9429D35EC00047F959 /* Model */,
117 | 0864598A2A201FCE006C28AB /* Resources */,
118 | 08645FE12A1E81730059BDCB /* Extension */,
119 | 08645FEA2A1E83C90059BDCB /* Other */,
120 | 08558B9329D35EB90047F959 /* Localizable */,
121 | 08558B8929D35EA40047F959 /* Assets.xcassets */,
122 | 08558B8B29D35EA40047F959 /* Preview Content */,
123 | );
124 | path = iWallet;
125 | sourceTree = "";
126 | };
127 | 08558B8B29D35EA40047F959 /* Preview Content */ = {
128 | isa = PBXGroup;
129 | children = (
130 | 08558B8C29D35EA40047F959 /* Preview Assets.xcassets */,
131 | );
132 | path = "Preview Content";
133 | sourceTree = "";
134 | };
135 | 08558B9329D35EB90047F959 /* Localizable */ = {
136 | isa = PBXGroup;
137 | children = (
138 | 0880C66F29E8682B00E088DB /* Localizable.strings */,
139 | );
140 | path = Localizable;
141 | sourceTree = "";
142 | };
143 | 08558B9429D35EC00047F959 /* Model */ = {
144 | isa = PBXGroup;
145 | children = (
146 | 08645FDB2A1E80460059BDCB /* CategoryModel.swift */,
147 | 08645FDF2A1E80CF0059BDCB /* TransactionModel.swift */,
148 | 08645FDD2A1E808E0059BDCB /* CategoryTypeModel.swift */,
149 | 08645FE42A1E81E60059BDCB /* CurrencyModel.swift */,
150 | 08645FE62A1E824B0059BDCB /* DefaultCategoriesModel.swift */,
151 | );
152 | path = Model;
153 | sourceTree = "";
154 | };
155 | 08558B9529D35EC70047F959 /* View */ = {
156 | isa = PBXGroup;
157 | children = (
158 | 08645FEB2A1E84510059BDCB /* Welcome */,
159 | 08D1C19B29E0A3C000B58CDE /* Home */,
160 | 08D1C19A29E0A39200B58CDE /* Transaction */,
161 | 08D1C19929E0A38900B58CDE /* Category */,
162 | 08D1C19C29E0A3C800B58CDE /* Setting */,
163 | 08558B9629D35ED50047F959 /* Templates */,
164 | );
165 | path = View;
166 | sourceTree = "";
167 | };
168 | 08558B9629D35ED50047F959 /* Templates */ = {
169 | isa = PBXGroup;
170 | children = (
171 | 08558B9729D35F890047F959 /* BalanceView.swift */,
172 | 08558B9F29D381F10047F959 /* ColorPickerView.swift */,
173 | 08558BA129D388080047F959 /* IconPickerView.swift */,
174 | 086A5A1F29E34B0000D044F7 /* PreviewCardView.swift */,
175 | );
176 | path = Templates;
177 | sourceTree = "";
178 | };
179 | 0864598A2A201FCE006C28AB /* Resources */ = {
180 | isa = PBXGroup;
181 | children = (
182 | 086459882A201BB0006C28AB /* Colors.swift */,
183 | 08A2C2742A24C77400B8FA6D /* Icons.swift */,
184 | );
185 | path = Resources;
186 | sourceTree = "";
187 | };
188 | 08645FE12A1E81730059BDCB /* Extension */ = {
189 | isa = PBXGroup;
190 | children = (
191 | 08645FE22A1E817D0059BDCB /* ExtensionFloat.swift */,
192 | );
193 | path = Extension;
194 | sourceTree = "";
195 | };
196 | 08645FEA2A1E83C90059BDCB /* Other */ = {
197 | isa = PBXGroup;
198 | children = (
199 | 08645FE82A1E835D0059BDCB /* PlayFeedbackHaptic.swift */,
200 | );
201 | path = Other;
202 | sourceTree = "";
203 | };
204 | 08645FEB2A1E84510059BDCB /* Welcome */ = {
205 | isa = PBXGroup;
206 | children = (
207 | 0880C67429E8AB9200E088DB /* WelcomeView.swift */,
208 | );
209 | path = Welcome;
210 | sourceTree = "";
211 | };
212 | 08645FEC2A1E84AF0059BDCB /* App */ = {
213 | isa = PBXGroup;
214 | children = (
215 | 08558B8529D35EA40047F959 /* iWalletApp.swift */,
216 | );
217 | path = App;
218 | sourceTree = "";
219 | };
220 | 08D1C19929E0A38900B58CDE /* Category */ = {
221 | isa = PBXGroup;
222 | children = (
223 | 08558B9B29D37DDC0047F959 /* CategoryView.swift */,
224 | 08558B9D29D380920047F959 /* AddCategoryView.swift */,
225 | 0864598D2A20269C006C28AB /* CategoryItemView.swift */,
226 | 08F15F192A2A95890009FC0A /* PickerCategoryView.swift */,
227 | );
228 | path = Category;
229 | sourceTree = "";
230 | };
231 | 08D1C19A29E0A39200B58CDE /* Transaction */ = {
232 | isa = PBXGroup;
233 | children = (
234 | 08D38C4F29D465640078C6C4 /* AddTransactionView.swift */,
235 | 08D38C5329D48FE00078C6C4 /* TransactionView.swift */,
236 | 08AE95FE29E3236600F8AFA7 /* TransactionCategoryView.swift */,
237 | 0812FA472A39085A00764C0E /* EditTransactionView.swift */,
238 | );
239 | path = Transaction;
240 | sourceTree = "";
241 | };
242 | 08D1C19B29E0A3C000B58CDE /* Home */ = {
243 | isa = PBXGroup;
244 | children = (
245 | 08558B8729D35EA40047F959 /* HomeView.swift */,
246 | );
247 | path = Home;
248 | sourceTree = "";
249 | };
250 | 08D1C19C29E0A3C800B58CDE /* Setting */ = {
251 | isa = PBXGroup;
252 | children = (
253 | 08558B9929D36D6D0047F959 /* SettingView.swift */,
254 | );
255 | path = Setting;
256 | sourceTree = "";
257 | };
258 | 08D1C19D29E0A5E700B58CDE /* ViewModel */ = {
259 | isa = PBXGroup;
260 | children = (
261 | 08558BA529D394950047F959 /* AppViewModel.swift */,
262 | 08645FED2A1EA7280059BDCB /* CategoryViewModel.swift */,
263 | 08645FEF2A1FBD030059BDCB /* TransactionViewModel.swift */,
264 | );
265 | path = ViewModel;
266 | sourceTree = "";
267 | };
268 | /* End PBXGroup section */
269 |
270 | /* Begin PBXNativeTarget section */
271 | 08558B8129D35EA40047F959 /* iWallet */ = {
272 | isa = PBXNativeTarget;
273 | buildConfigurationList = 08558B9029D35EA40047F959 /* Build configuration list for PBXNativeTarget "iWallet" */;
274 | buildPhases = (
275 | 08558B7E29D35EA40047F959 /* Sources */,
276 | 08558B7F29D35EA40047F959 /* Frameworks */,
277 | 08558B8029D35EA40047F959 /* Resources */,
278 | );
279 | buildRules = (
280 | );
281 | dependencies = (
282 | );
283 | name = iWallet;
284 | packageProductDependencies = (
285 | 08558BA829D394F40047F959 /* Realm */,
286 | 08558BAA29D394F40047F959 /* RealmSwift */,
287 | );
288 | productName = iWallet;
289 | productReference = 08558B8229D35EA40047F959 /* iWallet.app */;
290 | productType = "com.apple.product-type.application";
291 | };
292 | /* End PBXNativeTarget section */
293 |
294 | /* Begin PBXProject section */
295 | 08558B7A29D35EA40047F959 /* Project object */ = {
296 | isa = PBXProject;
297 | attributes = {
298 | BuildIndependentTargetsInParallel = 1;
299 | LastSwiftUpdateCheck = 1420;
300 | LastUpgradeCheck = 1430;
301 | TargetAttributes = {
302 | 08558B8129D35EA40047F959 = {
303 | CreatedOnToolsVersion = 14.2;
304 | };
305 | };
306 | };
307 | buildConfigurationList = 08558B7D29D35EA40047F959 /* Build configuration list for PBXProject "iWallet" */;
308 | compatibilityVersion = "Xcode 14.0";
309 | developmentRegion = en;
310 | hasScannedForEncodings = 0;
311 | knownRegions = (
312 | Base,
313 | ru,
314 | en,
315 | );
316 | mainGroup = 08558B7929D35EA40047F959;
317 | packageReferences = (
318 | 08558BA729D394F40047F959 /* XCRemoteSwiftPackageReference "realm-swift" */,
319 | );
320 | productRefGroup = 08558B8329D35EA40047F959 /* Products */;
321 | projectDirPath = "";
322 | projectRoot = "";
323 | targets = (
324 | 08558B8129D35EA40047F959 /* iWallet */,
325 | );
326 | };
327 | /* End PBXProject section */
328 |
329 | /* Begin PBXResourcesBuildPhase section */
330 | 08558B8029D35EA40047F959 /* Resources */ = {
331 | isa = PBXResourcesBuildPhase;
332 | buildActionMask = 2147483647;
333 | files = (
334 | 08558B8D29D35EA40047F959 /* Preview Assets.xcassets in Resources */,
335 | 0880C66D29E8682B00E088DB /* Localizable.strings in Resources */,
336 | 08558B8A29D35EA40047F959 /* Assets.xcassets in Resources */,
337 | );
338 | runOnlyForDeploymentPostprocessing = 0;
339 | };
340 | /* End PBXResourcesBuildPhase section */
341 |
342 | /* Begin PBXSourcesBuildPhase section */
343 | 08558B7E29D35EA40047F959 /* Sources */ = {
344 | isa = PBXSourcesBuildPhase;
345 | buildActionMask = 2147483647;
346 | files = (
347 | 08A2C2752A24C77400B8FA6D /* Icons.swift in Sources */,
348 | 08645FE02A1E80CF0059BDCB /* TransactionModel.swift in Sources */,
349 | 086459892A201BB0006C28AB /* Colors.swift in Sources */,
350 | 08558BA629D394950047F959 /* AppViewModel.swift in Sources */,
351 | 08558B9829D35F890047F959 /* BalanceView.swift in Sources */,
352 | 08645FE92A1E835D0059BDCB /* PlayFeedbackHaptic.swift in Sources */,
353 | 08645FE32A1E817D0059BDCB /* ExtensionFloat.swift in Sources */,
354 | 086A5A2029E34B0000D044F7 /* PreviewCardView.swift in Sources */,
355 | 08AE95FF29E3236600F8AFA7 /* TransactionCategoryView.swift in Sources */,
356 | 08D38C5029D465640078C6C4 /* AddTransactionView.swift in Sources */,
357 | 08558BA029D381F10047F959 /* ColorPickerView.swift in Sources */,
358 | 08F15F1A2A2A95890009FC0A /* PickerCategoryView.swift in Sources */,
359 | 08645FDC2A1E80460059BDCB /* CategoryModel.swift in Sources */,
360 | 08645FF02A1FBD030059BDCB /* TransactionViewModel.swift in Sources */,
361 | 08D38C5429D48FE00078C6C4 /* TransactionView.swift in Sources */,
362 | 08558B9A29D36D6D0047F959 /* SettingView.swift in Sources */,
363 | 0880C67529E8AB9200E088DB /* WelcomeView.swift in Sources */,
364 | 08558B8829D35EA40047F959 /* HomeView.swift in Sources */,
365 | 08645FEE2A1EA7280059BDCB /* CategoryViewModel.swift in Sources */,
366 | 08645FE72A1E824B0059BDCB /* DefaultCategoriesModel.swift in Sources */,
367 | 08645FDE2A1E808E0059BDCB /* CategoryTypeModel.swift in Sources */,
368 | 0864598E2A20269C006C28AB /* CategoryItemView.swift in Sources */,
369 | 08645FE52A1E81E60059BDCB /* CurrencyModel.swift in Sources */,
370 | 0812FA482A39085A00764C0E /* EditTransactionView.swift in Sources */,
371 | 08558B9C29D37DDC0047F959 /* CategoryView.swift in Sources */,
372 | 08558BA229D388080047F959 /* IconPickerView.swift in Sources */,
373 | 08558B8629D35EA40047F959 /* iWalletApp.swift in Sources */,
374 | 08558B9E29D380920047F959 /* AddCategoryView.swift in Sources */,
375 | );
376 | runOnlyForDeploymentPostprocessing = 0;
377 | };
378 | /* End PBXSourcesBuildPhase section */
379 |
380 | /* Begin PBXVariantGroup section */
381 | 0880C66F29E8682B00E088DB /* Localizable.strings */ = {
382 | isa = PBXVariantGroup;
383 | children = (
384 | 0880C66E29E8682B00E088DB /* en */,
385 | 0880C67129E86A6500E088DB /* ru */,
386 | );
387 | name = Localizable.strings;
388 | sourceTree = "";
389 | };
390 | /* End PBXVariantGroup section */
391 |
392 | /* Begin XCBuildConfiguration section */
393 | 08558B8E29D35EA40047F959 /* Debug */ = {
394 | isa = XCBuildConfiguration;
395 | buildSettings = {
396 | ALWAYS_SEARCH_USER_PATHS = NO;
397 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
398 | CLANG_ANALYZER_NONNULL = YES;
399 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
400 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
401 | CLANG_ENABLE_MODULES = YES;
402 | CLANG_ENABLE_OBJC_ARC = YES;
403 | CLANG_ENABLE_OBJC_WEAK = YES;
404 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
405 | CLANG_WARN_BOOL_CONVERSION = YES;
406 | CLANG_WARN_COMMA = YES;
407 | CLANG_WARN_CONSTANT_CONVERSION = YES;
408 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
409 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
410 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
411 | CLANG_WARN_EMPTY_BODY = YES;
412 | CLANG_WARN_ENUM_CONVERSION = YES;
413 | CLANG_WARN_INFINITE_RECURSION = YES;
414 | CLANG_WARN_INT_CONVERSION = YES;
415 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
416 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
417 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
418 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
419 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
420 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
421 | CLANG_WARN_STRICT_PROTOTYPES = YES;
422 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
423 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
424 | CLANG_WARN_UNREACHABLE_CODE = YES;
425 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
426 | COPY_PHASE_STRIP = NO;
427 | DEBUG_INFORMATION_FORMAT = dwarf;
428 | ENABLE_STRICT_OBJC_MSGSEND = YES;
429 | ENABLE_TESTABILITY = YES;
430 | GCC_C_LANGUAGE_STANDARD = gnu11;
431 | GCC_DYNAMIC_NO_PIC = NO;
432 | GCC_NO_COMMON_BLOCKS = YES;
433 | GCC_OPTIMIZATION_LEVEL = 0;
434 | GCC_PREPROCESSOR_DEFINITIONS = (
435 | "DEBUG=1",
436 | "$(inherited)",
437 | );
438 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
439 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
440 | GCC_WARN_UNDECLARED_SELECTOR = YES;
441 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
442 | GCC_WARN_UNUSED_FUNCTION = YES;
443 | GCC_WARN_UNUSED_VARIABLE = YES;
444 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
445 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
446 | MTL_FAST_MATH = YES;
447 | ONLY_ACTIVE_ARCH = YES;
448 | SDKROOT = iphoneos;
449 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
450 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
451 | };
452 | name = Debug;
453 | };
454 | 08558B8F29D35EA40047F959 /* Release */ = {
455 | isa = XCBuildConfiguration;
456 | buildSettings = {
457 | ALWAYS_SEARCH_USER_PATHS = NO;
458 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
459 | CLANG_ANALYZER_NONNULL = YES;
460 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
461 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
462 | CLANG_ENABLE_MODULES = YES;
463 | CLANG_ENABLE_OBJC_ARC = YES;
464 | CLANG_ENABLE_OBJC_WEAK = YES;
465 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
466 | CLANG_WARN_BOOL_CONVERSION = YES;
467 | CLANG_WARN_COMMA = YES;
468 | CLANG_WARN_CONSTANT_CONVERSION = YES;
469 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
470 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
471 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
472 | CLANG_WARN_EMPTY_BODY = YES;
473 | CLANG_WARN_ENUM_CONVERSION = YES;
474 | CLANG_WARN_INFINITE_RECURSION = YES;
475 | CLANG_WARN_INT_CONVERSION = YES;
476 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
477 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
478 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
479 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
480 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
481 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
482 | CLANG_WARN_STRICT_PROTOTYPES = YES;
483 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
484 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
485 | CLANG_WARN_UNREACHABLE_CODE = YES;
486 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
487 | COPY_PHASE_STRIP = NO;
488 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
489 | ENABLE_NS_ASSERTIONS = NO;
490 | ENABLE_STRICT_OBJC_MSGSEND = YES;
491 | GCC_C_LANGUAGE_STANDARD = gnu11;
492 | GCC_NO_COMMON_BLOCKS = YES;
493 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
494 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
495 | GCC_WARN_UNDECLARED_SELECTOR = YES;
496 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
497 | GCC_WARN_UNUSED_FUNCTION = YES;
498 | GCC_WARN_UNUSED_VARIABLE = YES;
499 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
500 | MTL_ENABLE_DEBUG_INFO = NO;
501 | MTL_FAST_MATH = YES;
502 | SDKROOT = iphoneos;
503 | SWIFT_COMPILATION_MODE = wholemodule;
504 | SWIFT_OPTIMIZATION_LEVEL = "-O";
505 | VALIDATE_PRODUCT = YES;
506 | };
507 | name = Release;
508 | };
509 | 08558B9129D35EA40047F959 /* Debug */ = {
510 | isa = XCBuildConfiguration;
511 | buildSettings = {
512 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
513 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
514 | CODE_SIGN_STYLE = Automatic;
515 | CURRENT_PROJECT_VERSION = 1;
516 | DEVELOPMENT_ASSET_PATHS = "\"iWallet/Preview Content\"";
517 | DEVELOPMENT_TEAM = 4243BL8MP3;
518 | ENABLE_PREVIEWS = YES;
519 | GENERATE_INFOPLIST_FILE = YES;
520 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
521 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
522 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
523 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
524 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
525 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
526 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
527 | LD_RUNPATH_SEARCH_PATHS = (
528 | "$(inherited)",
529 | "@executable_path/Frameworks",
530 | );
531 | MARKETING_VERSION = 1.1.8;
532 | PRODUCT_BUNDLE_IDENTIFIER = com.idevnva.iwallet;
533 | PRODUCT_NAME = "$(TARGET_NAME)";
534 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
535 | SUPPORTS_MACCATALYST = NO;
536 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
537 | SWIFT_EMIT_LOC_STRINGS = YES;
538 | SWIFT_VERSION = 5.0;
539 | TARGETED_DEVICE_FAMILY = 1;
540 | };
541 | name = Debug;
542 | };
543 | 08558B9229D35EA40047F959 /* Release */ = {
544 | isa = XCBuildConfiguration;
545 | buildSettings = {
546 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
547 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
548 | CODE_SIGN_STYLE = Automatic;
549 | CURRENT_PROJECT_VERSION = 1;
550 | DEVELOPMENT_ASSET_PATHS = "\"iWallet/Preview Content\"";
551 | DEVELOPMENT_TEAM = 4243BL8MP3;
552 | ENABLE_PREVIEWS = YES;
553 | GENERATE_INFOPLIST_FILE = YES;
554 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
555 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
556 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
557 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
558 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
559 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
560 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
561 | LD_RUNPATH_SEARCH_PATHS = (
562 | "$(inherited)",
563 | "@executable_path/Frameworks",
564 | );
565 | MARKETING_VERSION = 1.1.8;
566 | PRODUCT_BUNDLE_IDENTIFIER = com.idevnva.iwallet;
567 | PRODUCT_NAME = "$(TARGET_NAME)";
568 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
569 | SUPPORTS_MACCATALYST = NO;
570 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
571 | SWIFT_EMIT_LOC_STRINGS = YES;
572 | SWIFT_VERSION = 5.0;
573 | TARGETED_DEVICE_FAMILY = 1;
574 | };
575 | name = Release;
576 | };
577 | /* End XCBuildConfiguration section */
578 |
579 | /* Begin XCConfigurationList section */
580 | 08558B7D29D35EA40047F959 /* Build configuration list for PBXProject "iWallet" */ = {
581 | isa = XCConfigurationList;
582 | buildConfigurations = (
583 | 08558B8E29D35EA40047F959 /* Debug */,
584 | 08558B8F29D35EA40047F959 /* Release */,
585 | );
586 | defaultConfigurationIsVisible = 0;
587 | defaultConfigurationName = Release;
588 | };
589 | 08558B9029D35EA40047F959 /* Build configuration list for PBXNativeTarget "iWallet" */ = {
590 | isa = XCConfigurationList;
591 | buildConfigurations = (
592 | 08558B9129D35EA40047F959 /* Debug */,
593 | 08558B9229D35EA40047F959 /* Release */,
594 | );
595 | defaultConfigurationIsVisible = 0;
596 | defaultConfigurationName = Release;
597 | };
598 | /* End XCConfigurationList section */
599 |
600 | /* Begin XCRemoteSwiftPackageReference section */
601 | 08558BA729D394F40047F959 /* XCRemoteSwiftPackageReference "realm-swift" */ = {
602 | isa = XCRemoteSwiftPackageReference;
603 | repositoryURL = "https://github.com/realm/realm-swift.git";
604 | requirement = {
605 | branch = master;
606 | kind = branch;
607 | };
608 | };
609 | /* End XCRemoteSwiftPackageReference section */
610 |
611 | /* Begin XCSwiftPackageProductDependency section */
612 | 08558BA829D394F40047F959 /* Realm */ = {
613 | isa = XCSwiftPackageProductDependency;
614 | package = 08558BA729D394F40047F959 /* XCRemoteSwiftPackageReference "realm-swift" */;
615 | productName = Realm;
616 | };
617 | 08558BAA29D394F40047F959 /* RealmSwift */ = {
618 | isa = XCSwiftPackageProductDependency;
619 | package = 08558BA729D394F40047F959 /* XCRemoteSwiftPackageReference "realm-swift" */;
620 | productName = RealmSwift;
621 | };
622 | /* End XCSwiftPackageProductDependency section */
623 | };
624 | rootObject = 08558B7A29D35EA40047F959 /* Project object */;
625 | }
626 |
--------------------------------------------------------------------------------