WIP: Add budgets screen
Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
parent
1053fa91f8
commit
56488bd265
16 changed files with 317 additions and 54 deletions
|
@ -9,6 +9,9 @@
|
|||
/* Begin PBXBuildFile section */
|
||||
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 /* BudgetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2841022F2342D97300EAFA29 /* BudgetsView.swift */; };
|
||||
284102322342E12F00EAFA29 /* CategoriesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284102312342E12F00EAFA29 /* CategoriesView.swift */; };
|
||||
2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2857EAEC233DA30B0026BC83 /* LoadingView.swift */; };
|
||||
28AC94EE233C373900BFB70A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC94ED233C373900BFB70A /* AppDelegate.swift */; };
|
||||
28AC94F0233C373900BFB70A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC94EF233C373900BFB70A /* SceneDelegate.swift */; };
|
||||
|
@ -23,6 +26,8 @@
|
|||
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC9528233C433400BFB70A /* TransactionRepository.swift */; };
|
||||
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC952B233C434800BFB70A /* UserRepository.swift */; };
|
||||
28AC952E233C43A300BFB70A /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC952D233C43A300BFB70A /* User.swift */; };
|
||||
28FE6AF42342E3CB00D5543E /* BudgetsDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF32342E3CB00D5543E /* BudgetsDataStore.swift */; };
|
||||
28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */; };
|
||||
543ECE42233E82A40018A9D9 /* UserDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543ECE41233E82A40018A9D9 /* UserDataStore.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -46,6 +51,9 @@
|
|||
/* Begin PBXFileReference section */
|
||||
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 /* BudgetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetsView.swift; sourceTree = "<group>"; };
|
||||
284102312342E12F00EAFA29 /* CategoriesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoriesView.swift; sourceTree = "<group>"; };
|
||||
2857EAEC233DA30B0026BC83 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
|
||||
28AC94EA233C373900BFB70A /* Budget.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Budget.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
28AC94ED233C373900BFB70A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
|
@ -68,6 +76,8 @@
|
|||
28AC9528233C433400BFB70A /* TransactionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionRepository.swift; sourceTree = "<group>"; };
|
||||
28AC952B233C434800BFB70A /* UserRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepository.swift; sourceTree = "<group>"; };
|
||||
28AC952D233C43A300BFB70A /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.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>"; };
|
||||
543ECE41233E82A40018A9D9 /* UserDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataStore.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -96,6 +106,25 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
284102292342D8BB00EAFA29 /* Budget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2841022F2342D97300EAFA29 /* BudgetsView.swift */,
|
||||
2841022B2342D8E400EAFA29 /* Budget.swift */,
|
||||
28FE6AF32342E3CB00D5543E /* BudgetsDataStore.swift */,
|
||||
28FE6AF52342E4CC00D5543E /* BudgetRepository.swift */,
|
||||
);
|
||||
path = Budget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2841022A2342D8CB00EAFA29 /* Category */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
284102312342E12F00EAFA29 /* CategoriesView.swift */,
|
||||
);
|
||||
path = Category;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2857EAEB233DA2F90026BC83 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -127,6 +156,8 @@
|
|||
28AC94EC233C373900BFB70A /* Budget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2841022A2342D8CB00EAFA29 /* Category */,
|
||||
284102292342D8BB00EAFA29 /* Budget */,
|
||||
2857EAEB233DA2F90026BC83 /* Views */,
|
||||
28AC952A233C433C00BFB70A /* User */,
|
||||
28AC9527233C430A00BFB70A /* Network */,
|
||||
|
@ -332,14 +363,19 @@
|
|||
files = (
|
||||
28AC94EE233C373900BFB70A /* AppDelegate.swift in Sources */,
|
||||
28AC94F0233C373900BFB70A /* SceneDelegate.swift in Sources */,
|
||||
2841022C2342D8E400EAFA29 /* Budget.swift in Sources */,
|
||||
2841022723419A2B00EAFA29 /* TabbedBudgetView.swift in Sources */,
|
||||
28FE6AF42342E3CB00D5543E /* BudgetsDataStore.swift in Sources */,
|
||||
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */,
|
||||
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */,
|
||||
28AC9525233C42D100BFB70A /* BudgetApiService.swift in Sources */,
|
||||
2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */,
|
||||
284102322342E12F00EAFA29 /* CategoriesView.swift in Sources */,
|
||||
284102252341998300EAFA29 /* ContentView.swift in Sources */,
|
||||
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */,
|
||||
28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */,
|
||||
543ECE42233E82A40018A9D9 /* UserDataStore.swift in Sources */,
|
||||
284102302342D97300EAFA29 /* BudgetsView.swift in Sources */,
|
||||
28AC952E233C43A300BFB70A /* User.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "116"
|
||||
endingLineNumber = "116"
|
||||
landmarkName = "buildRequest(endPoint:method:data:)"
|
||||
landmarkName = "post(endPoint:data:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
|
@ -32,7 +32,7 @@
|
|||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "36"
|
||||
endingLineNumber = "36"
|
||||
landmarkName = "login(username:password:)"
|
||||
landmarkName = "init(_:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
|
|
16
Budget/Budget/Budget.swift
Normal file
16
Budget/Budget/Budget.swift
Normal file
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Budget.swift
|
||||
// Budget
|
||||
//
|
||||
// Created by Billy Brawner on 9/30/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Budget: Identifiable, Codable {
|
||||
let id: Int?
|
||||
let name: String
|
||||
let description: String?
|
||||
let users: [User]
|
||||
}
|
22
Budget/Budget/BudgetRepository.swift
Normal file
22
Budget/Budget/BudgetRepository.swift
Normal file
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// BudgetRepository.swift
|
||||
// Budget
|
||||
//
|
||||
// Created by Billy Brawner on 9/30/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class BudgetRepository {
|
||||
let apiService: BudgetApiService
|
||||
|
||||
init(_ apiService: BudgetApiService) {
|
||||
self.apiService = apiService
|
||||
}
|
||||
|
||||
func getBudgets() -> AnyPublisher<[Budget], NetworkError> {
|
||||
return apiService.getBudgets()
|
||||
}
|
||||
}
|
44
Budget/Budget/BudgetsDataStore.swift
Normal file
44
Budget/Budget/BudgetsDataStore.swift
Normal file
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// BudgetsDataStore.swift
|
||||
// Budget
|
||||
//
|
||||
// Created by Billy Brawner on 9/30/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class BudgetsDataStore: ObservableObject {
|
||||
var budgets: Result<[Budget], NetworkError> = .failure(.loading) {
|
||||
didSet {
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
func getBudgets() {
|
||||
self.budgets = .failure(.loading)
|
||||
|
||||
_ = self.budgetRepository.getBudgets()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { (status) in
|
||||
switch status {
|
||||
case .finished:
|
||||
return
|
||||
case .failure(let error):
|
||||
self.budgets = .failure(error)
|
||||
return
|
||||
}
|
||||
}, receiveValue: { (budgets) in
|
||||
self.budgets = .success(budgets)
|
||||
})
|
||||
}
|
||||
|
||||
init(_ budgetRepository: BudgetRepository) {
|
||||
self.budgetRepository = budgetRepository
|
||||
}
|
||||
|
||||
private let budgetRepository: BudgetRepository
|
||||
// Needed since the default implementation is currently broken
|
||||
let objectWillChange = ObservableObjectPublisher()
|
||||
}
|
68
Budget/Budget/BudgetsView.swift
Normal file
68
Budget/Budget/BudgetsView.swift
Normal file
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// BudgetsView.swift
|
||||
// Budget
|
||||
//
|
||||
// Created by Billy Brawner on 9/30/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct BudgetsView: View {
|
||||
@ObservedObject var budgetsDataStore: BudgetsDataStore
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
stateContent.navigationBarTitle("budgets")
|
||||
}
|
||||
}
|
||||
|
||||
var stateContent: AnyView {
|
||||
switch budgetsDataStore.budgets {
|
||||
case .success(let budgets):
|
||||
return AnyView(List(budgets) { budget in
|
||||
BudgetListItemView(budget)
|
||||
})
|
||||
case .failure(.loading):
|
||||
return AnyView(VStack {
|
||||
ActivityIndicator(isAnimating: .constant(true), style: .large)
|
||||
})
|
||||
default:
|
||||
// TODO: Handle each network failure type
|
||||
return AnyView(Text("budgets_load_failure"))
|
||||
}
|
||||
}
|
||||
|
||||
init(_ budgetsDataStore: BudgetsDataStore) {
|
||||
self.budgetsDataStore = budgetsDataStore
|
||||
self.budgetsDataStore.getBudgets()
|
||||
}
|
||||
}
|
||||
|
||||
struct BudgetListItemView: View {
|
||||
var budget: Budget
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(
|
||||
destination: CategoriesView()
|
||||
.navigationBarTitle(budget.name)
|
||||
) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(verbatim: budget.name)
|
||||
Text(verbatim: budget.description ?? "")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init (_ budget: Budget) {
|
||||
self.budget = budget
|
||||
}
|
||||
}
|
||||
|
||||
//struct BudgetsView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// BudgetsView(budgets: [])
|
||||
// }
|
||||
//}
|
21
Budget/Category/CategoriesView.swift
Normal file
21
Budget/Category/CategoriesView.swift
Normal file
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// CategoriesView.swift
|
||||
// Budget
|
||||
//
|
||||
// Created by Billy Brawner on 9/30/19.
|
||||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CategoriesView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
struct CategoriesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CategoriesView()
|
||||
}
|
||||
}
|
|
@ -13,17 +13,28 @@ struct ContentView: View {
|
|||
|
||||
var body: some View {
|
||||
Group {
|
||||
if userData.status == .authenticated {
|
||||
TabbedBudgetView(userData)
|
||||
} else {
|
||||
if showLogin() {
|
||||
LoginView(userData)
|
||||
} else {
|
||||
TabbedBudgetView(userData, budgetRepository: budgetRepository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showLogin() -> Bool {
|
||||
switch userData.currentUser {
|
||||
case .failure:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init (_ userData: UserDataStore) {
|
||||
private let budgetRepository: BudgetRepository
|
||||
|
||||
init (_ userData: UserDataStore, budgetRepository: BudgetRepository) {
|
||||
self.userData = userData
|
||||
self.budgetRepository = budgetRepository
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,10 +13,11 @@ struct LoginView: View {
|
|||
@State var username: String = ""
|
||||
@State var password: String = ""
|
||||
@ObservedObject var userData: UserDataStore
|
||||
let showLoader: Bool
|
||||
|
||||
var body: some View {
|
||||
LoadingView(
|
||||
isShowing: .constant(userData.status == UserStatus.authenticating),
|
||||
isShowing: .constant(showLoader),
|
||||
loadingText: "loading_login"
|
||||
) {
|
||||
VStack {
|
||||
|
@ -33,9 +34,13 @@ struct LoginView: View {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
init (_ userData: UserDataStore) {
|
||||
self.userData = userData
|
||||
if case userData.currentUser = Result<User, UserStatus>.failure(UserStatus.authenticating) {
|
||||
self.showLoader = true
|
||||
} else {
|
||||
self.showLoader = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,25 +12,60 @@ import Combine
|
|||
class BudgetApiService {
|
||||
let requestHelper: RequestHelper
|
||||
|
||||
init(requestHelper: RequestHelper) {
|
||||
init(_ requestHelper: RequestHelper) {
|
||||
self.requestHelper = requestHelper
|
||||
}
|
||||
|
||||
// MARK: Budgets
|
||||
|
||||
func getBudgets(count: Int? = nil, page: Int? = nil) -> AnyPublisher<[Budget], NetworkError> {
|
||||
var queries = [String: Array<String>]()
|
||||
if count != nil {
|
||||
queries["count"] = [String(count!)]
|
||||
}
|
||||
if (page != nil) {
|
||||
queries["page"] = [String(page!)]
|
||||
}
|
||||
return requestHelper.get("/budgets", queries: queries)
|
||||
}
|
||||
|
||||
func getBudget(_ id: Int) -> AnyPublisher<Budget, NetworkError> {
|
||||
return requestHelper.get("/budgets/\(id)")
|
||||
}
|
||||
|
||||
func getBudgetBalance(_ id: Int) -> AnyPublisher<Int, NetworkError> {
|
||||
return requestHelper.get("/budgets/\(id)/balance")
|
||||
}
|
||||
|
||||
func newBudget(_ budget: Budget) -> AnyPublisher<Budget, NetworkError> {
|
||||
return requestHelper.post("/budgets/new", data: budget)
|
||||
}
|
||||
|
||||
func updateBudget(_ budget: Budget) -> AnyPublisher<Budget, NetworkError> {
|
||||
return requestHelper.put("/budgets/\(budget.id!)", data: budget)
|
||||
}
|
||||
|
||||
// TODO: Figure out how to implement this
|
||||
// func deleteBudget(_ id: Int) -> AnyPublisher<Void, NetworkError> {
|
||||
// return requestHelper.delete("/budgets/\(id)")
|
||||
// }
|
||||
|
||||
// MARK: Users
|
||||
func login(username: String, password: String) -> AnyPublisher<User, NetworkError> {
|
||||
requestHelper.credentials = (username, password)
|
||||
return requestHelper.post(
|
||||
endPoint: "/users/login",
|
||||
"/users/login",
|
||||
data: LoginRequest(username: username, password: password)
|
||||
)
|
||||
}
|
||||
|
||||
func getUser(id: Int) -> AnyPublisher<User, NetworkError> {
|
||||
return requestHelper.get(endPoint: "/users/\(id)")
|
||||
return requestHelper.get("/users/\(id)")
|
||||
}
|
||||
|
||||
func searchUsers(query: String) -> AnyPublisher<[User], NetworkError> {
|
||||
return requestHelper.get(
|
||||
endPoint: "/users/search",
|
||||
"/users/search",
|
||||
queries: ["query": [query]]
|
||||
)
|
||||
}
|
||||
|
@ -50,7 +85,7 @@ class RequestHelper {
|
|||
}
|
||||
|
||||
func get<ResultType: Codable>(
|
||||
endPoint: String,
|
||||
_ endPoint: String,
|
||||
queries: [String: Array<String>]? = nil
|
||||
) -> AnyPublisher<ResultType, NetworkError> {
|
||||
var combinedEndPoint = endPoint
|
||||
|
@ -67,7 +102,7 @@ class RequestHelper {
|
|||
}
|
||||
|
||||
func post<ResultType: Codable>(
|
||||
endPoint: String,
|
||||
_ endPoint: String,
|
||||
data: Codable
|
||||
) -> AnyPublisher<ResultType, NetworkError> {
|
||||
return buildRequest(
|
||||
|
@ -78,7 +113,7 @@ class RequestHelper {
|
|||
}
|
||||
|
||||
func put<ResultType: Codable>(
|
||||
endPoint: String,
|
||||
_ endPoint: String,
|
||||
data: ResultType
|
||||
) -> AnyPublisher<ResultType, NetworkError> {
|
||||
return buildRequest(
|
||||
|
@ -88,7 +123,7 @@ class RequestHelper {
|
|||
)
|
||||
}
|
||||
|
||||
func delete<ResultType: Codable>(endPoint: String) -> AnyPublisher<ResultType, NetworkError> {
|
||||
func delete<ResultType: Codable>(_ endPoint: String) -> AnyPublisher<ResultType, NetworkError> {
|
||||
return buildRequest(endPoint: endPoint, method: "DELETE")
|
||||
}
|
||||
|
||||
|
@ -136,6 +171,7 @@ class RequestHelper {
|
|||
}
|
||||
|
||||
enum NetworkError: Error {
|
||||
case loading
|
||||
case unknown
|
||||
case notFound
|
||||
case unauthorized
|
||||
|
|
|
@ -13,7 +13,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
var window: UIWindow?
|
||||
|
||||
let userRepository: UserRepository
|
||||
|
||||
let budgetRepository: BudgetRepository
|
||||
|
||||
override init() {
|
||||
// TODO: Dependency injection?
|
||||
#if DEBUG
|
||||
|
@ -23,8 +24,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
let baseUrl = "https://budget-api.intra.wbrawner.com"
|
||||
#endif
|
||||
let requestHelper = RequestHelper(baseUrl: baseUrl)
|
||||
let apiService = BudgetApiService(requestHelper: requestHelper)
|
||||
let apiService = BudgetApiService(requestHelper)
|
||||
userRepository = UserRepository(apiService: apiService)
|
||||
budgetRepository = BudgetRepository(apiService)
|
||||
super.init()
|
||||
}
|
||||
|
||||
|
@ -34,7 +36,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||
|
||||
// Create the SwiftUI view that provides the window contents.
|
||||
let contentView = ContentView(UserDataStore(self.userRepository))
|
||||
let contentView = ContentView(
|
||||
UserDataStore(self.userRepository),
|
||||
budgetRepository: budgetRepository
|
||||
)
|
||||
// Use a UIHostingController as window root view controller.
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
|
|
|
@ -10,31 +10,28 @@ import SwiftUI
|
|||
|
||||
struct TabbedBudgetView: View {
|
||||
@ObservedObject var userData: UserDataStore
|
||||
let budgetRepository: BudgetRepository
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
TabView {
|
||||
Text("Transactions here")
|
||||
.tabItem {
|
||||
Image(systemName: "dollarsign.circle")
|
||||
Text("transactions")
|
||||
}
|
||||
Text("Budgets here")
|
||||
.tabItem {
|
||||
Image(systemName: "chart.pie.fill")
|
||||
Text("budgets")
|
||||
}
|
||||
Text("Profile here")
|
||||
.tabItem {
|
||||
Image(systemName: "person")
|
||||
Text("profile")
|
||||
}
|
||||
TabView {
|
||||
Text("Transactions here").tabItem {
|
||||
Image(systemName: "dollarsign.circle.fill")
|
||||
Text("transactions")
|
||||
}
|
||||
BudgetsView(BudgetsDataStore(budgetRepository)).tabItem {
|
||||
Image(systemName: "chart.pie.fill")
|
||||
Text("budgets")
|
||||
}
|
||||
Text("Profile here").tabItem {
|
||||
Image(systemName: "person.circle.fill")
|
||||
Text("profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init (_ userData: UserDataStore) {
|
||||
init (_ userData: UserDataStore, budgetRepository: BudgetRepository) {
|
||||
self.userData = userData
|
||||
self.budgetRepository = budgetRepository
|
||||
}
|
||||
}
|
||||
//
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct User: Codable {
|
||||
struct User: Codable, Equatable {
|
||||
let id: Int?
|
||||
let username: String
|
||||
let email: String?
|
||||
|
|
|
@ -3,16 +3,16 @@ import Combine
|
|||
|
||||
class UserDataStore: ObservableObject {
|
||||
|
||||
// Note: You can combine these into one Result type
|
||||
// Result<User, Status>
|
||||
var currentUser: User? = nil
|
||||
var status: UserStatus = .unauthenticated
|
||||
var currentUser: Result<User, UserStatus> = .failure(.unauthenticated) {
|
||||
didSet {
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
func login(username: String, password: String) {
|
||||
|
||||
// Changes the status and notifies any observers of the change
|
||||
self.status = .authenticating
|
||||
self.objectWillChange.send()
|
||||
self.currentUser = .failure(.authenticating)
|
||||
|
||||
// Perform the login
|
||||
_ = self.userRepository.login(username: username, password: password)
|
||||
|
@ -24,27 +24,23 @@ class UserDataStore: ObservableObject {
|
|||
// Do nothing it means the network request just ended
|
||||
case .failure( _):
|
||||
// Poulate your status with failed authenticating
|
||||
self.status = .failedAuthentication
|
||||
self.objectWillChange.send()
|
||||
self.currentUser = .failure(.failedAuthentication)
|
||||
}
|
||||
}) { (user) in
|
||||
self.currentUser = user
|
||||
if user.id != nil {
|
||||
self.status = .authenticated
|
||||
}
|
||||
self.objectWillChange.send()
|
||||
self.currentUser = .success(user)
|
||||
}
|
||||
}
|
||||
|
||||
init(_ userRepository: UserRepository) {
|
||||
self.userRepository = userRepository
|
||||
}
|
||||
|
||||
let objectWillChange = ObservableObjectPublisher() // needed since the default implementation is currently broken
|
||||
|
||||
// Needed since the default implementation is currently broken
|
||||
let objectWillChange = ObservableObjectPublisher()
|
||||
private let userRepository: UserRepository
|
||||
}
|
||||
|
||||
enum UserStatus {
|
||||
enum UserStatus: Error, Equatable {
|
||||
case unauthenticated
|
||||
case authenticating
|
||||
case failedAuthentication
|
||||
|
|
|
@ -19,3 +19,6 @@
|
|||
|
||||
// MARK: Budgets
|
||||
"budgets" = "Budgets";
|
||||
|
||||
// MARK: Profile
|
||||
"profile" = "Profile";
|
||||
|
|
|
@ -19,3 +19,6 @@
|
|||
|
||||
// MARK: Budgets
|
||||
"budgets" = "Presupuestos";
|
||||
|
||||
// MARK: Profile
|
||||
"profile" = "Perfil";
|
||||
|
|
Loading…
Reference in a new issue