diff --git a/Twigs.xcodeproj/project.pbxproj b/Twigs.xcodeproj/project.pbxproj index c7a69b8..7c683fb 100644 --- a/Twigs.xcodeproj/project.pbxproj +++ b/Twigs.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 282126A3235ABC1800072D52 /* TwigsInMemoryCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282126A2235ABC1800072D52 /* TwigsInMemoryCacheService.swift */; }; 282126BB235CDD3C00072D52 /* BudgetDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282126BA235CDD3C00072D52 /* BudgetDetailsView.swift */; }; 282126BD235CDE1400072D52 /* ProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282126BC235CDE1400072D52 /* ProgressView.swift */; }; - 284102252341998300EAFA29 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284102242341998300EAFA29 /* ContentView.swift */; }; 2841022723419A2B00EAFA29 /* TabbedBudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2841022623419A2B00EAFA29 /* TabbedBudgetView.swift */; }; 2841022C2342D8E400EAFA29 /* Budget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2841022B2342D8E400EAFA29 /* Budget.swift */; }; 284102302342D97300EAFA29 /* BudgetListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2841022F2342D97300EAFA29 /* BudgetListsView.swift */; }; @@ -76,7 +75,6 @@ 282126A4235BCB7500072D52 /* Twigs.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Twigs.entitlements; sourceTree = ""; }; 282126BA235CDD3C00072D52 /* BudgetDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetDetailsView.swift; sourceTree = ""; }; 282126BC235CDE1400072D52 /* ProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressView.swift; sourceTree = ""; }; - 284102242341998300EAFA29 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 2841022623419A2B00EAFA29 /* TabbedBudgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedBudgetView.swift; sourceTree = ""; }; 2841022B2342D8E400EAFA29 /* Budget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Budget.swift; sourceTree = ""; }; 2841022F2342D97300EAFA29 /* BudgetListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetListsView.swift; sourceTree = ""; }; @@ -450,7 +448,6 @@ 28FE6AFA23441E3700D5543E /* CategoryDataStore.swift in Sources */, 28B9E50E2346BCB2007C3909 /* RegistrationView.swift in Sources */, 284102322342E12F00EAFA29 /* CategoryListView.swift in Sources */, - 284102252341998300EAFA29 /* ContentView.swift in Sources */, 8043EB84271F26ED00498E73 /* CategoryDetailsView.swift in Sources */, 289510242352AAFC00BC862B /* UserDataStore.swift in Sources */, 28FE6B002344308600D5543E /* Transaction.swift in Sources */, diff --git a/Twigs/Budget/BudgetDetailsView.swift b/Twigs/Budget/BudgetDetailsView.swift index b695f21..37c949c 100644 --- a/Twigs/Budget/BudgetDetailsView.swift +++ b/Twigs/Budget/BudgetDetailsView.swift @@ -44,6 +44,11 @@ struct BudgetDetailsView: View { } } } + .navigationBarItems(trailing: HStack { + Button("budgets", action: { + self.budgetDataStore.deselectBudget() + }).padding() + }) } } diff --git a/Twigs/Budget/BudgetListsView.swift b/Twigs/Budget/BudgetListsView.swift index d0d4e22..bf52622 100644 --- a/Twigs/Budget/BudgetListsView.swift +++ b/Twigs/Budget/BudgetListsView.swift @@ -11,12 +11,12 @@ import SwiftUI import Combine struct BudgetListsView: View { - @EnvironmentObject var budgetsDataStore: BudgetsDataStore + @EnvironmentObject var budgetDataStore: BudgetsDataStore @ViewBuilder var body: some View { NavigationView { - switch budgetsDataStore.budgets { + switch budgetDataStore.budgets { case .success(let budgets): Section { List(budgets) { budget in @@ -31,32 +31,40 @@ struct BudgetListsView: View { // TODO: Handle each network failure type Text("budgets_load_failure").navigationBarTitle("budgets") Button("action_retry", action: { - self.budgetsDataStore.getBudgets() + self.budgetDataStore.getBudgets() }) } }.onAppear { - self.budgetsDataStore.getBudgets() + self.budgetDataStore.getBudgets() } - } } struct BudgetListItemView: View { - @EnvironmentObject var budgetsDataStore: BudgetsDataStore - var budget: Budget - + @EnvironmentObject var budgetDataStore: BudgetsDataStore + let budget: Budget + var body: some View { - NavigationLink( - budget.name, - tag: budget, - selection: $budgetsDataStore.budget, - destination: { - TabbedBudgetView(budget) - .navigationBarTitle(budget.name) + Button( + action: { + self.budgetDataStore.selectBudget(budget) + }, + label: { + VStack(alignment: .leading) { + Text(verbatim: budget.name) + .foregroundColor(.primary) + .lineLimit(1) + if budget.description?.isEmpty == false { + Text(verbatim: budget.description!) + .font(.subheadline) + .foregroundColor(.secondary) + .lineLimit(1) + } + } } ) } - + init (_ budget: Budget) { self.budget = budget } diff --git a/Twigs/Budget/BudgetsDataStore.swift b/Twigs/Budget/BudgetsDataStore.swift index df88224..8e14e67 100644 --- a/Twigs/Budget/BudgetsDataStore.swift +++ b/Twigs/Budget/BudgetsDataStore.swift @@ -15,19 +15,28 @@ class BudgetsDataStore: ObservableObject { private let transactionRepository: TransactionRepository private var currentRequest: AnyCancellable? = nil @Published var budgets: Result<[Budget], NetworkError> = .failure(.loading) - @Published var budget: Budget? = nil { + @Published var budget: Result? = .failure(.loading) { didSet { - UserDefaults.standard.set(budget?.id, forKey: LAST_BUDGET) + if case let .success(budget) = self.budget { + UserDefaults.standard.set(budget.id, forKey: LAST_BUDGET) + } } } @Published var overview: Result = .failure(.loading) + var showBudgetSelection: Bool { + get { + return self.budget == nil + } + set { } + } + init(budgetRepository: BudgetRepository, categoryRepository: CategoryRepository, transactionRepository: TransactionRepository) { self.budgetRepository = budgetRepository self.categoryRepository = categoryRepository self.transactionRepository = transactionRepository } - + func getBudgets(count: Int? = nil, page: Int? = nil) { self.budgets = .failure(.loading) @@ -53,9 +62,15 @@ class BudgetsDataStore: ObservableObject { } }, receiveValue: { (budgets) in self.budgets = .success(budgets.sorted(by: { $0.name < $1.name })) - if let id = UserDefaults.standard.string(forKey: LAST_BUDGET) { - if let budget = budgets.first(where: { $0.id == id }) { - self.budget = budget + if self.budget != nil { + if let id = UserDefaults.standard.string(forKey: LAST_BUDGET) { + if let budget = budgets.first(where: { $0.id == id }) { + self.budget = .success(budget) + } else { + self.budget = nil + } + } else { + self.budget = nil } } }) @@ -124,7 +139,7 @@ class BudgetsDataStore: ObservableObject { CategoryBalance(category: category, balance: $0.balance) }.eraseToAnyPublisher()) } - + self.currentRequest = Publishers.MergeMany(categorySums) .collect() .receive(on: DispatchQueue.main) @@ -157,6 +172,16 @@ class BudgetsDataStore: ObservableObject { }) }) } + + func selectBudget(_ budget: Budget) { + self.objectWillChange.send() + self.budget = .success(budget) + } + + func deselectBudget() { + self.objectWillChange.send() + self.budget = nil + } } private let LAST_BUDGET = "LAST_BUDGET" diff --git a/Twigs/ContentView.swift b/Twigs/ContentView.swift deleted file mode 100644 index ea65d5a..0000000 --- a/Twigs/ContentView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// ContentView.swift -// Budget -// -// Created by Billy Brawner on 9/29/19. -// Copyright © 2019 William Brawner. All rights reserved. -// - -import SwiftUI - -struct ContentView: View { - @EnvironmentObject var authenticationDataStore: AuthenticationDataStore - - @ViewBuilder - var body: some View { - if showLogin() { - LoginView() - } else { - BudgetListsView() - } - } - - func showLogin() -> Bool { - switch authenticationDataStore.currentUser { - case .failure: - return true - default: - return false - } - } -} - -//struct ContentView_Previews: PreviewProvider { -// static var previews: some View { -// ContentView() -// } -//} diff --git a/Twigs/TabbedBudgetView.swift b/Twigs/TabbedBudgetView.swift index 3c205e1..b6374ff 100644 --- a/Twigs/TabbedBudgetView.swift +++ b/Twigs/TabbedBudgetView.swift @@ -9,77 +9,90 @@ import SwiftUI struct TabbedBudgetView: View { + @EnvironmentObject var authenticationDataStore: AuthenticationDataStore @EnvironmentObject var budgetDataStore: BudgetsDataStore @EnvironmentObject var categoryDataStore: CategoryDataStore - let budget: Budget + @State var isSelectingBudget = true + @State var hasSelectedBudget = false @State var isAddingTransaction = false @State var tabSelection: Int = 0 - var body: some View { - TabView(selection: $tabSelection) { - BudgetDetailsView(budget: self.budget) + @ViewBuilder + var mainView: some View { + if case let .success(budget) = budgetDataStore.budget { + TabView(selection: $tabSelection) { + NavigationView { + BudgetDetailsView(budget: budget) + .navigationBarTitle("overview") + } .tabItem { Image(systemName: "chart.line.uptrend.xyaxis.circle.fill") Text("overview") } .tag(0) .keyboardShortcut("1") - TransactionListView(self.budget) - .sheet(isPresented: $isAddingTransaction, - onDismiss: { - isAddingTransaction = false - }, - content: { - AddTransactionView(showSheet: self.$isAddingTransaction, budgetId: self.budget.id) - .navigationBarTitle("add_transaction") - }) + NavigationView { + TransactionListView(budget) + .sheet(isPresented: $isAddingTransaction, + onDismiss: { + isAddingTransaction = false + }, + content: { + AddTransactionView(showSheet: self.$isAddingTransaction, budgetId: budget.id) + .navigationBarTitle("add_transaction") + }) + .navigationBarTitle("transactions") + } .tabItem { Image(systemName: "dollarsign.circle.fill") Text("transactions") } .tag(1) .keyboardShortcut("2") - CategoryListView(self.budget).tabItem { - Image(systemName: "chart.pie.fill") - Text("categories") - } - .tag(2) - .keyboardShortcut("3") - ProfileView().tabItem { - Image(systemName: "person.circle.fill") - Text("profile") - } - .tag(3) - .keyboardShortcut("4") - }.navigationBarItems( - trailing: HStack { - if tabSelection == 1 { - Button(action: { - self.isAddingTransaction = true - }) { - Image(systemName: "plus") - .padding() - } - .keyboardShortcut("n") + NavigationView { + CategoryListView(budget) + .navigationBarTitle("categories") } - } - ) - .onReceive(NotificationCenter.default.publisher(for: Notification.Name("switchTabs"))) { notification in - if let tabTag = notification.object as? Int { - if 0...3 ~= tabTag { - self.tabSelection = tabTag - print("Updating tabSelection to \(tabTag)") - } else { - print("Ignoring value \(tabTag)") - } + .tabItem { + Image(systemName: "chart.pie.fill") + Text("categories") } + .tag(2) + .keyboardShortcut("3") + NavigationView { + ProfileView() + .navigationBarTitle("profile") + } + .tabItem { + Image(systemName: "person.circle.fill") + Text("profile") + } + .tag(3) + .keyboardShortcut("4") } + } else { + Text("Loading…") + } } - init (_ budget: Budget) { - self.budget = budget + var body: some View { + mainView.sheet(isPresented: $authenticationDataStore.showLogin, + onDismiss: { + self.budgetDataStore.getBudgets() + }, + content: { + LoginView() + .environmentObject(authenticationDataStore) + }).sheet(isPresented: $budgetDataStore.showBudgetSelection, + content: { + BudgetListsView() + .environmentObject(budgetDataStore) + }) + .interactiveDismissDisabled(!hasSelectedBudget) } } + + // //struct TabbedBudgetView_Previews: PreviewProvider { // static var previews: some View { diff --git a/Twigs/Transaction/TransactionDataStore.swift b/Twigs/Transaction/TransactionDataStore.swift index 9e46039..2dfe631 100644 --- a/Twigs/Transaction/TransactionDataStore.swift +++ b/Twigs/Transaction/TransactionDataStore.swift @@ -71,7 +71,9 @@ class TransactionDataStore: ObservableObject { func saveTransaction(_ transaction: Transaction) { self.transaction = .failure(.loading) - +// if transaction.categoryId == "" { +// transaction.categoryId = nil +// } var transactionSavePublisher: AnyPublisher if (transaction.id != "") { transactionSavePublisher = self.transactionRepository.updateTransaction(transaction) diff --git a/Twigs/Transaction/TransactionDetailsView.swift b/Twigs/Transaction/TransactionDetailsView.swift index cb6026e..1c7704d 100644 --- a/Twigs/Transaction/TransactionDetailsView.swift +++ b/Twigs/Transaction/TransactionDetailsView.swift @@ -101,9 +101,18 @@ struct CategoryLineItem: View { struct BudgetLineItem: View { @EnvironmentObject var budgetDataStore: BudgetsDataStore + var budgetName: String { + get { + if case let .success(budget) = budgetDataStore.budget { + return budget.name + } else { + return "" + } + } + } var body: some View { - LabeledField(label: "budget", value: budgetDataStore.budget?.name ?? "", showDivider: true) + LabeledField(label: "budget", value: budgetName, showDivider: true) } } diff --git a/Twigs/Transaction/TransactionListView.swift b/Twigs/Transaction/TransactionListView.swift index b4bc555..4d43839 100644 --- a/Twigs/Transaction/TransactionListView.swift +++ b/Twigs/Transaction/TransactionListView.swift @@ -12,6 +12,7 @@ import Combine struct TransactionListView: View { @EnvironmentObject var transactionDataStore: TransactionDataStore @State var requestId: String = "" + @State var isAddingTransaction = false @ViewBuilder var body: some View { @@ -22,6 +23,20 @@ struct TransactionListView: View { TransactionListItemView(transaction) } } + .sheet(isPresented: $isAddingTransaction, content: { + AddTransactionView(showSheet: $isAddingTransaction, budgetId: self.budget.id) + .navigationBarTitle("add_transaction") + }) + .navigationBarItems( + trailing: HStack { + Button(action: { + self.isAddingTransaction = true + }) { + Image(systemName: "plus") + .padding() + } + } + ) case nil, .failure(.loading): VStack { ActivityIndicator(isAnimating: .constant(true), style: .large) diff --git a/Twigs/TwigsApp.swift b/Twigs/TwigsApp.swift index 477f738..6cd2aa2 100644 --- a/Twigs/TwigsApp.swift +++ b/Twigs/TwigsApp.swift @@ -35,7 +35,7 @@ struct TwigsApp: App { var body: some Scene { WindowGroup { - ContentView() + TabbedBudgetView() .environmentObject(dataStoreProvider.authenticationDataStore()) .environmentObject(dataStoreProvider.budgetsDataStore()) .environmentObject(dataStoreProvider.categoryDataStore()) diff --git a/Twigs/User/AuthenticationDataStore.swift b/Twigs/User/AuthenticationDataStore.swift index ee93a8f..12543e2 100644 --- a/Twigs/User/AuthenticationDataStore.swift +++ b/Twigs/User/AuthenticationDataStore.swift @@ -1,30 +1,35 @@ import Foundation import Combine +import SwiftUI class AuthenticationDataStore: ObservableObject { private var currentRequest: AnyCancellable? = nil - var currentUser: Result = .failure(.unauthenticated) { - didSet { - self.objectWillChange.send() + @Published var currentUser: Result = .failure(.unauthenticated) + var showLogin: Bool { + get { + switch currentUser { + case .success(_): + print("Authenticated") + return false + default: + print("Unauthenticated") + return true + } } + set { } } - + func login(username: String, password: String) { - // Changes the status and notifies any observers of the change self.currentUser = .failure(.authenticating) - print("Logging in") // Perform the login currentRequest = self.userRepository.login(username: username, password: password) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { (status) in switch status { case .finished: - print("Login done") return - // Do nothing it means the network request just ended case .failure(let error): - print("Login failed") self.currentRequest = nil switch error { case .jsonParsingFailed(let jsonError): @@ -32,7 +37,6 @@ class AuthenticationDataStore: ObservableObject { default: print(error.localizedDescription) } - // Poulate your status with failed authenticating self.currentUser = .failure(.failedAuthentication) } }) { (session) in @@ -57,9 +61,7 @@ class AuthenticationDataStore: ObservableObject { switch status { case .finished: return - // Do nothing it means the network request just ended case .failure( _): - // Poulate your status with failed authenticating self.currentUser = .failure(.failedAuthentication) } }) { (user) in @@ -69,12 +71,10 @@ class AuthenticationDataStore: ObservableObject { private func loadProfile() { guard let userId = UserDefaults.standard.string(forKey: USER_ID) else { - print("No saved userId, unable to load profile") self.currentUser = .failure(.unauthenticated) return } guard let token = UserDefaults.standard.string(forKey: TOKEN) else { - print("No saved token, unable to load profile") self.currentUser = .failure(.unauthenticated) return } @@ -88,11 +88,9 @@ class AuthenticationDataStore: ObservableObject { self.currentRequest = nil return case .failure(_): - print("Failed to load user") self.currentUser = .failure(.unauthenticated) } }) { (user) in - print("Got user, loading budgets") self.currentUser = .success(user) } } @@ -104,8 +102,6 @@ class AuthenticationDataStore: ObservableObject { } } - // Needed since the default implementation is currently broken - let objectWillChange = ObservableObjectPublisher() private let userRepository: UserRepository }