Implement basic support for multi-window
This commit is contained in:
parent
b035b027ab
commit
e8c7b7e3a9
14 changed files with 120 additions and 363 deletions
|
@ -39,7 +39,6 @@
|
|||
28FE6AF823441E1D00D5543E /* Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF723441E1D00D5543E /* Category.swift */; };
|
||||
28FE6AFA23441E3700D5543E /* CategoryDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF923441E3700D5543E /* CategoryDataStore.swift */; };
|
||||
28FE6AFC23441E4500D5543E /* CategoryRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AFB23441E4500D5543E /* CategoryRepository.swift */; };
|
||||
28FE6AFE234428BF00D5543E /* DataStoreProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AFD234428BF00D5543E /* DataStoreProvider.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 */; };
|
||||
|
@ -114,7 +113,6 @@
|
|||
28FE6AF723441E1D00D5543E /* Category.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Category.swift; sourceTree = "<group>"; };
|
||||
28FE6AF923441E3700D5543E /* CategoryDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDataStore.swift; sourceTree = "<group>"; };
|
||||
28FE6AFB23441E4500D5543E /* CategoryRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryRepository.swift; sourceTree = "<group>"; };
|
||||
28FE6AFD234428BF00D5543E /* DataStoreProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreProvider.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>"; };
|
||||
|
@ -225,7 +223,6 @@
|
|||
children = (
|
||||
282126A4235BCB7500072D52 /* Twigs.entitlements */,
|
||||
28AC94FB233C373A00BFB70A /* Info.plist */,
|
||||
28FE6AFD234428BF00D5543E /* DataStoreProvider.swift */,
|
||||
28CE8B9423525F990072BC4C /* Extensions.swift */,
|
||||
28AC94F1233C373900BFB70A /* LoginView.swift */,
|
||||
2888234623512DBF003D3847 /* Observable.swift */,
|
||||
|
@ -461,7 +458,6 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
28FE6AFE234428BF00D5543E /* DataStoreProvider.swift in Sources */,
|
||||
28A1E95A235006A300CA57FE /* AddTransactionView.swift in Sources */,
|
||||
2821266023555FD300072D52 /* EditTransactionForm.swift in Sources */,
|
||||
282126622357E45F00072D52 /* TransactionEditView.swift in Sources */,
|
||||
|
|
|
@ -17,51 +17,7 @@ protocol BudgetRepository {
|
|||
func deleteBudget(_ id: String) -> AnyPublisher<Empty, NetworkError>
|
||||
}
|
||||
|
||||
class NetworkBudgetRepository: BudgetRepository {
|
||||
let apiService: TwigsApiService
|
||||
let cacheService: TwigsInMemoryCacheService?
|
||||
|
||||
init(_ apiService: TwigsApiService, cacheService: TwigsInMemoryCacheService? = nil) {
|
||||
self.apiService = apiService
|
||||
self.cacheService = cacheService
|
||||
}
|
||||
|
||||
func getBudgets(count: Int?, page: Int?) -> AnyPublisher<[Budget], NetworkError> {
|
||||
if let budgets = cacheService?.getBudgets(count: count, page: page) {
|
||||
return budgets
|
||||
}
|
||||
|
||||
return apiService.getBudgets(count: count, page: page).map { (budgets: [Budget]) in
|
||||
self.cacheService?.addBudgets(budgets)
|
||||
return budgets
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func getBudget(_ id: String) -> AnyPublisher<Budget, NetworkError> {
|
||||
if let budget = cacheService?.getBudget(id) {
|
||||
return budget
|
||||
}
|
||||
return apiService.getBudget(id).map { budget in
|
||||
self.cacheService?.addBudget(budget)
|
||||
return budget
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func newBudget(_ budget: Budget) -> AnyPublisher<Budget, NetworkError> {
|
||||
return apiService.newBudget(budget)
|
||||
}
|
||||
|
||||
func updateBudget(_ budget: Budget) -> AnyPublisher<Budget, NetworkError> {
|
||||
return apiService.updateBudget(budget)
|
||||
}
|
||||
|
||||
func deleteBudget(_ id: String) -> AnyPublisher<Empty, NetworkError> {
|
||||
return apiService.deleteBudget(id)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
class MockBudgetRepository: BudgetRepository {
|
||||
static let budget = Budget(
|
||||
id: "1",
|
||||
|
@ -95,5 +51,4 @@ class MockBudgetRepository: BudgetRepository {
|
|||
return Result.Publisher(Empty()).eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -31,6 +31,7 @@ class BudgetsDataStore: ObservableObject {
|
|||
self.budgetRepository = budgetRepository
|
||||
self.categoryRepository = categoryRepository
|
||||
self.transactionRepository = transactionRepository
|
||||
self.getBudgets(count: nil, page: nil)
|
||||
}
|
||||
|
||||
func getBudgets(count: Int? = nil, page: Int? = nil) {
|
||||
|
|
|
@ -17,64 +17,7 @@ protocol CategoryRepository {
|
|||
func deleteCategory(_ id: String) -> AnyPublisher<Empty, NetworkError>
|
||||
}
|
||||
|
||||
class NetworkCategoryRepository: CategoryRepository {
|
||||
let apiService: TwigsApiService
|
||||
let cacheService: TwigsInMemoryCacheService?
|
||||
|
||||
init(_ apiService: TwigsApiService, cacheService: TwigsInMemoryCacheService? = nil) {
|
||||
self.apiService = apiService
|
||||
self.cacheService = cacheService
|
||||
}
|
||||
|
||||
func getCategories(budgetId: String?, expense: Bool?, archived: Bool?, count: Int?, page: Int?) -> AnyPublisher<[Category], NetworkError> {
|
||||
if let categories = cacheService?.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page) {
|
||||
print("Returning categories from cache")
|
||||
return categories
|
||||
}
|
||||
|
||||
print("No cached categories, fetching from network")
|
||||
return apiService.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page).map { (categories: [Category]) in
|
||||
self.cacheService?.addCategories(categories)
|
||||
return categories
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func getCategory(_ categoryId: String) -> AnyPublisher<Category, NetworkError> {
|
||||
if let category = cacheService?.getCategory(categoryId) {
|
||||
print("Returning category from cache")
|
||||
return category
|
||||
}
|
||||
print("Category with ID \(categoryId) not cached, returning from network")
|
||||
return apiService.getCategory(categoryId).map { category in
|
||||
self.cacheService?.addCategory(category)
|
||||
return category
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func createCategory(_ category: Category) -> AnyPublisher<Category, NetworkError> {
|
||||
return apiService.newCategory(category).map {
|
||||
self.cacheService?.addCategory($0)
|
||||
return $0
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func updateCategory(_ category: Category) -> AnyPublisher<Category, NetworkError> {
|
||||
return apiService.updateCategory(category).map {
|
||||
self.cacheService?.updateCategory($0)
|
||||
return $0
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func deleteCategory(_ id: String) -> AnyPublisher<Empty, NetworkError> {
|
||||
return apiService.deleteCategory(id).map {
|
||||
self.cacheService?.removeCategory(id)
|
||||
return $0
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
class MockCategoryRepository: CategoryRepository {
|
||||
static let category = Category(
|
||||
budgetId: MockBudgetRepository.budget.id,
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
//
|
||||
// DataStoreProvider.swift
|
||||
// Budget
|
||||
//
|
||||
// Created by Billy Brawner on 10/1/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Wrapper for all types of data stores. Some are considered singletons, such as the AuthenticationDataStore, while others are created as needed
|
||||
*/
|
||||
class DataStoreProvider {
|
||||
private let budgetRepository: BudgetRepository
|
||||
private let categoryRepository: CategoryRepository
|
||||
private let transactionRepository: TransactionRepository
|
||||
private let userRepository: UserRepository
|
||||
|
||||
private let _authenticationDataStore: AuthenticationDataStore
|
||||
|
||||
func budgetsDataStore() -> BudgetsDataStore {
|
||||
return BudgetsDataStore(budgetRepository: budgetRepository, categoryRepository: categoryRepository, transactionRepository: transactionRepository)
|
||||
}
|
||||
|
||||
func categoryDataStore() -> CategoryDataStore {
|
||||
return CategoryDataStore(categoryRepository)
|
||||
}
|
||||
|
||||
func transactionDataStore() -> TransactionDataStore {
|
||||
return TransactionDataStore(transactionRepository)
|
||||
}
|
||||
|
||||
func authenticationDataStore() -> AuthenticationDataStore {
|
||||
return self._authenticationDataStore
|
||||
}
|
||||
|
||||
func userDataStore() -> UserDataStore {
|
||||
return UserDataStore(userRepository)
|
||||
}
|
||||
|
||||
init(
|
||||
budgetRepository: BudgetRepository,
|
||||
categoryRepository: CategoryRepository,
|
||||
transactionRepository: TransactionRepository,
|
||||
userRepository: UserRepository
|
||||
) {
|
||||
self.budgetRepository = budgetRepository
|
||||
self.categoryRepository = categoryRepository
|
||||
self.transactionRepository = transactionRepository
|
||||
self.userRepository = userRepository
|
||||
self._authenticationDataStore = AuthenticationDataStore(userRepository)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
class MockDataStoreProvider: DataStoreProvider {
|
||||
|
||||
override func authenticationDataStore() -> AuthenticationDataStore {
|
||||
return MockAuthenticationDataStore(MockUserRepository())
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(
|
||||
budgetRepository: MockBudgetRepository(),
|
||||
categoryRepository: MockCategoryRepository(),
|
||||
transactionRepository: MockTransactionRepository(),
|
||||
userRepository: MockUserRepository()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
|
@ -9,13 +9,29 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
|
||||
class TwigsApiService: RecurringTransactionsRepository {
|
||||
class TwigsApiService: BudgetRepository, CategoryRepository, RecurringTransactionsRepository, TransactionRepository, UserRepository {
|
||||
let requestHelper: RequestHelper
|
||||
|
||||
convenience init() {
|
||||
self.init(RequestHelper())
|
||||
}
|
||||
|
||||
init(_ requestHelper: RequestHelper) {
|
||||
self.requestHelper = requestHelper
|
||||
}
|
||||
|
||||
func setToken(_ token: String) {
|
||||
requestHelper.token = token
|
||||
}
|
||||
|
||||
func setServer(_ server: String) {
|
||||
var correctServer = server
|
||||
if !server.starts(with: "http://") && !server.starts(with: "https://") {
|
||||
correctServer = "http://\(correctServer)"
|
||||
}
|
||||
requestHelper.baseUrl = correctServer
|
||||
}
|
||||
|
||||
// MARK: Budgets
|
||||
|
||||
func getBudgets(count: Int? = nil, page: Int? = nil) -> AnyPublisher<[Budget], NetworkError> {
|
||||
|
@ -47,8 +63,8 @@ class TwigsApiService: RecurringTransactionsRepository {
|
|||
|
||||
// MARK: Transactions
|
||||
|
||||
func getTransactions(
|
||||
budgetIds: [String]? = nil,
|
||||
func getTransactions(
|
||||
budgetIds: [String],
|
||||
categoryIds: [String]? = nil,
|
||||
from: Date? = nil,
|
||||
to: Date? = nil,
|
||||
|
@ -56,9 +72,7 @@ class TwigsApiService: RecurringTransactionsRepository {
|
|||
page: Int? = nil
|
||||
) -> AnyPublisher<[Transaction], NetworkError> {
|
||||
var queries = [String: Array<String>]()
|
||||
if budgetIds != nil {
|
||||
queries["budgetIds"] = budgetIds!
|
||||
}
|
||||
queries["budgetIds"] = budgetIds
|
||||
if categoryIds != nil {
|
||||
queries["categoryIds"] = categoryIds!
|
||||
}
|
||||
|
@ -81,7 +95,7 @@ class TwigsApiService: RecurringTransactionsRepository {
|
|||
return requestHelper.get("/api/transactions/\(id)")
|
||||
}
|
||||
|
||||
func newTransaction(_ transaction: Transaction) -> AnyPublisher<Transaction, NetworkError> {
|
||||
func createTransaction(_ transaction: Transaction) -> AnyPublisher<Transaction, NetworkError> {
|
||||
return requestHelper.post("/api/transactions", data: transaction, type: Transaction.self)
|
||||
}
|
||||
|
||||
|
@ -140,7 +154,7 @@ class TwigsApiService: RecurringTransactionsRepository {
|
|||
return requestHelper.get("/api/categories/\(id)/balance")
|
||||
}
|
||||
|
||||
func newCategory(_ category: Category) -> AnyPublisher<Category, NetworkError> {
|
||||
func createCategory(_ category: Category) -> AnyPublisher<Category, NetworkError> {
|
||||
return requestHelper.post("/api/categories", data: category, type: Category.self)
|
||||
}
|
||||
|
||||
|
@ -174,11 +188,11 @@ class TwigsApiService: RecurringTransactionsRepository {
|
|||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func getUser(id: String) -> AnyPublisher<User, NetworkError> {
|
||||
func getUser(_ id: String) -> AnyPublisher<User, NetworkError> {
|
||||
return requestHelper.get("/api/users/\(id)")
|
||||
}
|
||||
|
||||
func searchUsers(query: String) -> AnyPublisher<[User], NetworkError> {
|
||||
func searchUsers(_ query: String) -> AnyPublisher<[User], NetworkError> {
|
||||
return requestHelper.get(
|
||||
"/api/users/search",
|
||||
queries: ["query": [query]]
|
||||
|
|
|
@ -9,33 +9,35 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
|
||||
class TwigsInMemoryCacheService {
|
||||
class TwigsInMemoryCacheService: TwigsApiService {
|
||||
var budgets = Set<Budget>()
|
||||
var categories = Set<Category>()
|
||||
var transactions = Set<Transaction>()
|
||||
|
||||
// MARK: Budgets
|
||||
func getBudgets(count: Int? = nil, page: Int? = nil) -> AnyPublisher<[Budget], NetworkError>? {
|
||||
override func getBudgets(count: Int? = nil, page: Int? = nil) -> AnyPublisher<[Budget], NetworkError> {
|
||||
let results = budgets.sorted { (first, second) -> Bool in
|
||||
return first.name < second.name
|
||||
}
|
||||
if results.isEmpty {
|
||||
return nil
|
||||
return super.getBudgets(count: count, page: page).map { (budgets: [Budget]) in
|
||||
self.addBudgets(budgets)
|
||||
return budgets
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
return Result.Publisher(.success(results.slice(count: count, page: page))).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func getBudget(_ id: String) -> AnyPublisher<Budget, NetworkError>? {
|
||||
override func getBudget(_ id: String) -> AnyPublisher<Budget, NetworkError> {
|
||||
guard let budget = budgets.first(where: { $0.id == id }) else {
|
||||
return nil
|
||||
return super.getBudget(id).map { budget in
|
||||
self.addBudget(budget)
|
||||
return budget
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
return Result.Publisher(.success(budget)).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func getBudgetBalance(_ id: String) -> AnyPublisher<Int, NetworkError>? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func addBudgets(_ budgets: [Budget]) {
|
||||
budgets.forEach { addBudget($0) }
|
||||
}
|
||||
|
@ -44,24 +46,8 @@ class TwigsInMemoryCacheService {
|
|||
self.budgets.insert(budget)
|
||||
}
|
||||
|
||||
// MARK: Transactions
|
||||
func getTransactions(
|
||||
budgetIds: [Int]? = nil,
|
||||
categoryIds: [Int]? = nil,
|
||||
from: Date? = nil,
|
||||
to: Date? = nil,
|
||||
count: Int? = nil,
|
||||
page: Int? = nil
|
||||
) -> AnyPublisher<[Transaction], NetworkError>? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTransaction(_ id: String) -> AnyPublisher<Transaction, NetworkError>? {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: Categories
|
||||
func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = nil, count: Int? = nil, page: Int? = nil) -> AnyPublisher<[Category], NetworkError>? {
|
||||
override func getCategories(budgetId: String? = nil, expense: Bool? = nil, archived: Bool? = nil, count: Int? = nil, page: Int? = nil) -> AnyPublisher<[Category], NetworkError> {
|
||||
var results = categories
|
||||
if budgetId != nil {
|
||||
results = categories.filter { $0.budgetId == budgetId }
|
||||
|
@ -73,23 +59,25 @@ class TwigsInMemoryCacheService {
|
|||
results = results.filter { $0.archived == archived }
|
||||
}
|
||||
if results.isEmpty {
|
||||
return nil
|
||||
return super.getCategories(budgetId: budgetId, expense: expense, archived: archived, count: count, page: page).map { (categories: [Category]) in
|
||||
self.addCategories(categories)
|
||||
return categories
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
let sortedResults = results.sorted { $0.title < $1.title }
|
||||
return Result.Publisher(.success(sortedResults.slice(count: count, page: page))).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func getCategory(_ id: String) -> AnyPublisher<Category, NetworkError>? {
|
||||
override func getCategory(_ id: String) -> AnyPublisher<Category, NetworkError> {
|
||||
guard let category = categories.first(where: { $0.id == id }) else {
|
||||
return nil
|
||||
return super.getCategory(id).map { category in
|
||||
self.addCategory(category)
|
||||
return category
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
return Result.Publisher(.success(category)).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func getCategoryBalance(_ id: String) -> AnyPublisher<Int, NetworkError>? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func addCategories(_ categories: [Category]) {
|
||||
categories.forEach { addCategory($0) }
|
||||
}
|
||||
|
@ -98,11 +86,26 @@ class TwigsInMemoryCacheService {
|
|||
self.categories.insert(category)
|
||||
}
|
||||
|
||||
func updateCategory(_ category: Category) {
|
||||
if let index = self.categories.firstIndex(where: { $0.id == category.id }) {
|
||||
self.categories.remove(at: index)
|
||||
}
|
||||
self.categories.insert(category)
|
||||
override func createCategory(_ category: Category) -> AnyPublisher<Category, NetworkError> {
|
||||
return super.createCategory(category).map {
|
||||
self.categories.insert(category)
|
||||
return $0
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
override func updateCategory(_ category: Category) -> AnyPublisher<Category, NetworkError> {
|
||||
return super.updateCategory(category).map {
|
||||
self.removeCategory(category.id)
|
||||
self.categories.insert(category)
|
||||
return $0
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
override func deleteCategory(_ id: String) -> AnyPublisher<Empty, NetworkError> {
|
||||
return super.deleteCategory(id).map {
|
||||
self.removeCategory(id)
|
||||
return $0
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func removeCategory(_ id: String) {
|
||||
|
@ -110,15 +113,6 @@ class TwigsInMemoryCacheService {
|
|||
self.categories.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Users
|
||||
func getUser(id: String) -> AnyPublisher<User, NetworkError>? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUsers(count: Int? = nil, page: Int? = nil) -> AnyPublisher<[User], NetworkError>? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,14 +10,18 @@ import SwiftUI
|
|||
|
||||
struct SidebarBudgetView: View {
|
||||
@EnvironmentObject var authenticationDataStore: AuthenticationDataStore
|
||||
@EnvironmentObject var budgetDataStore: BudgetsDataStore
|
||||
@EnvironmentObject var categoryDataStore: CategoryDataStore
|
||||
@StateObject var budgetDataStore: BudgetsDataStore
|
||||
let apiService: TwigsApiService
|
||||
@State var isSelectingBudget = true
|
||||
@State var hasSelectedBudget = false
|
||||
@State var isAddingTransaction = false
|
||||
@State var tabSelection: Int? = 0
|
||||
|
||||
init(_ apiService: TwigsApiService) {
|
||||
self.apiService = apiService
|
||||
self._budgetDataStore = StateObject(wrappedValue: BudgetsDataStore(budgetRepository: apiService, categoryRepository: apiService, transactionRepository: apiService))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var mainView: some View {
|
||||
if case let .success(budget) = budgetDataStore.budget {
|
||||
|
@ -26,32 +30,36 @@ struct SidebarBudgetView: View {
|
|||
NavigationLink(
|
||||
tag: 0,
|
||||
selection: $tabSelection,
|
||||
destination: { BudgetDetailsView(budget: budget).navigationBarTitle("overview") },
|
||||
label: { Label("overview", systemImage: "chart.line.uptrend.xyaxis.circle.fill") }
|
||||
destination: { BudgetDetailsView(budget: budget).navigationBarTitle("overview")
|
||||
},
|
||||
label: { Label("overview", systemImage: "chart.line.uptrend.xyaxis") }
|
||||
)
|
||||
.keyboardShortcut("1")
|
||||
.keyboardShortcut("1")
|
||||
NavigationLink(
|
||||
tag: 1,
|
||||
selection: $tabSelection,
|
||||
destination: { TransactionListView(budget).navigationBarTitle("transactions") },
|
||||
label: { Label("transactions", systemImage: "dollarsign.circle.fill") })
|
||||
.keyboardShortcut("2")
|
||||
label: { Label("transactions", systemImage: "dollarsign.circle") })
|
||||
.keyboardShortcut("2")
|
||||
NavigationLink(
|
||||
tag: 2,
|
||||
selection: $tabSelection,
|
||||
destination: { CategoryListView(budget).navigationBarTitle("categories") },
|
||||
label: { Label("categories", systemImage: "chart.pie.fill") })
|
||||
.keyboardShortcut("3")
|
||||
label: { Label("categories", systemImage: "chart.pie") })
|
||||
.keyboardShortcut("3")
|
||||
NavigationLink(
|
||||
tag: 3,
|
||||
selection: $tabSelection,
|
||||
destination: { RecurringTransactionsListView(dataStore: RecurringTransactionDataStore(apiService, budgetId: budget.id)).navigationBarTitle("recurring_transactions") },
|
||||
label: { Label("recurring_transactions", systemImage: "arrow.triangle.2.circlepath.circle.fill") })
|
||||
.keyboardShortcut("4")
|
||||
label: { Label("recurring_transactions", systemImage: "arrow.triangle.2.circlepath") })
|
||||
.keyboardShortcut("4")
|
||||
BudgetListsView()
|
||||
}
|
||||
.navigationTitle(budget.name)
|
||||
}
|
||||
}.environmentObject(TransactionDataStore(apiService))
|
||||
.environmentObject(CategoryDataStore(apiService))
|
||||
.environmentObject(budgetDataStore)
|
||||
.environmentObject(UserDataStore(apiService))
|
||||
} else {
|
||||
ActivityIndicator(isAnimating: .constant(true), style: .large)
|
||||
}
|
||||
|
@ -60,17 +68,17 @@ struct SidebarBudgetView: View {
|
|||
@ViewBuilder
|
||||
var body: some View {
|
||||
mainView
|
||||
.sheet(isPresented: $authenticationDataStore.showLogin,
|
||||
onDismiss: {
|
||||
self.budgetDataStore.getBudgets()
|
||||
},
|
||||
content: {
|
||||
LoginView()
|
||||
.environmentObject(authenticationDataStore)
|
||||
.onDisappear {
|
||||
self.budgetDataStore.getBudgets()
|
||||
}
|
||||
})
|
||||
.sheet(isPresented: $authenticationDataStore.showLogin,
|
||||
onDismiss: {
|
||||
self.budgetDataStore.getBudgets()
|
||||
},
|
||||
content: {
|
||||
LoginView()
|
||||
.environmentObject(authenticationDataStore)
|
||||
.onDisappear {
|
||||
self.budgetDataStore.getBudgets()
|
||||
}
|
||||
})
|
||||
.interactiveDismissDisabled(true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,14 +10,18 @@ import SwiftUI
|
|||
|
||||
struct TabbedBudgetView: View {
|
||||
@EnvironmentObject var authenticationDataStore: AuthenticationDataStore
|
||||
@EnvironmentObject var budgetDataStore: BudgetsDataStore
|
||||
@EnvironmentObject var categoryDataStore: CategoryDataStore
|
||||
@StateObject var budgetDataStore: BudgetsDataStore
|
||||
let apiService: TwigsApiService
|
||||
@State var isSelectingBudget = true
|
||||
@State var hasSelectedBudget = false
|
||||
@State var isAddingTransaction = false
|
||||
@State var tabSelection: Int = 0
|
||||
|
||||
init(_ apiService: TwigsApiService) {
|
||||
self.apiService = apiService
|
||||
self._budgetDataStore = StateObject(wrappedValue: BudgetsDataStore(budgetRepository: apiService, categoryRepository: apiService, transactionRepository: apiService))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var mainView: some View {
|
||||
if case let .success(budget) = budgetDataStore.budget {
|
||||
|
@ -75,7 +79,10 @@ struct TabbedBudgetView: View {
|
|||
}
|
||||
.tag(3)
|
||||
.keyboardShortcut("4")
|
||||
}
|
||||
}.environmentObject(TransactionDataStore(apiService))
|
||||
.environmentObject(CategoryDataStore(apiService))
|
||||
.environmentObject(budgetDataStore)
|
||||
.environmentObject(UserDataStore(apiService))
|
||||
} else {
|
||||
ActivityIndicator(isAnimating: .constant(true), style: .large)
|
||||
}
|
||||
|
|
|
@ -14,9 +14,7 @@ class TransactionDataStore: ObservableObject {
|
|||
private var currentRequest: AnyCancellable? = nil
|
||||
private var sumRequests: [String:AnyCancellable] = [:]
|
||||
@Published var transactions: [String:Result<OrderedDictionary<String, [Transaction]>, NetworkError>] = ["": .failure(.loading)]
|
||||
|
||||
@Published var transaction: Result<Transaction, NetworkError> = .failure(.unknown)
|
||||
|
||||
@Published var sums: [String:Result<BalanceResponse, NetworkError>] = ["": .failure(.loading)]
|
||||
|
||||
func getTransactions(_ budgetId: String, categoryId: String? = nil, from: Date? = nil, count: Int? = nil, page: Int? = nil) -> String {
|
||||
|
@ -31,6 +29,7 @@ class TransactionDataStore: ObservableObject {
|
|||
budgetIds: [budgetId],
|
||||
categoryIds: categoryIds,
|
||||
from: from ?? Date.firstOfMonth,
|
||||
to: nil,
|
||||
count: count,
|
||||
page: page
|
||||
)
|
||||
|
|
|
@ -10,7 +10,7 @@ import Foundation
|
|||
import Combine
|
||||
|
||||
protocol TransactionRepository {
|
||||
func getTransactions(budgetIds: [String], categoryIds: [String]?, from: Date?, count: Int?, page: Int?) -> AnyPublisher<[Transaction], NetworkError>
|
||||
func getTransactions(budgetIds: [String], categoryIds: [String]?, from: Date?, to: Date?, count: Int?, page: Int?) -> AnyPublisher<[Transaction], NetworkError>
|
||||
func getTransaction(_ transactionId: String) -> AnyPublisher<Transaction, NetworkError>
|
||||
func createTransaction(_ transaction: Transaction) -> AnyPublisher<Transaction, NetworkError>
|
||||
func updateTransaction(_ transaction: Transaction) -> AnyPublisher<Transaction, NetworkError>
|
||||
|
@ -18,38 +18,6 @@ protocol TransactionRepository {
|
|||
func sumTransactions(budgetId: String?, categoryId: String?, from: Date?, to: Date?) -> AnyPublisher<BalanceResponse, NetworkError>
|
||||
}
|
||||
|
||||
class NetworkTransactionRepository: TransactionRepository {
|
||||
let apiService: TwigsApiService
|
||||
|
||||
init(_ apiService: TwigsApiService) {
|
||||
self.apiService = apiService
|
||||
}
|
||||
|
||||
func getTransactions(budgetIds: [String], categoryIds: [String]?, from: Date?, count: Int?, page: Int?) -> AnyPublisher<[Transaction], NetworkError> {
|
||||
return apiService.getTransactions(budgetIds: budgetIds, categoryIds: categoryIds, from: from, count: count, page: page)
|
||||
}
|
||||
|
||||
func getTransaction(_ transactionId: String) -> AnyPublisher<Transaction, NetworkError> {
|
||||
return apiService.getTransaction(transactionId)
|
||||
}
|
||||
|
||||
func createTransaction(_ transaction: Transaction) -> AnyPublisher<Transaction, NetworkError> {
|
||||
return apiService.newTransaction(transaction)
|
||||
}
|
||||
|
||||
func updateTransaction(_ transaction: Transaction) -> AnyPublisher<Transaction, NetworkError> {
|
||||
return apiService.updateTransaction(transaction)
|
||||
}
|
||||
|
||||
func deleteTransaction(_ transactionId: String) -> AnyPublisher<Empty, NetworkError> {
|
||||
return apiService.deleteTransaction(transactionId)
|
||||
}
|
||||
|
||||
func sumTransactions(budgetId: String?, categoryId: String?, from: Date?, to: Date?) -> AnyPublisher<BalanceResponse, NetworkError> {
|
||||
return apiService.sumTransactions(budgetId: budgetId, categoryId: categoryId, from: from, to: to)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
class MockTransactionRepository: TransactionRepository {
|
||||
static let transaction: Transaction = Transaction(
|
||||
|
@ -64,7 +32,7 @@ class MockTransactionRepository: TransactionRepository {
|
|||
budgetId: MockBudgetRepository.budget.id
|
||||
)
|
||||
|
||||
func getTransactions(budgetIds: [String], categoryIds: [String]?, from: Date?, count: Int?, page: Int?) -> AnyPublisher<[Transaction], NetworkError> {
|
||||
func getTransactions(budgetIds: [String], categoryIds: [String]?, from: Date?, to: Date?, count: Int?, page: Int?) -> AnyPublisher<[Transaction], NetworkError> {
|
||||
return Result.Publisher([MockTransactionRepository.transaction]).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
|
|
@ -10,46 +10,28 @@ import SwiftUI
|
|||
|
||||
@main
|
||||
struct TwigsApp: App {
|
||||
let requestHelper = RequestHelper()
|
||||
let cacheService = TwigsInMemoryCacheService()
|
||||
let apiService: TwigsApiService
|
||||
let budgetRepository: BudgetRepository
|
||||
let categoryRepository: CategoryRepository
|
||||
let transactionRepository:TransactionRepository
|
||||
let userRepository: UserRepository
|
||||
let dataStoreProvider: DataStoreProvider
|
||||
@StateObject var authDataStore: AuthenticationDataStore
|
||||
let apiService: TwigsApiService = TwigsInMemoryCacheService()
|
||||
|
||||
init() {
|
||||
self.apiService = TwigsApiService(requestHelper)
|
||||
self.budgetRepository = NetworkBudgetRepository(apiService, cacheService: cacheService)
|
||||
self.categoryRepository = NetworkCategoryRepository(apiService, cacheService: cacheService)
|
||||
self.transactionRepository = NetworkTransactionRepository(apiService)
|
||||
self.userRepository = NetworkUserRepository(apiService)
|
||||
self.dataStoreProvider = DataStoreProvider(
|
||||
budgetRepository: budgetRepository,
|
||||
categoryRepository: categoryRepository,
|
||||
transactionRepository: transactionRepository,
|
||||
userRepository: userRepository
|
||||
)
|
||||
let authDataStore = AuthenticationDataStore(self.apiService)
|
||||
self._authDataStore = StateObject(wrappedValue: authDataStore)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var mainView: some View {
|
||||
if UIDevice.current.userInterfaceIdiom == .mac || UIDevice.current.userInterfaceIdiom == .pad {
|
||||
SidebarBudgetView(apiService: apiService)
|
||||
SidebarBudgetView(apiService)
|
||||
.environmentObject(authDataStore)
|
||||
} else {
|
||||
TabbedBudgetView(apiService: apiService)
|
||||
TabbedBudgetView(apiService)
|
||||
.environmentObject(authDataStore)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
mainView
|
||||
.environmentObject(dataStoreProvider.authenticationDataStore())
|
||||
.environmentObject(dataStoreProvider.budgetsDataStore())
|
||||
.environmentObject(dataStoreProvider.categoryDataStore())
|
||||
.environmentObject(dataStoreProvider.transactionDataStore())
|
||||
.environmentObject(dataStoreProvider.userDataStore())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,42 +18,6 @@ protocol UserRepository {
|
|||
func register(username: String, email: String, password: String) -> AnyPublisher<User, NetworkError>
|
||||
}
|
||||
|
||||
class NetworkUserRepository: UserRepository {
|
||||
let apiService: TwigsApiService
|
||||
|
||||
init(_ apiService: TwigsApiService) {
|
||||
self.apiService = apiService
|
||||
}
|
||||
|
||||
func setToken(_ token: String) {
|
||||
self.apiService.requestHelper.token = token
|
||||
}
|
||||
|
||||
func getUser(_ id: String) -> AnyPublisher<User, NetworkError> {
|
||||
return apiService.getUser(id: id)
|
||||
}
|
||||
|
||||
func searchUsers(_ withUsername: String) -> AnyPublisher<[User], NetworkError> {
|
||||
return apiService.searchUsers(query: withUsername)
|
||||
}
|
||||
|
||||
func setServer(_ server: String) {
|
||||
var correctServer = server
|
||||
if !server.starts(with: "http://") && !server.starts(with: "https://") {
|
||||
correctServer = "http://\(correctServer)"
|
||||
}
|
||||
apiService.requestHelper.baseUrl = correctServer
|
||||
}
|
||||
|
||||
func login(username: String, password: String) -> AnyPublisher<LoginResponse, NetworkError> {
|
||||
return apiService.login(username: username, password: password)
|
||||
}
|
||||
|
||||
func register(username: String, email: String, password: String) -> AnyPublisher<User, NetworkError> {
|
||||
return apiService.register(username: username, email: email, password: password)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
class MockUserRepository: UserRepository {
|
||||
|
|
Loading…
Reference in a new issue