Use sheet for budget selection

This commit is contained in:
William Brawner 2021-11-03 11:17:18 -06:00
parent f6d50c5af3
commit 3b74d90a9a
11 changed files with 164 additions and 131 deletions

View file

@ -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 = "<group>"; };
282126BA235CDD3C00072D52 /* BudgetDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetDetailsView.swift; sourceTree = "<group>"; };
282126BC235CDE1400072D52 /* ProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressView.swift; sourceTree = "<group>"; };
284102242341998300EAFA29 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
2841022623419A2B00EAFA29 /* TabbedBudgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedBudgetView.swift; sourceTree = "<group>"; };
2841022B2342D8E400EAFA29 /* Budget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Budget.swift; sourceTree = "<group>"; };
2841022F2342D97300EAFA29 /* BudgetListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetListsView.swift; sourceTree = "<group>"; };
@ -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 */,

View file

@ -44,6 +44,11 @@ struct BudgetDetailsView: View {
}
}
}
.navigationBarItems(trailing: HStack {
Button("budgets", action: {
self.budgetDataStore.deselectBudget()
}).padding()
})
}
}

View file

@ -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
}

View file

@ -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<Budget, NetworkError>? = .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<BudgetOverview, NetworkError> = .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"

View file

@ -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()
// }
//}

View file

@ -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 {

View file

@ -71,7 +71,9 @@ class TransactionDataStore: ObservableObject {
func saveTransaction(_ transaction: Transaction) {
self.transaction = .failure(.loading)
// if transaction.categoryId == "" {
// transaction.categoryId = nil
// }
var transactionSavePublisher: AnyPublisher<Transaction, NetworkError>
if (transaction.id != "") {
transactionSavePublisher = self.transactionRepository.updateTransaction(transaction)

View file

@ -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)
}
}

View file

@ -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)

View file

@ -35,7 +35,7 @@ struct TwigsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
TabbedBudgetView()
.environmentObject(dataStoreProvider.authenticationDataStore())
.environmentObject(dataStoreProvider.budgetsDataStore())
.environmentObject(dataStoreProvider.categoryDataStore())

View file

@ -1,30 +1,35 @@
import Foundation
import Combine
import SwiftUI
class AuthenticationDataStore: ObservableObject {
private var currentRequest: AnyCancellable? = nil
var currentUser: Result<User, UserStatus> = .failure(.unauthenticated) {
didSet {
self.objectWillChange.send()
@Published var currentUser: Result<User, UserStatus> = .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
}