Combine all top-level datastores together

This commit is contained in:
William Brawner 2022-05-17 19:04:27 -06:00
parent 2cffa9c83c
commit f62670aaec
32 changed files with 629 additions and 711 deletions

View file

@ -16,7 +16,6 @@
284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2841022F2342D97300EAFA29 /* BudgetListsView.swift */; };
284102322342E12F00EAFA29 /* CategoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284102312342E12F00EAFA29 /* CategoryListView.swift */; };
2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2857EAEC233DA30B0026BC83 /* LoadingView.swift */; };
289510242352AAFC00BC862B /* UserDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 289510232352AAFC00BC862B /* UserDataStore.swift */; };
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC94F1233C373900BFB70A /* LoginView.swift */; };
28AC94F4233C373A00BFB70A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 28AC94F3233C373A00BFB70A /* Assets.xcassets */; };
28AC94F7233C373A00BFB70A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 28AC94F6233C373A00BFB70A /* Preview Assets.xcassets */; };
@ -28,20 +27,15 @@
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC952B233C434800BFB70A /* UserRepository.swift */; };
28B9E50E2346BCB2007C3909 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28B9E50D2346BCB2007C3909 /* RegistrationView.swift */; };
28CE8B9523525F990072BC4C /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28CE8B9423525F990072BC4C /* Extensions.swift */; };
28FE6AF42342E3CB00D5543E /* BudgetsDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF32342E3CB00D5543E /* BudgetsDataStore.swift */; };
28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */; };
28FE6AFA23441E3700D5543E /* CategoryListDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF923441E3700D5543E /* CategoryListDataStore.swift */; };
28FE6AFC23441E4500D5543E /* CategoryRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AFB23441E4500D5543E /* CategoryRepository.swift */; };
28FE6B002344308600D5543E /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AFF2344308600D5543E /* Transaction.swift */; };
28FE6B022344331B00D5543E /* TransactionDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B012344331B00D5543E /* TransactionDataStore.swift */; };
28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B03234449DC00D5543E /* TransactionListView.swift */; };
28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */; };
543ECE42233E82A40018A9D9 /* AuthenticationDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */; };
8005FD5D277EAB0200E48B23 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8005FD5C277EAB0200E48B23 /* MainView.swift */; };
800DFC2C277FF47A00EDCE9B /* AsyncData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800DFC2B277FF47A00EDCE9B /* AsyncData.swift */; };
801D08CC275ECEFA00931465 /* RecurringTransactionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */; };
801D08CE275F189E00931465 /* RecurringTransactionsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */; };
801D08D0275F1AE300931465 /* RecurringTransactionDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */; };
801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */; };
802161D0277647920075761A /* AsyncObservableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 802161CF277647920075761A /* AsyncObservableObject.swift */; };
8021EFAD280A0FCA00043F18 /* TwigsCore in Frameworks */ = {isa = PBXBuildFile; productRef = 8021EFAC280A0FCA00043F18 /* TwigsCore */; };
@ -54,10 +48,12 @@
8076A84F2809FE8E006B9DC9 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A84E2809FE8E006B9DC9 /* ArgumentParser */; };
8076A8522809FE99006B9DC9 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 8076A8512809FE99006B9DC9 /* Collections */; };
80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80820144275FFD380040996E /* SidebarBudgetView.swift */; };
808CA1A728354005002EDD59 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 808CA1A628354005002EDD59 /* XCTest.framework */; };
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */; };
80A419ED2787C0A00090C515 /* TwigsCli.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A419EC2787C0A00090C515 /* TwigsCli.swift */; };
80A419F52787C1520090C515 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 80A419F42787C1520090C515 /* ArgumentParser */; };
80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */; };
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D2CE192833448500EDD6C2 /* DataStore.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -100,7 +96,6 @@
2841022F2342D97300EAFA29 /* BudgetListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetListsView.swift; sourceTree = "<group>"; };
284102312342E12F00EAFA29 /* CategoryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryListView.swift; sourceTree = "<group>"; };
2857EAEC233DA30B0026BC83 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
289510232352AAFC00BC862B /* UserDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataStore.swift; sourceTree = "<group>"; };
28AC94EA233C373900BFB70A /* Twigs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Twigs.app; sourceTree = BUILT_PRODUCTS_DIR; };
28AC94F1233C373900BFB70A /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
28AC94F3233C373A00BFB70A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -118,20 +113,15 @@
28AC952B233C434800BFB70A /* UserRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = "<group>"; };
28B9E50D2346BCB2007C3909 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = "<group>"; };
28CE8B9423525F990072BC4C /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
28FE6AF32342E3CB00D5543E /* BudgetsDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetsDataStore.swift; sourceTree = "<group>"; };
28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetRepository.swift; sourceTree = "<group>"; };
28FE6AF923441E3700D5543E /* CategoryListDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryListDataStore.swift; sourceTree = "<group>"; };
28FE6AFB23441E4500D5543E /* CategoryRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryRepository.swift; sourceTree = "<group>"; };
28FE6AFF2344308600D5543E /* Transaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = "<group>"; };
28FE6B012344331B00D5543E /* TransactionDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDataStore.swift; sourceTree = "<group>"; };
28FE6B03234449DC00D5543E /* TransactionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionListView.swift; sourceTree = "<group>"; };
28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailsView.swift; sourceTree = "<group>"; };
543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationDataStore.swift; sourceTree = "<group>"; };
8005FD5C277EAB0200E48B23 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
800DFC2B277FF47A00EDCE9B /* AsyncData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncData.swift; sourceTree = "<group>"; };
801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsListView.swift; sourceTree = "<group>"; };
801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionsRepository.swift; sourceTree = "<group>"; };
801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionDataStore.swift; sourceTree = "<group>"; };
801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecurringTransactionDetailsView.swift; sourceTree = "<group>"; };
802161CF277647920075761A /* AsyncObservableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncObservableObject.swift; sourceTree = "<group>"; };
8021EFAB280A0FA100043F18 /* TwigsCore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TwigsCore; path = ../TwigsCore; sourceTree = "<group>"; };
@ -142,11 +132,13 @@
8044BA3E27853054009A78D4 /* CategoryForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryForm.swift; sourceTree = "<group>"; };
806C784F272B700B00FA1375 /* TwigsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsApp.swift; sourceTree = "<group>"; };
80820144275FFD380040996E /* SidebarBudgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarBudgetView.swift; sourceTree = "<group>"; };
808CA1A628354005002EDD59 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
809B942227221EC800B1DAE2 /* CategoryFormSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryFormSheet.swift; sourceTree = "<group>"; };
809B94242722597800B1DAE2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
80A419EA2787C0A00090C515 /* twigs-cli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "twigs-cli"; sourceTree = BUILT_PRODUCTS_DIR; };
80A419EC2787C0A00090C515 /* TwigsCli.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwigsCli.swift; sourceTree = "<group>"; };
80D1FC13277C1EF9007F17FB /* InlineLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineLoadingView.swift; sourceTree = "<group>"; };
80D2CE192833448500EDD6C2 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -164,6 +156,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
808CA1A728354005002EDD59 /* XCTest.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -197,7 +190,6 @@
isa = PBXGroup;
children = (
2841022F2342D97300EAFA29 /* BudgetListsView.swift */,
28FE6AF32342E3CB00D5543E /* BudgetsDataStore.swift */,
28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */,
282126BA235CDD3C00072D52 /* BudgetDetailsView.swift */,
);
@ -207,7 +199,6 @@
2841022A2342D8CB00EAFA29 /* Category */ = {
isa = PBXGroup;
children = (
28FE6AF923441E3700D5543E /* CategoryListDataStore.swift */,
8043EB83271F26ED00498E73 /* CategoryDetailsView.swift */,
284102312342E12F00EAFA29 /* CategoryListView.swift */,
28FE6AFB23441E4500D5543E /* CategoryRepository.swift */,
@ -278,6 +269,7 @@
2857EAEB233DA2F90026BC83 /* Views */,
802161CF277647920075761A /* AsyncObservableObject.swift */,
800DFC2B277FF47A00EDCE9B /* AsyncData.swift */,
80D2CE192833448500EDD6C2 /* DataStore.swift */,
);
path = Twigs;
sourceTree = "<group>";
@ -313,7 +305,6 @@
children = (
28AC9528233C433400BFB70A /* TransactionRepository.swift */,
28FE6AFF2344308600D5543E /* Transaction.swift */,
28FE6B012344331B00D5543E /* TransactionDataStore.swift */,
28FE6B03234449DC00D5543E /* TransactionListView.swift */,
28FE6B0523444A9800D5543E /* TransactionDetailsView.swift */,
2821265F23555FD300072D52 /* TransactionFormSheet.swift */,
@ -335,8 +326,6 @@
isa = PBXGroup;
children = (
28AC952B233C434800BFB70A /* UserRepository.swift */,
543ECE41233E82A40018A9D9 /* AuthenticationDataStore.swift */,
289510232352AAFC00BC862B /* UserDataStore.swift */,
);
path = User;
sourceTree = "<group>";
@ -344,6 +333,7 @@
8005FD59277E623900E48B23 /* Frameworks */ = {
isa = PBXGroup;
children = (
808CA1A628354005002EDD59 /* XCTest.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -361,7 +351,6 @@
children = (
801D08CB275ECEFA00931465 /* RecurringTransactionsListView.swift */,
801D08CD275F189E00931465 /* RecurringTransactionsRepository.swift */,
801D08CF275F1AE300931465 /* RecurringTransactionDataStore.swift */,
801D08D1275FB7DE00931465 /* RecurringTransactionDetailsView.swift */,
);
path = "Recurring Transactions";
@ -552,31 +541,26 @@
801D08D2275FB7DE00931465 /* RecurringTransactionDetailsView.swift in Sources */,
28FE6B0623444A9800D5543E /* TransactionDetailsView.swift in Sources */,
80D1FC14277C1EF9007F17FB /* InlineLoadingView.swift in Sources */,
28FE6AF42342E3CB00D5543E /* BudgetsDataStore.swift in Sources */,
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */,
28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */,
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */,
802161D0277647920075761A /* AsyncObservableObject.swift in Sources */,
282126BB235CDD3C00072D52 /* BudgetDetailsView.swift in Sources */,
801D08D0275F1AE300931465 /* RecurringTransactionDataStore.swift in Sources */,
80D2CE1A2833448500EDD6C2 /* DataStore.swift in Sources */,
2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */,
282126A3235ABC1800072D52 /* TwigsInMemoryCacheService.swift in Sources */,
800DFC2C277FF47A00EDCE9B /* AsyncData.swift in Sources */,
8044BA3B2784B659009A78D4 /* TransactionDetails.swift in Sources */,
28FE6AFA23441E3700D5543E /* CategoryListDataStore.swift in Sources */,
28B9E50E2346BCB2007C3909 /* RegistrationView.swift in Sources */,
284102322342E12F00EAFA29 /* CategoryListView.swift in Sources */,
8043EB84271F26ED00498E73 /* CategoryDetailsView.swift in Sources */,
289510242352AAFC00BC862B /* UserDataStore.swift in Sources */,
28FE6B002344308600D5543E /* Transaction.swift in Sources */,
8005FD5D277EAB0200E48B23 /* MainView.swift in Sources */,
282126A1235929B800072D52 /* ProfileView.swift in Sources */,
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */,
809B942327221EC800B1DAE2 /* CategoryFormSheet.swift in Sources */,
28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */,
28FE6B022344331B00D5543E /* TransactionDataStore.swift in Sources */,
8044BA3F27853054009A78D4 /* CategoryForm.swift in Sources */,
543ECE42233E82A40018A9D9 /* AuthenticationDataStore.swift in Sources */,
80820145275FFD380040996E /* SidebarBudgetView.swift in Sources */,
8044BA3927828E9D009A78D4 /* CategoryDataStore.swift in Sources */,
284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */,

View file

@ -10,14 +10,14 @@ import SwiftUI
import TwigsCore
struct BudgetDetailsView: View {
@EnvironmentObject var budgetDataStore: BudgetsDataStore
@EnvironmentObject var dataStore: DataStore
let budget: Budget
@ViewBuilder
var body: some View {
InlineLoadingView(
data: self.$budgetDataStore.overview,
action: { await self.budgetDataStore.loadOverview(self.budget) },
data: self.$dataStore.overview,
action: { await self.dataStore.loadOverview(self.budget) },
errorTextLocalizedStringKey: "budgets_load_failure"
) { overview in
List {
@ -96,7 +96,7 @@ struct ExpensesOverview: View {
struct BudgetDetailsView_Previews: PreviewProvider {
static var previews: some View {
BudgetDetailsView(budget: MockBudgetRepository.budget)
.environmentObject(BudgetsDataStore(budgetRepository: MockBudgetRepository(), categoryRepository: MockCategoryRepository(), transactionRepository: MockTransactionRepository()))
.environmentObject(TwigsInMemoryCacheService())
}
}
#endif

View file

@ -12,12 +12,12 @@ import Combine
import TwigsCore
struct BudgetListsView: View {
@EnvironmentObject var budgetDataStore: BudgetsDataStore
@EnvironmentObject var dataStore: DataStore
var body: some View {
InlineLoadingView(
data: $budgetDataStore.budgets,
action: { await self.budgetDataStore.getBudgets(count: nil, page: nil) },
data: $dataStore.budgets,
action: { await self.dataStore.getBudgets(count: nil, page: nil) },
errorTextLocalizedStringKey: "budgets_load_failure"
) { (budgets: [Budget]) in
Section("budgets") {
@ -30,13 +30,13 @@ struct BudgetListsView: View {
}
struct BudgetListItemView: View {
@EnvironmentObject var budgetDataStore: BudgetsDataStore
@EnvironmentObject var dataStore: DataStore
let budget: Budget
var body: some View {
Button(
action: {
self.budgetDataStore.selectBudget(budget)
self.dataStore.selectBudget(budget)
},
label: {
VStack(alignment: .leading) {

View file

@ -1,99 +0,0 @@
//
// BudgetsDataStore.swift
// Budget
//
// Created by Billy Brawner on 9/30/19.
// Copyright © 2019 William Brawner. All rights reserved.
//
import Foundation
import Combine
import TwigsCore
private let LAST_BUDGET = "LAST_BUDGET"
@MainActor
class BudgetsDataStore: ObservableObject {
private let budgetRepository: BudgetRepository
private let categoryRepository: CategoryRepository
private let transactionRepository: TransactionRepository
@Published var budgets: AsyncData<[Budget]> = .empty
@Published var budget: AsyncData<Budget> = .empty {
didSet {
self.overview = .empty
if case let .success(budget) = self.budget {
UserDefaults.standard.set(budget.id, forKey: LAST_BUDGET)
self.showBudgetSelection = false
Task {
await loadOverview(budget)
}
}
}
}
@Published var overview: AsyncData<BudgetOverview> = .empty
@Published var showBudgetSelection: Bool = true
init(budgetRepository: BudgetRepository, categoryRepository: CategoryRepository, transactionRepository: TransactionRepository) {
self.budgetRepository = budgetRepository
self.categoryRepository = categoryRepository
self.transactionRepository = transactionRepository
}
func getBudgets(count: Int? = nil, page: Int? = nil) async {
// TODO: Find some way to extract this to a generic function
self.budgets = .loading
do {
let budgets = try await self.budgetRepository.getBudgets(count: count, page: page).sorted(by: { $0.name < $1.name })
self.budgets = .success(budgets)
if self.budget != .empty {
return
}
if let id = UserDefaults.standard.string(forKey: LAST_BUDGET), let lastBudget = budgets.first(where: { $0.id == id }) {
self.budget = .success(lastBudget)
} else {
if let budget = budgets.first {
self.budget = .success(budget)
}
}
} catch {
self.budgets = .error(error)
}
}
func loadOverview(_ budget: Budget) async {
self.overview = .loading
do {
let budgetBalance = try await self.transactionRepository.sumTransactions(budgetId: budget.id, categoryId: nil, from: nil, to: nil)
let categories = try await self.categoryRepository.getCategories(budgetId: budget.id, expense: nil, archived: false, count: nil, page: nil)
var budgetOverview = BudgetOverview(budget: budget, balance: budgetBalance.balance)
try await withThrowingTaskGroup(of: (TwigsCore.Category, BalanceResponse).self) { group in
for category in categories {
group.addTask {
return (category, try await self.transactionRepository.sumTransactions(budgetId: nil, categoryId: category.id, from: nil, to: nil))
}
}
for try await (category, response) in group {
if category.expense {
budgetOverview.expectedExpenses += category.amount
} else {
budgetOverview.expectedIncome += category.amount
}
if category.expense {
budgetOverview.actualExpenses += abs(response.balance)
} else {
budgetOverview.actualIncome += response.balance
}
}
}
self.overview = .success(budgetOverview)
} catch {
self.overview = .error(error)
}
}
func selectBudget(_ budget: Budget) {
self.budget = .success(budget)
}
}

View file

@ -12,16 +12,16 @@ import TwigsCore
@MainActor
class CategoryDataStore: ObservableObject {
@Published var sum: AsyncData<Int> = .empty
let transactionRepository: TransactionRepository
let apiService: TwigsApiService
init(transactionRepository: TransactionRepository) {
self.transactionRepository = transactionRepository
init(_ apiService: TwigsApiService) {
self.apiService = apiService
}
func sum(categoryId: String, from: Date? = nil, to: Date? = nil) async {
self.sum = .loading
do {
let sum = try await self.transactionRepository.sumTransactions(budgetId: nil, categoryId: categoryId, from: from, to: to).balance
let sum = try await self.apiService.sumTransactions(budgetId: nil, categoryId: categoryId, from: from, to: to).balance
self.sum = .success(sum)
} catch {
self.sum = .error(error)

View file

@ -10,9 +10,8 @@ import SwiftUI
import TwigsCore
struct CategoryDetailsView: View {
@EnvironmentObject var categoryListDataStore: CategoryListDataStore
@EnvironmentObject var dataStore: DataStore
@EnvironmentObject var categoryDataStore: CategoryDataStore
@EnvironmentObject var transactionDataStore: TransactionDataStore
@EnvironmentObject var apiService: TwigsApiService
let budget: Budget
@State var sum: Int? = 0
@ -35,8 +34,8 @@ struct CategoryDetailsView: View {
}
var body: some View {
if let category = categoryListDataStore.selectedCategory {
TransactionListView(apiService: apiService, budget: budget, category: category) {
if let category = dataStore.selectedCategory {
TransactionListView() {
VStack {
Text(verbatim: category.description ?? "")
.padding()
@ -59,7 +58,7 @@ struct CategoryDetailsView: View {
}, content: {
CategoryFormSheet(categoryForm: CategoryForm(
category: category,
categoryList: categoryListDataStore,
dataStore: dataStore,
budgetId: category.budgetId
))
})
@ -86,7 +85,7 @@ struct LabeledCounter: View {
struct CategoryDetailsView_Previews: PreviewProvider {
static var previews: some View {
CategoryDetailsView(MockBudgetRepository.budget)
.environmentObject(TransactionDataStore(MockTransactionRepository()))
.environmentObject(TwigsInMemoryCacheService())
}
}
#endif

View file

@ -11,7 +11,7 @@ import TwigsCore
class CategoryForm: ObservableObject {
let category: TwigsCore.Category?
let categoryList: CategoryListDataStore
let dataStore: DataStore
let budgetId: String
let categoryId: String
@Published var title: String
@ -23,11 +23,11 @@ class CategoryForm: ObservableObject {
init(
category: TwigsCore.Category?,
categoryList: CategoryListDataStore,
dataStore: DataStore,
budgetId: String
) {
self.category = category
self.categoryList = categoryList
self.dataStore = dataStore
self.budgetId = budgetId
self.categoryId = category?.id ?? ""
self.showDelete = !self.categoryId.isEmpty
@ -42,7 +42,7 @@ class CategoryForm: ObservableObject {
func save() async {
let amount = Int((Double(self.amount) ?? 0.0) * 100)
await categoryList.save(TwigsCore.Category(
await dataStore.save(TwigsCore.Category(
budgetId: budgetId,
id: categoryId,
title: title,
@ -57,6 +57,6 @@ class CategoryForm: ObservableObject {
guard let category = self.category else {
return
}
await categoryList.delete(category)
await dataStore.delete(category)
}
}

View file

@ -10,13 +10,13 @@ import SwiftUI
import TwigsCore
struct CategoryFormSheet: View {
@EnvironmentObject var categoryList: CategoryListDataStore
@EnvironmentObject var dataStore: DataStore
@ObservedObject var categoryForm: CategoryForm
@State private var showingAlert = false
@ViewBuilder
var stateContent: some View {
switch categoryList.category {
switch dataStore.category {
case .success(_):
EmptyView()
case .saving(_):
@ -62,7 +62,7 @@ struct CategoryFormSheet: View {
stateContent
.navigationBarItems(
leading: Button("cancel") {
categoryList.cancelEdit()
dataStore.cancelEditCategory()
},
trailing: Button("save") {
Task {

View file

@ -1,90 +0,0 @@
//
// CategoryListDataStore.swift
// Budget
//
// Created by Billy Brawner on 10/1/19.
// Copyright © 2019 William Brawner. All rights reserved.
//
import Foundation
import Combine
import TwigsCore
@MainActor
class CategoryListDataStore: ObservableObject {
@Published var categories: AsyncData<[TwigsCore.Category]> = .empty
@Published var category: AsyncData<TwigsCore.Category> = .empty {
didSet {
if case let .success(category) = self.category {
self.selectedCategory = category
} else if case .empty = self.category {
self.selectedCategory = nil
}
}
}
@Published var selectedCategory: TwigsCore.Category? = nil
func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = false, count: Int? = nil, page: Int? = nil) async {
self.categories = .loading
do {
let categories = try await categoryRepository.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page)
self.categories = .success(categories)
} catch {
self.categories = .error(error)
}
}
func save(_ category: TwigsCore.Category) async {
self.category = .loading
do {
var savedCategory: TwigsCore.Category
if category.id != "" {
savedCategory = try await self.categoryRepository.updateCategory(category)
} else {
savedCategory = try await self.categoryRepository.createCategory(category)
}
self.category = .success(savedCategory)
if case let .success(categories) = self.categories {
var updatedCategories = categories.filter(withoutId: category.id)
updatedCategories.append(savedCategory)
self.categories = .success(updatedCategories.sorted(by: { $0.title < $1.title }))
}
} catch {
self.category = .error(error, category)
}
}
func delete(_ category: TwigsCore.Category) async {
self.category = .loading
do {
try await self.categoryRepository.deleteCategory(category.id)
self.category = .empty
if case let .success(categories) = self.categories {
self.categories = .success(categories.filter(withoutId: category.id))
}
} catch {
self.category = .error(error, category)
}
}
func edit(_ category: TwigsCore.Category) async {
self.category = .editing(category)
}
func cancelEdit() {
if let category = self.selectedCategory {
self.category = .success(category)
} else {
self.category = .empty
}
}
func clearSelectedCategory() {
self.category = .empty
}
private let categoryRepository: CategoryRepository
init(_ categoryRepository: CategoryRepository) {
self.categoryRepository = categoryRepository
}
}

View file

@ -11,26 +11,25 @@ import Combine
import TwigsCore
struct CategoryListView: View {
@EnvironmentObject var categoryDataStore: CategoryListDataStore
@EnvironmentObject var apiService: TwigsApiService
@EnvironmentObject var dataStore: DataStore
@State var requestId: String = ""
@ViewBuilder
var body: some View {
InlineLoadingView(
data: $categoryDataStore.categories,
action: { await self.categoryDataStore.getCategories(budgetId: budget.id, expense: nil, archived: nil, count: nil, page: nil) },
data: $dataStore.categories,
action: { await self.dataStore.getCategories(budgetId: budget.id, expense: nil, archived: nil, count: nil, page: nil) },
errorTextLocalizedStringKey: "Failed to load categories"
) { categories in
List {
Section {
ForEach(categories.filter { !$0.archived }) { category in
CategoryListItemView(CategoryDataStore(transactionRepository: apiService), budget: budget, category: category)
CategoryListItemView(CategoryDataStore(dataStore.apiService), budget: budget, category: category)
}
}
Section("Archived") {
ForEach(categories.filter { $0.archived }) { category in
CategoryListItemView(CategoryDataStore(transactionRepository: apiService), budget: budget, category: category)
CategoryListItemView(CategoryDataStore(dataStore.apiService), budget: budget, category: category)
}
}
}
@ -46,7 +45,7 @@ struct CategoryListView: View {
struct CategoryListItemView: View {
let category: TwigsCore.Category
let budget: Budget
@EnvironmentObject var categoryListDataStore: CategoryListDataStore
@EnvironmentObject var dataStore: DataStore
@ObservedObject var categoryDataStore: CategoryDataStore
init(_ categoryDataStore: CategoryDataStore, budget: Budget, category: TwigsCore.Category) {
@ -68,11 +67,11 @@ struct CategoryListItemView: View {
var body: some View {
NavigationLink(
tag: category,
selection: $categoryListDataStore.selectedCategory,
selection: $dataStore.selectedCategory,
destination: {
CategoryDetailsView(self.budget)
.environmentObject(categoryDataStore)
.navigationBarTitle(categoryListDataStore.selectedCategory?.title ?? "")
.navigationBarTitle(dataStore.selectedCategory?.title ?? "")
},
label: {
VStack(alignment: .leading) {

460
Twigs/DataStore.swift Normal file
View file

@ -0,0 +1,460 @@
//
// DataStore.swift
// Twigs
//
// Created by William Brawner on 5/16/22.
// Copyright © 2022 William Brawner. All rights reserved.
//
import Collections
import Combine
import Foundation
import TwigsCore
private let LAST_BUDGET = "LAST_BUDGET"
@MainActor
class DataStore : ObservableObject {
let apiService: TwigsApiService
@Published var budgets: AsyncData<[Budget]> = .empty
@Published var budget: AsyncData<Budget> = .empty {
didSet {
self.overview = .empty
if case let .success(budget) = self.budget {
UserDefaults.standard.set(budget.id, forKey: LAST_BUDGET)
self.showBudgetSelection = false
Task {
await loadOverview(budget)
}
}
}
}
@Published var overview: AsyncData<BudgetOverview> = .empty
@Published var showBudgetSelection: Bool = true
init(
_ apiService: TwigsApiService
) {
self.apiService = apiService
self.baseUrl = UserDefaults.standard.string(forKey: KEY_BASE_URL)
self.token = UserDefaults.standard.string(forKey: KEY_TOKEN)
self.userId = UserDefaults.standard.string(forKey: KEY_USER_ID)
}
func getBudgets(count: Int? = nil, page: Int? = nil) async {
// TODO: Find some way to extract this to a generic function
self.budgets = .loading
do {
let budgets = try await self.apiService.getBudgets(count: count, page: page).sorted(by: { $0.name < $1.name })
self.budgets = .success(budgets)
if self.budget != .empty {
return
}
if let id = UserDefaults.standard.string(forKey: LAST_BUDGET), let lastBudget = budgets.first(where: { $0.id == id }) {
self.budget = .success(lastBudget)
} else {
if let budget = budgets.first {
self.budget = .success(budget)
}
}
} catch {
self.budgets = .error(error)
}
}
func loadOverview(_ budget: Budget) async {
self.overview = .loading
do {
let budgetBalance = try await self.apiService.sumTransactions(budgetId: budget.id, categoryId: nil, from: nil, to: nil)
let categories = try await self.apiService.getCategories(budgetId: budget.id, expense: nil, archived: false, count: nil, page: nil)
var budgetOverview = BudgetOverview(budget: budget, balance: budgetBalance.balance)
try await withThrowingTaskGroup(of: (TwigsCore.Category, BalanceResponse).self) { group in
for category in categories {
group.addTask {
return (category, try await self.apiService.sumTransactions(budgetId: nil, categoryId: category.id, from: nil, to: nil))
}
}
for try await (category, response) in group {
if category.expense {
budgetOverview.expectedExpenses += category.amount
} else {
budgetOverview.expectedIncome += category.amount
}
if category.expense {
budgetOverview.actualExpenses += abs(response.balance)
} else {
budgetOverview.actualIncome += response.balance
}
}
}
self.overview = .success(budgetOverview)
} catch {
self.overview = .error(error)
}
}
func selectBudget(_ budget: Budget) {
self.budget = .success(budget)
}
@Published var categories: AsyncData<[TwigsCore.Category]> = .empty
@Published var category: AsyncData<TwigsCore.Category> = .empty {
didSet {
if case let .success(category) = self.category {
self.selectedCategory = category
} else if case .empty = self.category {
self.selectedCategory = nil
}
}
}
@Published var selectedCategory: TwigsCore.Category? = nil
func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = false, count: Int? = nil, page: Int? = nil) async {
self.categories = .loading
do {
let categories = try await apiService.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page)
self.categories = .success(categories)
} catch {
self.categories = .error(error)
}
}
func save(_ category: TwigsCore.Category) async {
self.category = .loading
do {
var savedCategory: TwigsCore.Category
if category.id != "" {
savedCategory = try await self.apiService.updateCategory(category)
} else {
savedCategory = try await self.apiService.createCategory(category)
}
self.category = .success(savedCategory)
if case let .success(categories) = self.categories {
var updatedCategories = categories.filter(withoutId: category.id)
updatedCategories.append(savedCategory)
self.categories = .success(updatedCategories.sorted(by: { $0.title < $1.title }))
}
} catch {
self.category = .error(error, category)
}
}
func delete(_ category: TwigsCore.Category) async {
self.category = .loading
do {
try await self.apiService.deleteCategory(category.id)
self.category = .empty
if case let .success(categories) = self.categories {
self.categories = .success(categories.filter(withoutId: category.id))
}
} catch {
self.category = .error(error, category)
}
}
func edit(_ category: TwigsCore.Category) async {
self.category = .editing(category)
}
func cancelEditCategory() {
if let category = self.selectedCategory {
self.category = .success(category)
} else {
self.category = .empty
}
}
func clearSelectedCategory() {
self.category = .empty
}
@Published var recurringTransactions: AsyncData<[RecurringTransaction]> = .empty
@Published var recurringTransaction: AsyncData<RecurringTransaction> = .empty {
didSet {
if case let .success(transaction) = self.recurringTransaction {
self.selectedRecurringTransaction = transaction
} else if case .empty = recurringTransaction {
self.selectedRecurringTransaction = nil
}
}
}
@Published var selectedRecurringTransaction: RecurringTransaction? = nil
func getRecurringTransactions() async {
guard case let .success(budget) = self.budget else {
return
}
self.recurringTransactions = .loading
do {
let transactions = try await self.apiService.getRecurringTransactions(budget.id)
self.recurringTransactions = .success(transactions.sorted(by: { $0.title < $1.title }))
} catch {
self.recurringTransactions = .error(error)
}
}
func saveRecurringTransaction(_ transaction: RecurringTransaction) async {
self.recurringTransaction = .loading
do {
var savedTransaction: RecurringTransaction
if (transaction.id != "") {
savedTransaction = try await self.apiService.updateRecurringTransaction(transaction)
} else {
savedTransaction = try await self.apiService.createRecurringTransaction(transaction)
}
self.recurringTransaction = .success(savedTransaction)
if case var .success(transactions) = self.recurringTransactions {
transactions = transactions.filter(withoutId: savedTransaction.id)
transactions.append(savedTransaction)
self.recurringTransactions = .success(transactions.sorted(by: { $0.title < $1.title }))
}
} catch {
self.recurringTransactions = .error(error)
}
}
func deleteRecurringTransaction(_ transaction: RecurringTransaction) async {
self.recurringTransaction = .loading
do {
try await self.apiService.deleteRecurringTransaction(transaction.id)
self.recurringTransaction = .empty
if case let .success(transactions) = self.recurringTransactions {
self.recurringTransactions = .success(transactions.filter(withoutId: transaction.id))
}
} catch {
self.recurringTransaction = .error(error, transaction)
}
}
func clearSelectedRecurringTransaction() {
self.recurringTransaction = .empty
}
@Published var transactions: AsyncData<OrderedDictionary<String, [Transaction]>> = .empty
@Published var transaction: AsyncData<Transaction> = .empty {
didSet {
if case let .success(transaction) = self.transaction {
self.selectedTransaction = transaction
} else if case .empty = self.transaction {
self.selectedTransaction = nil
}
}
}
@Published var selectedTransaction: Transaction? = nil
private var budgetId: String = ""
private var categoryId: String? = nil
private var from: Date? = nil
private var count: Int? = nil
private var page: Int? = nil
func getTransactions(from: Date? = nil, count: Int? = nil, page: Int? = nil) async {
self.from = from
self.count = count
self.page = page
await self.getTransactions()
}
func getTransactions() async {
guard case let .success(budget) = self.budget else {
self.transactions = .error(NetworkError.unknown)
return
}
self.budgetId = budget.id
if case let .success(category) = self.category {
self.categoryId = category.id
} else {
self.categoryId = nil
}
self.transactions = .loading
do {
var categoryIds: [String] = []
if let categoryId = categoryId {
categoryIds.append(categoryId)
}
let transactions = try await self.apiService.getTransactions(
budgetIds: [budgetId],
categoryIds: categoryIds,
from: from ?? Date.firstOfMonth,
to: nil,
count: count,
page: page
)
let groupedTransactions = OrderedDictionary<String,[Transaction]>(grouping: transactions, by: { $0.date.toLocaleString() })
self.transactions = .success(groupedTransactions)
} catch {
self.transactions = .error(error)
}
}
func saveTransaction(_ transaction: Transaction) async {
self.transaction = .saving(transaction)
do {
var savedTransaction: Transaction
if (transaction.id != "") {
savedTransaction = try await self.apiService.updateTransaction(transaction)
} else {
savedTransaction = try await self.apiService.createTransaction(transaction)
}
self.transaction = .success(savedTransaction)
await getTransactions()
} catch {
self.transaction = .error(error, transaction)
}
}
func deleteTransaction(_ transaction: Transaction) async {
self.transaction = .loading
do {
try await self.apiService.deleteTransaction(transaction.id)
self.transaction = .empty
} catch {
self.transaction = .error(error, transaction)
}
}
func newTransaction() {
var budgetId = ""
if case let .success(budget) = self.budget {
budgetId = budget.id
}
var categoryId: String? = nil
if case let .success(category) = self.category {
categoryId = category.id
}
var createdBy = ""
if case let .success(user) = self.currentUser {
createdBy = user.id
}
self.transaction = .editing(TwigsCore.Transaction(categoryId: categoryId, createdBy: createdBy, budgetId: budgetId))
}
func editTransaction(_ transaction: Transaction) {
self.transaction = .editing(transaction)
}
func cancelEditTransaction() {
if let transaction = self.selectedTransaction {
self.transaction = .success(transaction)
} else {
self.transaction = .empty
}
}
func clearSelectedTransaction() {
self.transaction = .empty
}
@Published var currentUser: AsyncData<User> = .empty {
didSet {
if case .success(_) = self.currentUser {
self.showLogin = false
} else {
self.showLogin = true
}
}
}
private let KEY_BASE_URL = "BASE_URL"
private let KEY_TOKEN = "TOKEN"
private let KEY_USER_ID = "USER_ID"
@Published var baseUrl: String? {
didSet {
self.apiService.baseUrl = baseUrl
UserDefaults.standard.set(baseUrl, forKey: KEY_BASE_URL)
}
}
@Published var token: String? {
didSet {
self.apiService.token = token
UserDefaults.standard.set(token, forKey: KEY_TOKEN)
}
}
@Published var userId: String? {
didSet {
UserDefaults.standard.set(userId, forKey: KEY_USER_ID)
}
}
@Published var showLogin: Bool = true
func login(server: String, username: String, password: String) async {
self.currentUser = .loading
self.apiService.baseUrl = server
// The API Service applies some validation and correcting of the server before returning it so we use that
// value instead of the original one
self.baseUrl = self.apiService.baseUrl ?? ""
do {
let response = try await self.apiService.login(username: username, password: password)
self.token = response.token
self.userId = response.userId
await self.loadProfile()
} catch {
switch error {
case NetworkError.jsonParsingFailed(let jsonError):
print(jsonError.localizedDescription)
default:
print(error.localizedDescription)
}
self.currentUser = .error(error)
}
}
func register(server: String, username: String, email: String, password: String, confirmPassword: String) async {
// TODO: Validate other fields as well
if !password.elementsEqual(confirmPassword) {
// TODO: Show error message to user
return
}
self.apiService.baseUrl = server
// The API Service applies some validation and correcting of the server before returning it so we use that
// value instead of the original one
self.baseUrl = self.apiService.baseUrl ?? ""
do {
_ = try await apiService.register(username: username, email: email, password: password)
} catch {
switch error {
case NetworkError.jsonParsingFailed(let jsonError):
print(jsonError.localizedDescription)
default:
print(error.localizedDescription)
}
return
}
await self.login(server: server, username: username, password: password)
}
func loadProfile() async {
guard let userId = self.userId, !userId.isEmpty else {
self.currentUser = .error(UserStatus.unauthenticated)
return
}
do {
let user = try await self.apiService.getUser(userId)
self.currentUser = .success(user)
} catch {
self.currentUser = .error(error)
}
}
@Published var user: AsyncData<User> = .empty
func getUser(_ id: String) async {
do {
let user = try await self.apiService.getUser(id)
self.user = .success(user)
} catch {
self.currentUser = .error(error)
}
}
}
enum UserStatus: Error, Equatable {
case unauthenticated
case authenticating
case failedAuthentication
case authenticated
case passwordMismatch
}

View file

@ -13,7 +13,7 @@ struct LoginView: View {
@State var server: String = ""
@State var username: String = ""
@State var password: String = ""
@EnvironmentObject var dataStore: AuthenticationDataStore
@EnvironmentObject var dataStore: DataStore
var loading: Bool {
switch dataStore.user {
case .loading:

View file

@ -10,8 +10,9 @@ import SwiftUI
import TwigsCore
struct ProfileView: View {
@EnvironmentObject var authDataStore: AuthenticationDataStore
@EnvironmentObject var dataStore: DataStore
@ViewBuilder
var body: some View {
VStack(spacing: 10) {
Image(systemName: "person.circle.fill")
@ -21,7 +22,9 @@ struct ProfileView: View {
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 5)
Text(authDataStore.currentUser!.username)
if case let .success(user) = self.dataStore.currentUser {
Text(user.username)
}
NavigationLink(destination: EmptyView()) {
Text("change_password")
}

View file

@ -1,79 +0,0 @@
//
// RecurringTransactionDataStore.swift
// Twigs
//
// Created by William Brawner on 12/6/21.
// Copyright © 2021 William Brawner. All rights reserved.
//
import Foundation
import Combine
import Collections
import TwigsCore
@MainActor
class RecurringTransactionDataStore: AsyncObservableObject {
private let repository: RecurringTransactionsRepository
@Published var transactions: AsyncData<[RecurringTransaction]> = .empty
@Published var transaction: AsyncData<RecurringTransaction> = .empty {
didSet {
if case let .success(transaction) = self.transaction {
self.selectedTransaction = transaction
} else if case .empty = transaction {
self.selectedTransaction = nil
}
}
}
@Published var selectedTransaction: RecurringTransaction? = nil
init(_ repository: RecurringTransactionsRepository) {
self.repository = repository
}
func getRecurringTransactions(_ budgetId: String) async {
self.transactions = .loading
do {
let transactions = try await self.repository.getRecurringTransactions(budgetId)
self.transactions = .success(transactions.sorted(by: { $0.title < $1.title }))
} catch {
self.transactions = .error(error)
}
}
func saveRecurringTransaction(_ transaction: RecurringTransaction) async {
self.transaction = .loading
do {
var savedTransaction: RecurringTransaction
if (transaction.id != "") {
savedTransaction = try await self.repository.updateRecurringTransaction(transaction)
} else {
savedTransaction = try await self.repository.createRecurringTransaction(transaction)
}
self.transaction = .success(savedTransaction)
if case var .success(transactions) = self.transactions {
transactions = transactions.filter(withoutId: savedTransaction.id)
transactions.append(savedTransaction)
self.transactions = .success(transactions.sorted(by: { $0.title < $1.title }))
}
} catch {
self.transactions = .error(error)
}
}
func deleteRecurringTransaction(_ transaction: RecurringTransaction) async {
self.transactions = .loading
do {
try await self.repository.deleteRecurringTransaction(transaction.id)
self.transaction = .empty
if case let .success(transactions) = self.transactions {
self.transactions = .success(transactions.filter(withoutId: transaction.id))
}
} catch {
self.transaction = .error(error, transaction)
}
}
func clearSelectedRecurringTransaction() {
self.transaction = .empty
}
}

View file

@ -10,10 +10,10 @@ import SwiftUI
import TwigsCore
struct RecurringTransactionDetailsView: View {
@EnvironmentObject var dataStore: RecurringTransactionDataStore
@EnvironmentObject var dataStore: DataStore
var body: some View {
if let transaction = dataStore.selectedTransaction {
if let transaction = dataStore.selectedRecurringTransaction {
ScrollView {
VStack(alignment: .leading) {
Text(transaction.title)

View file

@ -10,13 +10,12 @@ import SwiftUI
import TwigsCore
struct RecurringTransactionsListView: View {
@ObservedObject var dataStore: RecurringTransactionDataStore
let budget: Budget
@EnvironmentObject var dataStore: DataStore
var body: some View {
InlineLoadingView(
data: $dataStore.transactions,
action: { await self.dataStore.getRecurringTransactions(self.budget.id) },
data: $dataStore.recurringTransactions,
action: { await self.dataStore.getRecurringTransactions() },
errorTextLocalizedStringKey: "Failed to load recurring transactions"
) { (transactions: [RecurringTransaction]) in
List {
@ -31,13 +30,13 @@ struct RecurringTransactionsListView: View {
#if DEBUG
struct RecurringTransactionView_Previews: PreviewProvider {
static var previews: some View {
RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(MockRecurringTransactionRepository()), budget: MockBudgetRepository.budget)
RecurringTransactionsListView()
}
}
#endif
struct RecurringTransactionsListItemView: View {
@EnvironmentObject var dataStore: RecurringTransactionDataStore
@EnvironmentObject var dataStore: DataStore
let transaction: RecurringTransaction
init (_ transaction: RecurringTransaction) {
@ -47,7 +46,7 @@ struct RecurringTransactionsListItemView: View {
var body: some View {
NavigationLink(
tag: transaction,
selection: $dataStore.selectedTransaction,
selection: $dataStore.selectedRecurringTransaction,
destination: {
RecurringTransactionDetailsView()
.navigationBarTitle("details", displayMode: .inline)

View file

@ -22,7 +22,7 @@ class MockRecurringTransactionRepository: RecurringTransactionsRepository {
amount: 10000,
categoryId: MockCategoryRepository.category.id,
expense: true,
createdBy: MockUserRepository.user.id,
createdBy: MockUserRepository.currentUser.id,
budgetId: MockBudgetRepository.budget.id
)

View file

@ -14,7 +14,7 @@ struct RegistrationView: View {
@State var email: String = ""
@State var password: String = ""
@State var confirmedPassword: String = ""
@EnvironmentObject var dataStore: AuthenticationDataStore
@EnvironmentObject var dataStore: DataStore
var body: some View {
VStack {
@ -37,7 +37,7 @@ struct RegistrationView: View {
.textContentType(UITextContentType.newPassword)
Button("action_register", action: {
Task {
try await self.dataStore.register(
await self.dataStore.register(
server: self.server,
username: self.username,
email: self.email,

View file

@ -10,16 +10,14 @@ import SwiftUI
import TwigsCore
struct SidebarBudgetView: View {
@EnvironmentObject var authenticationDataStore: AuthenticationDataStore
@EnvironmentObject var budgetDataStore: BudgetsDataStore
@EnvironmentObject var apiService: TwigsApiService
@EnvironmentObject var dataStore: DataStore
@State var isSelectingBudget = true
@State var hasSelectedBudget = false
@State var tabSelection: Int? = 0
@ViewBuilder
var mainView: some View {
if case let .success(budget) = self.budgetDataStore.budget {
if case let .success(budget) = self.dataStore.budget {
NavigationView {
List {
NavigationLink(
@ -33,7 +31,7 @@ struct SidebarBudgetView: View {
NavigationLink(
tag: 1,
selection: $tabSelection,
destination: { TransactionListView<EmptyView>(apiService: apiService, budget: budget).navigationBarTitle("transactions") },
destination: { TransactionListView<EmptyView>().navigationBarTitle("transactions") },
label: { Label("transactions", systemImage: "dollarsign.circle") })
.keyboardShortcut("2")
NavigationLink(
@ -45,7 +43,7 @@ struct SidebarBudgetView: View {
NavigationLink(
tag: 3,
selection: $tabSelection,
destination: { RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(apiService), budget: budget).navigationBarTitle("recurring_transactions") },
destination: { RecurringTransactionsListView().navigationBarTitle("recurring_transactions") },
label: { Label("recurring_transactions", systemImage: "arrow.triangle.2.circlepath") })
.keyboardShortcut("4")
BudgetListsView()
@ -64,18 +62,18 @@ struct SidebarBudgetView: View {
@ViewBuilder
var body: some View {
mainView
.sheet(isPresented: $authenticationDataStore.showLogin,
.sheet(isPresented: $dataStore.showLogin,
onDismiss: {
Task {
await self.budgetDataStore.getBudgets()
await self.dataStore.getBudgets()
}
},
content: {
LoginView()
.environmentObject(authenticationDataStore)
.environmentObject(dataStore)
.onDisappear {
Task {
await self.budgetDataStore.getBudgets()
await self.dataStore.getBudgets()
}
}
})

View file

@ -10,21 +10,20 @@ import SwiftUI
import TwigsCore
struct TabbedBudgetView: View {
@EnvironmentObject var authenticationDataStore: AuthenticationDataStore
@EnvironmentObject var budgetDataStore: BudgetsDataStore
@EnvironmentObject var dataStore: DataStore
@EnvironmentObject var apiService: TwigsApiService
@AppStorage("budget_tab") var tabSelection: Int = 0
@ViewBuilder
var mainView: some View {
if case let .success(budget) = budgetDataStore.budget {
if case let .success(budget) = dataStore.budget {
TabView(selection: $tabSelection) {
NavigationView {
BudgetDetailsView(budget: budget)
.navigationBarTitle("overview")
.navigationBarItems(leading: HStack {
Button("budgets", action: {
self.budgetDataStore.showBudgetSelection = true
self.dataStore.showBudgetSelection = true
}).padding()
})
}
@ -35,7 +34,7 @@ struct TabbedBudgetView: View {
.tag(0)
.keyboardShortcut("1")
NavigationView {
TransactionListView<EmptyView>(apiService: apiService, budget: budget)
TransactionListView<EmptyView>()
.navigationBarTitle("transactions")
}
.tabItem {
@ -55,7 +54,7 @@ struct TabbedBudgetView: View {
.tag(2)
.keyboardShortcut("3")
NavigationView {
RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(apiService), budget: budget)
RecurringTransactionsListView()
.navigationBarTitle("recurring_transactions")
}
.tabItem {
@ -71,24 +70,24 @@ struct TabbedBudgetView: View {
}
var body: some View {
mainView.sheet(isPresented: $authenticationDataStore.showLogin,
mainView.sheet(isPresented: $dataStore.showLogin,
onDismiss: {
Task {
await self.budgetDataStore.getBudgets()
await self.dataStore.getBudgets()
}
},
content: {
LoginView()
.environmentObject(authenticationDataStore)
.environmentObject(dataStore)
.onDisappear {
Task {
await self.budgetDataStore.getBudgets()
await self.dataStore.getBudgets()
}
}
}).sheet(isPresented: $budgetDataStore.showBudgetSelection,
}).sheet(isPresented: $dataStore.showBudgetSelection,
content: {
List {
BudgetListsView().environmentObject(budgetDataStore)
BudgetListsView().environmentObject(dataStore)
}
})
.interactiveDismissDisabled(true)

View file

@ -1,110 +0,0 @@
//
// TransactionDataStore.swift
// Budget
//
// Created by Billy Brawner on 10/1/19.
// Copyright © 2019 William Brawner. All rights reserved.
//
import Foundation
import Combine
import Collections
import TwigsCore
@MainActor
class TransactionDataStore: ObservableObject {
@Published var transactions: AsyncData<OrderedDictionary<String, [Transaction]>> = .empty
@Published var transaction: AsyncData<Transaction> = .empty {
didSet {
if case let .success(transaction) = self.transaction {
self.selectedTransaction = transaction
} else if case .empty = self.transaction {
self.selectedTransaction = nil
}
}
}
@Published var selectedTransaction: Transaction? = nil
private var budgetId: String = ""
private var categoryId: String? = nil
private var from: Date? = nil
private var count: Int? = nil
private var page: Int? = nil
func getTransactions(_ budgetId: String, categoryId: String? = nil, from: Date? = nil, count: Int? = nil, page: Int? = nil) async {
self.budgetId = budgetId
self.categoryId = categoryId
self.from = from
self.count = count
self.page = page
await self.getTransactions()
}
func getTransactions() async {
self.transactions = .loading
do {
var categoryIds: [String] = []
if let categoryId = categoryId {
categoryIds.append(categoryId)
}
let transactions = try await self.transactionRepository.getTransactions(
budgetIds: [budgetId],
categoryIds: categoryIds,
from: from ?? Date.firstOfMonth,
to: nil,
count: count,
page: page
)
let groupedTransactions = OrderedDictionary<String,[Transaction]>(grouping: transactions, by: { $0.date.toLocaleString() })
self.transactions = .success(groupedTransactions)
} catch {
self.transactions = .error(error)
}
}
func saveTransaction(_ transaction: Transaction) async {
self.transaction = .saving(transaction)
do {
var savedTransaction: Transaction
if (transaction.id != "") {
savedTransaction = try await self.transactionRepository.updateTransaction(transaction)
} else {
savedTransaction = try await self.transactionRepository.createTransaction(transaction)
}
self.transaction = .success(savedTransaction)
await getTransactions()
} catch {
self.transaction = .error(error, transaction)
}
}
func deleteTransaction(_ transaction: Transaction) async {
self.transaction = .loading
do {
try await self.transactionRepository.deleteTransaction(transaction.id)
self.transaction = .empty
} catch {
self.transaction = .error(error, transaction)
}
}
func editTransaction(_ transaction: Transaction) {
self.transaction = .editing(transaction)
}
func cancelEdit() {
if let transaction = self.selectedTransaction {
self.transaction = .success(transaction)
} else {
self.transaction = .empty
}
}
func clearSelectedTransaction() {
self.transaction = .empty
}
private let transactionRepository: TransactionRepository
init(_ transactionRepository: TransactionRepository) {
self.transactionRepository = transactionRepository
}
}

View file

@ -14,14 +14,10 @@ class TransactionDetails: ObservableObject {
@Published var category: AsyncData<TwigsCore.Category> = .empty
@Published var budget: AsyncData<Budget> = .empty
@Published var user: AsyncData<User> = .empty
let budgetRepository: BudgetRepository
let categoryRepository: CategoryRepository
let userRepository: UserRepository
let apiService: TwigsApiService
init(budgetRepository: BudgetRepository, categoryRepository: CategoryRepository, userRepository: UserRepository) {
self.budgetRepository = budgetRepository
self.categoryRepository = categoryRepository
self.userRepository = userRepository
init(_ apiService: TwigsApiService) {
self.apiService = apiService
}
func loadDetails(_ transaction: TwigsCore.Transaction) async {
@ -41,7 +37,7 @@ class TransactionDetails: ObservableObject {
private func loadBudget(_ id: String) async {
self.budget = .loading
do {
let budget = try await budgetRepository.getBudget(id)
let budget = try await apiService.getBudget(id)
self.budget = .success(budget)
} catch {
self.budget = .error(error)
@ -51,7 +47,7 @@ class TransactionDetails: ObservableObject {
private func loadCategory(_ id: String) async {
self.category = .loading
do {
let category = try await categoryRepository.getCategory(id)
let category = try await apiService.getCategory(id)
self.category = .success(category)
} catch {
self.category = .error(error)
@ -61,7 +57,7 @@ class TransactionDetails: ObservableObject {
private func loadUser(_ id: String) async {
self.user = .loading
do {
let user = try await userRepository.getUser(id)
let user = try await apiService.getUser(id)
self.user = .success(user)
} catch {
self.user = .error(error)

View file

@ -8,12 +8,11 @@
import SwiftUI
import TwigsCore
import XCTest
import ArgumentParser
struct TransactionDetailsView: View {
@EnvironmentObject var apiService: TwigsApiService
@EnvironmentObject var authDataStore: AuthenticationDataStore
@EnvironmentObject var dataStore: TransactionDataStore
@EnvironmentObject var dataStore: DataStore
@ObservedObject var transactionDetails: TransactionDetails
var editing: Bool {
if case .editing(_) = dataStore.transaction {
@ -28,6 +27,15 @@ struct TransactionDetailsView: View {
init(_ transactionDetails: TransactionDetails) {
self.transactionDetails = transactionDetails
}
private var currentUserId: String? {
get {
if case let .success(currentUser) = self.dataStore.currentUser {
return currentUser.id
} else {
return nil
}
}
}
var body: some View {
if let transaction = self.dataStore.selectedTransaction {
@ -63,10 +71,8 @@ struct TransactionDetailsView: View {
})
.sheet(isPresented: .constant(self.editing), onDismiss: nil) {
TransactionFormSheet(transactionForm: TransactionForm(
budgetRepository: apiService,
categoryRepository: apiService,
transactionList: dataStore,
createdBy: authDataStore.currentUser!.id,
dataStore: dataStore,
createdBy: currentUserId!,
budgetId: transaction.budgetId,
categoryId: transaction.categoryId,
transaction: transaction
@ -173,7 +179,7 @@ struct UserLineItem: View {
#if DEBUG
struct TransactionDetailsView_Previews: PreviewProvider {
static var previews: some View {
TransactionDetailsView(TransactionDetails(budgetRepository: MockBudgetRepository(), categoryRepository: MockCategoryRepository(), userRepository: MockUserRepository()))
TransactionDetailsView(TransactionDetails(TwigsInMemoryCacheService()))
}
}
#endif

View file

@ -10,9 +10,8 @@ import Foundation
import TwigsCore
class TransactionForm: ObservableObject {
let budgetRepository: BudgetRepository
let categoryRepository: CategoryRepository
let transactionList: TransactionDataStore
let apiService: TwigsApiService
let dataStore: DataStore
let transaction: TwigsCore.Transaction?
let createdBy: String
let transactionId: String
@ -34,20 +33,17 @@ class TransactionForm: ObservableObject {
let showDelete: Bool
init(
budgetRepository: BudgetRepository,
categoryRepository: CategoryRepository,
transactionList: TransactionDataStore,
dataStore: DataStore,
createdBy: String,
budgetId: String,
categoryId: String? = nil,
transaction: TwigsCore.Transaction? = nil
) {
self.budgetRepository = budgetRepository
self.categoryRepository = categoryRepository
self.apiService = dataStore.apiService
self.budgetId = budgetId
self.categoryId = categoryId ?? ""
self.createdBy = createdBy
self.transactionList = transactionList
self.dataStore = dataStore
let baseTransaction = transaction ?? TwigsCore.Transaction(categoryId: categoryId, createdBy: createdBy, budgetId: budgetId)
self.transaction = transaction
self.transactionId = baseTransaction.id
@ -64,13 +60,13 @@ class TransactionForm: ObservableObject {
self.categories = .loading
var budgets: [Budget]
do {
budgets = try await budgetRepository.getBudgets(count: nil, page: nil)
budgets = try await apiService.getBudgets(count: nil, page: nil)
self.budgets = .success(budgets)
} catch {
self.budgets = .error(error)
}
do {
let categories = try await categoryRepository.getCategories(budgetId: nil, expense: nil, archived: false, count: nil, page: nil)
let categories = try await apiService.getCategories(budgetId: nil, expense: nil, archived: false, count: nil, page: nil)
self.cachedCategories = categories
updateCategories()
} catch {
@ -80,7 +76,7 @@ class TransactionForm: ObservableObject {
func save() async {
let amount = Double(self.amount) ?? 0.0
await transactionList.saveTransaction(Transaction(
await dataStore.saveTransaction(Transaction(
id: transactionId,
title: title,
description: description,
@ -97,7 +93,7 @@ class TransactionForm: ObservableObject {
guard let transaction = self.transaction else {
return
}
await transactionList.deleteTransaction(transaction)
await dataStore.deleteTransaction(transaction)
}
private func updateCategories() {

View file

@ -10,14 +10,14 @@ import SwiftUI
import TwigsCore
struct TransactionFormSheet: View {
@EnvironmentObject var transactionDataStore: TransactionDataStore
@EnvironmentObject var dataStore: DataStore
@ObservedObject var transactionForm: TransactionForm
@State private var showingAlert = false
@ViewBuilder
var body: some View {
NavigationView {
switch self.transactionDataStore.transaction {
switch self.dataStore.transaction {
case .loading:
EmbeddedLoadingView()
default:
@ -63,7 +63,7 @@ struct TransactionFormSheet: View {
}
.navigationTitle(transactionForm.transactionId.isEmpty ? "add_transaction" : "edit_transaction")
.navigationBarItems(
leading: Button("cancel", action: { transactionForm.transactionList.cancelEdit() }),
leading: Button("cancel", action: { dataStore.cancelEditTransaction() }),
trailing: Button("save", action: {
Task {
await transactionForm.save()

View file

@ -12,16 +12,14 @@ import Collections
import TwigsCore
struct TransactionListView<Content>: View where Content: View {
@EnvironmentObject var authDataStore: AuthenticationDataStore
@StateObject var transactionDataStore: TransactionDataStore
let apiService: TwigsApiService
@EnvironmentObject var dataStore: DataStore
@State var search: String = ""
@ViewBuilder let header: (() -> Content)?
var addingTransaction: Bool {
if case .editing(_) = self.transactionDataStore.transaction {
if case .editing(_) = self.dataStore.transaction {
return true
}
if case .saving(_) = self.transactionDataStore.transaction {
if case .saving(_) = self.dataStore.transaction {
return true
}
return false
@ -55,11 +53,41 @@ struct TransactionListView<Content>: View where Content: View {
}
}
private var currentUserId: String? {
get {
if case let .success(currentUser) = dataStore.currentUser {
return currentUser.id
} else {
return nil
}
}
}
private var budgetId: String? {
get {
if case let .success(budget) = dataStore.budget {
return budget.id
} else {
return nil
}
}
}
private var categoryId: String? {
get {
if case let .success(category) = dataStore.category {
return category.id
} else {
return nil
}
}
}
@ViewBuilder
var body: some View {
InlineLoadingView(
data: $transactionDataStore.transactions,
action: { await transactionDataStore.getTransactions(self.budget.id, categoryId: self.category?.id) },
data: $dataStore.transactions,
action: { await dataStore.getTransactions() },
errorTextLocalizedStringKey: "Failed to load transactions"
) { transactions in
List {
@ -67,25 +95,23 @@ struct TransactionListView<Content>: View where Content: View {
}
.searchable(text: $search)
.refreshable {
await transactionDataStore.getTransactions(self.budget.id, categoryId: self.category?.id, from: nil, count: nil, page: nil)
await dataStore.getTransactions()
}
.sheet(
isPresented: .constant(addingTransaction),
content: {
TransactionFormSheet(transactionForm: TransactionForm(
budgetRepository: apiService,
categoryRepository: apiService,
transactionList: transactionDataStore,
createdBy: authDataStore.currentUser!.id,
budgetId: self.budget.id,
categoryId: self.category?.id,
dataStore: dataStore,
createdBy: currentUserId ?? "",
budgetId: budgetId ?? "",
categoryId: categoryId,
transaction: nil
))
})
})
.navigationBarItems(
trailing: HStack {
Button(action: {
transactionDataStore.editTransaction(TwigsCore.Transaction(createdBy: authDataStore.currentUser!.id, budgetId: budget.id))
dataStore.newTransaction()
}) {
Image(systemName: "plus")
.padding()
@ -95,20 +121,13 @@ struct TransactionListView<Content>: View where Content: View {
}
}
let budget: Budget
let category: TwigsCore.Category?
init(apiService: TwigsApiService, budget: Budget, category: TwigsCore.Category? = nil, header: (() -> Content)? = nil) {
self.apiService = apiService
self._transactionDataStore = StateObject(wrappedValue: TransactionDataStore(apiService))
self.budget = budget
self.category = category
init(header: (() -> Content)? = nil) {
self.header = header
}
}
struct TransactionListItemView: View {
@EnvironmentObject var dataStore: TransactionDataStore
@EnvironmentObject var apiService: TwigsApiService
@EnvironmentObject var dataStore: DataStore
var transaction: TwigsCore.Transaction
var body: some View {
@ -116,11 +135,8 @@ struct TransactionListItemView: View {
tag: self.transaction,
selection: self.$dataStore.selectedTransaction,
destination: {
TransactionDetailsView(TransactionDetails(
budgetRepository: apiService,
categoryRepository: apiService,
userRepository: apiService
)).navigationBarTitle("details", displayMode: .inline)
TransactionDetailsView(TransactionDetails(dataStore.apiService))
.navigationBarTitle("details", displayMode: .inline)
},
label: {
HStack {

View file

@ -20,7 +20,7 @@ class MockTransactionRepository: TransactionRepository {
amount: 10000,
categoryId: MockCategoryRepository.category.id,
expense: true,
createdBy: MockUserRepository.user.id,
createdBy: MockUserRepository.currentUser.id,
budgetId: MockBudgetRepository.budget.id
)

View file

@ -12,20 +12,10 @@ import TwigsCore
@main
struct TwigsApp: App {
@StateObject var apiService: TwigsInMemoryCacheService = TwigsInMemoryCacheService()
@AppStorage("BASE_URL") var baseUrl: String = ""
@AppStorage("TOKEN") var token: String = ""
@AppStorage("USER_ID") var userId: String = ""
var body: some Scene {
WindowGroup {
MainView(self.apiService, baseUrl: self.$baseUrl, token: self.$token, userId: self.$userId)
.environmentObject(apiService as TwigsApiService)
.onAppear {
if self.baseUrl != "", self.token != "" {
self.apiService.baseUrl = self.baseUrl
self.apiService.token = self.token
}
}
MainView(apiService as TwigsApiService)
}
}
}

View file

@ -1,104 +0,0 @@
import Foundation
import Combine
import SwiftUI
import TwigsCore
@MainActor
class AuthenticationDataStore: ObservableObject {
@Published var user: AsyncData<User> = .empty {
didSet {
if case let .success(user) = self.user {
currentUser = user
}
}
}
@Published var currentUser: User? = nil {
didSet {
self.showLogin = self.currentUser == nil
}
}
@Binding private var baseUrl: String
@Binding private var token: String
@Binding private var userId: String
@Published var showLogin: Bool = true
let apiService: TwigsApiService
init(_ apiService: TwigsApiService, baseUrl: Binding<String>, token: Binding<String>, userId: Binding<String>) {
self.apiService = apiService
self._baseUrl = baseUrl
self._token = token
self._userId = userId
}
func login(server: String, username: String, password: String) async {
self.user = .loading
self.apiService.baseUrl = server
// The API Service applies some validation and correcting of the server before returning it so we use that
// value instead of the original one
self.baseUrl = self.apiService.baseUrl ?? ""
do {
let response = try await self.apiService.login(username: username, password: password)
self.token = response.token
self.userId = response.userId
await self.loadProfile()
} catch {
switch error {
case NetworkError.jsonParsingFailed(let jsonError):
print(jsonError.localizedDescription)
default:
print(error.localizedDescription)
}
self.user = .error(error)
}
}
func register(server: String, username: String, email: String, password: String, confirmPassword: String) async {
// TODO: Validate other fields as well
if !password.elementsEqual(confirmPassword) {
// TODO: Show error message to user
return
}
self.apiService.baseUrl = server
// The API Service applies some validation and correcting of the server before returning it so we use that
// value instead of the original one
self.baseUrl = self.apiService.baseUrl ?? ""
do {
_ = try await apiService.register(username: username, email: email, password: password)
} catch {
switch error {
case NetworkError.jsonParsingFailed(let jsonError):
print(jsonError.localizedDescription)
default:
print(error.localizedDescription)
}
return
}
await self.login(server: server, username: username, password: password)
}
func loadProfile() async {
if userId == "" {
self.user = .error(UserStatus.unauthenticated)
return
}
do {
let user = try await self.apiService.getUser(userId)
self.user = .success(user)
} catch {
self.user = .error(error)
}
}
}
private let BASE_URL = "BASE_URL"
private let TOKEN = "TOKEN"
private let USER_ID = "USER_ID"
enum UserStatus: Error, Equatable {
case unauthenticated
case authenticating
case failedAuthentication
case authenticated
case passwordMismatch
}

View file

@ -1,30 +0,0 @@
//
// UserDataStore.swift
// Twigs
//
// Created by Billy Brawner on 10/12/19.
// Copyright © 2019 William Brawner. All rights reserved.
//
import Foundation
import Combine
import TwigsCore
class UserDataStore: AsyncObservableObject {
@Published var user: AsyncData<User> = .empty
func getUser(_ id: String) async {
do {
let user = try await self.userRepository.getUser(id)
self.user = .success(user)
} catch {
self.user = .error(error)
}
}
private let userRepository: UserRepository
init(_ userRepository: UserRepository) {
self.userRepository = userRepository
}
}

View file

@ -13,7 +13,7 @@ import TwigsCore
#if DEBUG
class MockUserRepository: UserRepository {
static let loginResponse = LoginResponse(token: "token", expiration: "2020-01-01T12:00:00Z", userId: "0")
static let user = User(id: "0", username: "root", email: "root@localhost", avatar: nil)
static let currentUser = User(id: "0", username: "root", email: "root@localhost", avatar: nil)
static var token: String? = nil
func setToken(_ token: String) {
@ -21,11 +21,11 @@ class MockUserRepository: UserRepository {
}
func getUser(_ id: String) async throws -> User {
return MockUserRepository.user
return MockUserRepository.currentUser
}
func searchUsers(_ withUsername: String) async throws -> [User] {
return [MockUserRepository.user]
return [MockUserRepository.currentUser]
}
func setServer(_ server: String) {
@ -36,7 +36,7 @@ class MockUserRepository: UserRepository {
}
func register(username: String, email: String, password: String) async throws -> User {
return MockUserRepository.user
return MockUserRepository.currentUser
}
}

View file

@ -10,22 +10,12 @@ import SwiftUI
import TwigsCore
struct MainView: View {
@StateObject var authenticationDataStore: AuthenticationDataStore
@StateObject var budgetDataStore: BudgetsDataStore
@StateObject var transactionList: TransactionDataStore
@StateObject var categoryList: CategoryListDataStore
@StateObject var userDataStore: UserDataStore
@StateObject var recurringTransactionList: RecurringTransactionDataStore
@StateObject var dataStore: DataStore
let apiService: TwigsApiService
init(_ apiService: TwigsApiService, baseUrl: Binding<String>, token: Binding<String>, userId: Binding<String>) {
init(_ apiService: TwigsApiService) {
self.apiService = apiService
self._authenticationDataStore = StateObject(wrappedValue: AuthenticationDataStore(apiService, baseUrl: baseUrl, token: token, userId: userId))
self._budgetDataStore = StateObject(wrappedValue: BudgetsDataStore(budgetRepository: apiService, categoryRepository: apiService, transactionRepository: apiService))
self._categoryList = StateObject(wrappedValue: CategoryListDataStore(apiService))
self._userDataStore = StateObject(wrappedValue: UserDataStore(apiService))
self._transactionList = StateObject(wrappedValue: TransactionDataStore(apiService))
self._recurringTransactionList = StateObject(wrappedValue: RecurringTransactionDataStore(apiService))
self._dataStore = StateObject(wrappedValue: DataStore(apiService))
}
@ViewBuilder
@ -39,15 +29,10 @@ struct MainView: View {
var body: some View {
mainView
.environmentObject(transactionList)
.environmentObject(categoryList)
.environmentObject(budgetDataStore)
.environmentObject(userDataStore)
.environmentObject(recurringTransactionList)
.environmentObject(authenticationDataStore)
.environmentObject(dataStore)
.onAppear {
Task {
await self.authenticationDataStore.loadProfile()
await self.dataStore.loadProfile()
}
}
}
@ -55,6 +40,6 @@ struct MainView: View {
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView(TwigsInMemoryCacheService(), baseUrl: .constant(""), token: .constant(""), userId: .constant(""))
MainView(TwigsInMemoryCacheService())
}
}