Use sheet for budget selection
This commit is contained in:
parent
f6d50c5af3
commit
3b74d90a9a
11 changed files with 164 additions and 131 deletions
|
@ -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 */,
|
||||
|
|
|
@ -44,6 +44,11 @@ struct BudgetDetailsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.navigationBarItems(trailing: HStack {
|
||||
Button("budgets", action: {
|
||||
self.budgetDataStore.deselectBudget()
|
||||
}).padding()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
// }
|
||||
//}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -35,7 +35,7 @@ struct TwigsApp: App {
|
|||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
TabbedBudgetView()
|
||||
.environmentObject(dataStoreProvider.authenticationDataStore())
|
||||
.environmentObject(dataStoreProvider.budgetsDataStore())
|
||||
.environmentObject(dataStoreProvider.categoryDataStore())
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue