Implement some caching and session persistence

Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
Billy Brawner 2019-10-19 15:18:06 -07:00
parent 9a26790cda
commit 84bb2e2176
20 changed files with 673 additions and 47 deletions

View file

@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
2821266023555FD300072D52 /* EditTransactionForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2821265F23555FD300072D52 /* EditTransactionForm.swift */; };
282126622357E45F00072D52 /* TransactionEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282126612357E45F00072D52 /* TransactionEditView.swift */; };
282126A1235929B800072D52 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282126A0235929B800072D52 /* ProfileView.swift */; };
282126A3235ABC1800072D52 /* BudgetAppInMemoryCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282126A2235ABC1800072D52 /* BudgetAppInMemoryCacheService.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 */; };
@ -27,7 +29,7 @@
28AC9505233C373A00BFB70A /* BudgetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC9504233C373A00BFB70A /* BudgetTests.swift */; };
28AC9510233C373A00BFB70A /* BudgetUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC950F233C373A00BFB70A /* BudgetUITests.swift */; };
28AC951F233C381C00BFB70A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 28AC9521233C381C00BFB70A /* Localizable.strings */; };
28AC9525233C42D100BFB70A /* BudgetApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC9524233C42D100BFB70A /* BudgetApiService.swift */; };
28AC9525233C42D100BFB70A /* BudgetAppApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28AC9524233C42D100BFB70A /* BudgetAppApiService.swift */; };
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 */; };
@ -66,6 +68,8 @@
/* Begin PBXFileReference section */
2821265F23555FD300072D52 /* EditTransactionForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTransactionForm.swift; sourceTree = "<group>"; };
282126612357E45F00072D52 /* TransactionEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionEditView.swift; sourceTree = "<group>"; };
282126A0235929B800072D52 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
282126A2235ABC1800072D52 /* BudgetAppInMemoryCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetAppInMemoryCacheService.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>"; };
@ -92,7 +96,7 @@
28AC9520233C381C00BFB70A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
28AC9522233C384C00BFB70A /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/LaunchScreen.strings"; sourceTree = "<group>"; };
28AC9523233C384C00BFB70A /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
28AC9524233C42D100BFB70A /* BudgetApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetApiService.swift; sourceTree = "<group>"; };
28AC9524233C42D100BFB70A /* BudgetAppApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetAppApiService.swift; sourceTree = "<group>"; };
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>"; };
@ -136,6 +140,14 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
2821269F2359299D00072D52 /* Profile */ = {
isa = PBXGroup;
children = (
282126A0235929B800072D52 /* ProfileView.swift */,
);
path = Profile;
sourceTree = "<group>";
};
284102292342D8BB00EAFA29 /* Budget */ = {
isa = PBXGroup;
children = (
@ -189,6 +201,7 @@
28AC94EC233C373900BFB70A /* BudgetApp */ = {
isa = PBXGroup;
children = (
2821269F2359299D00072D52 /* Profile */,
2841022A2342D8CB00EAFA29 /* Category */,
284102292342D8BB00EAFA29 /* Budget */,
2857EAEB233DA2F90026BC83 /* Views */,
@ -257,7 +270,8 @@
28AC9527233C430A00BFB70A /* Network */ = {
isa = PBXGroup;
children = (
28AC9524233C42D100BFB70A /* BudgetApiService.swift */,
28AC9524233C42D100BFB70A /* BudgetAppApiService.swift */,
282126A2235ABC1800072D52 /* BudgetAppInMemoryCacheService.swift */,
);
path = Network;
sourceTree = "<group>";
@ -420,9 +434,10 @@
28AC952C233C434800BFB70A /* UserRepository.swift in Sources */,
28FE6B04234449DC00D5543E /* TransactionListView.swift in Sources */,
28AC94F2233C373900BFB70A /* LoginView.swift in Sources */,
28AC9525233C42D100BFB70A /* BudgetApiService.swift in Sources */,
28AC9525233C42D100BFB70A /* BudgetAppApiService.swift in Sources */,
2888234723512DBF003D3847 /* Observable.swift in Sources */,
2857EAED233DA30B0026BC83 /* LoadingView.swift in Sources */,
282126A3235ABC1800072D52 /* BudgetAppInMemoryCacheService.swift in Sources */,
28FE6AFA23441E3700D5543E /* CategoryDataStore.swift in Sources */,
28B9E50E2346BCB2007C3909 /* RegistrationView.swift in Sources */,
284102322342E12F00EAFA29 /* CategoryListView.swift in Sources */,
@ -430,6 +445,7 @@
289510242352AAFC00BC862B /* UserDataStore.swift in Sources */,
28FE6B002344308600D5543E /* Transaction.swift in Sources */,
28FE6AF823441E1D00D5543E /* Category.swift in Sources */,
282126A1235929B800072D52 /* ProfileView.swift in Sources */,
28AC9529233C433400BFB70A /* TransactionRepository.swift in Sources */,
28FE6AF62342E4CC00D5543E /* BudgetRepository.swift in Sources */,
28FE6B022344331B00D5543E /* TransactionDataStore.swift in Sources */,

View file

@ -1409,5 +1409,332 @@
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "ECFFF901-C549-437A-A8A3-6C7AEF033822"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/Transaction/TransactionListView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "44"
endingLineNumber = "44"
landmarkName = "init(_:category:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "DB07923E-819C-4854-97A5-94A5E1790913"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/Category/CategoryRepository.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "33"
endingLineNumber = "33"
landmarkName = "getCategories(budgetId:count:page:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "93983EAA-5286-4CBE-928D-86988FB1D153"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/Category/CategoryDataStore.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "26"
endingLineNumber = "26"
landmarkName = "getCategories(budgetId:count:page:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "33A6CFC5-F78D-491F-A793-732352FDD1CB"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/Category/CategoryDataStore.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "39"
endingLineNumber = "39"
landmarkName = "getCategories(budgetId:count:page:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "CA398500-BEE8-4717-B727-FAA2C62EC711"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/Category/CategoryRepository.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "29"
endingLineNumber = "29"
landmarkName = "getCategories(budgetId:count:page:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.ExceptionBreakpoint">
<BreakpointContent
uuid = "152C6CF4-A03F-46DB-A690-08535EE452D6"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
scope = "0"
stopOnStyle = "0">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "C352446F-A182-4BAB-8C01-23DB336F2301"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/Network/BudgetAppInMemoryCacheService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "74"
endingLineNumber = "74"
landmarkName = "getCategories(budgetId:count:page:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "C00678FC-5D4C-4072-93FD-6D2C0E4D47E9"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/User/AuthenticationDataStore.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "37"
endingLineNumber = "37"
landmarkName = "login(username:password:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "0A8006D2-C24E-4AE5-8F7E-78ED5D1D6B7D"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/User/AuthenticationDataStore.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "88"
endingLineNumber = "88"
landmarkName = "init(_:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "F00DC3DF-E704-4B70-B744-57B4767E501D"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/User/AuthenticationDataStore.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "74"
endingLineNumber = "74"
landmarkName = "loadFromExistingSession()"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "C0F886DF-04BE-4C45-9A4B-17370122B446"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/User/AuthenticationDataStore.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "81"
endingLineNumber = "81"
landmarkName = "loadFromExistingSession()"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "3E49E2C2-F186-44D2-BA8C-DB27F0623DE1"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/Network/BudgetAppApiService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "260"
endingLineNumber = "260"
landmarkName = "delete(_:)"
landmarkType = "7">
<Locations>
<Location
uuid = "3E49E2C2-F186-44D2-BA8C-DB27F0623DE1 - 36db867e35bc722d"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (Foundation.Data, __C.NSURLResponse) throws -&gt; BudgetApp.Empty in BudgetApp.RequestHelper.delete(Swift.String) -&gt; Combine.AnyPublisher&lt;BudgetApp.Empty, BudgetApp.NetworkError&gt;"
moduleName = "BudgetApp"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/billy/Projects/Budget/BudgetApp/Network/BudgetAppApiService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "260"
endingLineNumber = "260"
offsetFromSymbolStart = "590">
</Location>
<Location
uuid = "3E49E2C2-F186-44D2-BA8C-DB27F0623DE1 - 36db867e35bc722d"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (Foundation.Data, __C.NSURLResponse) throws -&gt; BudgetApp.Empty in BudgetApp.RequestHelper.delete(Swift.String) -&gt; Combine.AnyPublisher&lt;BudgetApp.Empty, BudgetApp.NetworkError&gt;"
moduleName = "BudgetApp"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/billy/Projects/Budget/BudgetApp/Network/BudgetAppApiService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "260"
endingLineNumber = "260"
offsetFromSymbolStart = "871">
</Location>
<Location
uuid = "3E49E2C2-F186-44D2-BA8C-DB27F0623DE1 - 36db867e35bc722d"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (Foundation.Data, __C.NSURLResponse) throws -&gt; BudgetApp.Empty in BudgetApp.RequestHelper.delete(Swift.String) -&gt; Combine.AnyPublisher&lt;BudgetApp.Empty, BudgetApp.NetworkError&gt;"
moduleName = "BudgetApp"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/billy/Projects/Budget/BudgetApp/Network/BudgetAppApiService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "260"
endingLineNumber = "260"
offsetFromSymbolStart = "1042">
</Location>
<Location
uuid = "3E49E2C2-F186-44D2-BA8C-DB27F0623DE1 - 36db867e35bc722d"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (Foundation.Data, __C.NSURLResponse) throws -&gt; BudgetApp.Empty in BudgetApp.RequestHelper.delete(Swift.String) -&gt; Combine.AnyPublisher&lt;BudgetApp.Empty, BudgetApp.NetworkError&gt;"
moduleName = "BudgetApp"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/billy/Projects/Budget/BudgetApp/Network/BudgetAppApiService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "260"
endingLineNumber = "260"
offsetFromSymbolStart = "1118">
</Location>
<Location
uuid = "3E49E2C2-F186-44D2-BA8C-DB27F0623DE1 - 36db867e35bc722d"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (Foundation.Data, __C.NSURLResponse) throws -&gt; BudgetApp.Empty in BudgetApp.RequestHelper.delete(Swift.String) -&gt; Combine.AnyPublisher&lt;BudgetApp.Empty, BudgetApp.NetworkError&gt;"
moduleName = "BudgetApp"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/billy/Projects/Budget/BudgetApp/Network/BudgetAppApiService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "260"
endingLineNumber = "260"
offsetFromSymbolStart = "1269">
</Location>
</Locations>
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "DC8F6E88-A769-4ADC-8354-9BC089230827"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "BudgetApp/Network/BudgetAppApiService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "262"
endingLineNumber = "262"
landmarkName = "delete(_:)"
landmarkType = "7">
<Locations>
<Location
uuid = "DC8F6E88-A769-4ADC-8354-9BC089230827 - 36db867e35bc722d"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (Foundation.Data, __C.NSURLResponse) throws -&gt; BudgetApp.Empty in BudgetApp.RequestHelper.delete(Swift.String) -&gt; Combine.AnyPublisher&lt;BudgetApp.Empty, BudgetApp.NetworkError&gt;"
moduleName = "BudgetApp"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/billy/Projects/Budget/BudgetApp/Network/BudgetAppApiService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "262"
endingLineNumber = "262"
offsetFromSymbolStart = "1102">
</Location>
<Location
uuid = "DC8F6E88-A769-4ADC-8354-9BC089230827 - 36db867e35bc722d"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (Foundation.Data, __C.NSURLResponse) throws -&gt; BudgetApp.Empty in BudgetApp.RequestHelper.delete(Swift.String) -&gt; Combine.AnyPublisher&lt;BudgetApp.Empty, BudgetApp.NetworkError&gt;"
moduleName = "BudgetApp"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/billy/Projects/Budget/BudgetApp/Network/BudgetAppApiService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "262"
endingLineNumber = "262"
offsetFromSymbolStart = "1178">
</Location>
<Location
uuid = "DC8F6E88-A769-4ADC-8354-9BC089230827 - 36db867e35bc722d"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "closure #1 (Foundation.Data, __C.NSURLResponse) throws -&gt; BudgetApp.Empty in BudgetApp.RequestHelper.delete(Swift.String) -&gt; Combine.AnyPublisher&lt;BudgetApp.Empty, BudgetApp.NetworkError&gt;"
moduleName = "BudgetApp"
usesParentBreakpointCondition = "Yes"
urlString = "file:///Users/billy/Projects/Budget/BudgetApp/Network/BudgetAppApiService.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "262"
endingLineNumber = "262"
offsetFromSymbolStart = "1180">
</Location>
</Locations>
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

View file

@ -75,8 +75,10 @@ struct BudgetListItemView: View {
}
}
#if DEBUG
struct BudgetListsView_Previews: PreviewProvider {
static var previews: some View {
BudgetListsView(MockDataStoreProvider())
}
}
#endif

View file

@ -19,18 +19,33 @@ protocol BudgetRepository {
}
class NetworkBudgetRepository: BudgetRepository {
let apiService: BudgetApiService
let apiService: BudgetAppApiService
let cacheService: BudgetAppInMemoryCacheService?
init(_ apiService: BudgetApiService) {
init(_ apiService: BudgetAppApiService, cacheService: BudgetAppInMemoryCacheService? = nil) {
self.apiService = apiService
self.cacheService = cacheService
}
func getBudgets(count: Int?, page: Int?) -> AnyPublisher<[Budget], NetworkError> {
return apiService.getBudgets(count: count, page: page)
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: Int) -> AnyPublisher<Budget, NetworkError> {
return apiService.getBudget(id)
if let budget = cacheService?.getBudget(id) {
return budget
}
return apiService.getBudget(id).map { budget in
self.cacheService?.addBudget(budget)
return budget
}.eraseToAnyPublisher()
}
func getBudgetBalance(_ id: Int) -> AnyPublisher<Int, NetworkError> {
@ -54,10 +69,10 @@ class NetworkBudgetRepository: BudgetRepository {
class MockBudgetRepository: BudgetRepository {
static let budget = Budget(
id: 1,
name: "Test Budget",
description: "A mock budget used for testing",
users: []
id: 1,
name: "Test Budget",
description: "A mock budget used for testing",
users: []
)
func getBudgets(count: Int?, page: Int?) -> AnyPublisher<[Budget], NetworkError> {
@ -82,7 +97,7 @@ class MockBudgetRepository: BudgetRepository {
name: "Test Budget",
description: "A mock budget used for testing",
users: []
)).eraseToAnyPublisher()
)).eraseToAnyPublisher()
}
func deleteBudget(_ id: Int) -> AnyPublisher<Empty, NetworkError> {

View file

@ -35,6 +35,7 @@ class CategoryDataStore: ObservableObject {
self.categories = .failure(error)
}
}, receiveValue: { (categories) in
print("Received \(categories.count) categories")
self.categories = .success(categories)
})
}
@ -51,8 +52,8 @@ class CategoryDataStore: ObservableObject {
case .failure(let error):
self.category = .failure(error)
}
}, receiveValue: { (categories) in
self.category = .success(categories)
}, receiveValue: { (category) in
self.category = .success(category)
})
}

View file

@ -15,18 +15,37 @@ protocol CategoryRepository {
}
class NetworkCategoryRepository: CategoryRepository {
let apiService: BudgetApiService
init(_ apiService: BudgetApiService) {
let apiService: BudgetAppApiService
let cacheService: BudgetAppInMemoryCacheService?
init(_ apiService: BudgetAppApiService, cacheService: BudgetAppInMemoryCacheService? = nil) {
self.apiService = apiService
self.cacheService = cacheService
}
func getCategories(budgetId: Int?, count: Int?, page: Int?) -> AnyPublisher<[Category], NetworkError> {
return apiService.getCategories(budgetId: budgetId, count: count, page: page)
if let categories = cacheService?.getCategories(budgetId: budgetId, count: count, page: page) {
print("Returning categories from cache")
return categories
}
print("No cached categories, fetching from network")
return apiService.getCategories(budgetId: budgetId, count: count, page: page).map { (categories: [Category]) in
self.cacheService?.addCategories(categories)
return categories
}.eraseToAnyPublisher()
}
func getCategory(_ categoryId: Int) -> AnyPublisher<Category, NetworkError> {
return apiService.getCategory(categoryId)
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()
}
}

View file

@ -56,6 +56,11 @@ class DataStoreProvider {
#if DEBUG
class MockDataStoreProvider: DataStoreProvider {
override func authenticationDataStore() -> AuthenticationDataStore {
return MockAuthenticationDataStore(MockUserRepository())
}
init() {
super.init(
budgetRepository: MockBudgetRepository(),

View file

@ -9,7 +9,7 @@
import Foundation
import Combine
class BudgetApiService {
class BudgetAppApiService {
let requestHelper: RequestHelper
init(_ requestHelper: RequestHelper) {
@ -189,6 +189,10 @@ class BudgetApiService {
func deleteUser(_ user: User) -> AnyPublisher<Empty, NetworkError> {
return requestHelper.delete("/users/\(user.id!)")
}
func getProfile() -> AnyPublisher<User, NetworkError> {
return requestHelper.get("/users/me")
}
}
class RequestHelper {

View file

@ -0,0 +1,125 @@
//
// BudgetApiService.swift
// Budget
//
// Created by Billy Brawner on 9/25/19.
// Copyright © 2019 William Brawner. All rights reserved.
//
import Foundation
import Combine
class BudgetAppInMemoryCacheService {
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>? {
let results = budgets.sorted { (first, second) -> Bool in
return first.name < second.name
}
if results.isEmpty {
return nil
}
return Result.Publisher(.success(results.slice(count: count, page: page))).eraseToAnyPublisher()
}
func getBudget(_ id: Int) -> AnyPublisher<Budget, NetworkError>? {
guard let budget = budgets.first(where: { $0.id == id }) else {
return nil
}
return Result.Publisher(.success(budget)).eraseToAnyPublisher()
}
func getBudgetBalance(_ id: Int) -> AnyPublisher<Int, NetworkError>? {
return nil
}
func addBudgets(_ budgets: [Budget]) {
budgets.forEach { addBudget($0) }
}
func addBudget(_ budget: Budget) {
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: Int) -> AnyPublisher<Transaction, NetworkError>? {
return nil
}
// MARK: Categories
func getCategories(budgetId: Int? = nil, count: Int? = nil, page: Int? = nil) -> AnyPublisher<[Category], NetworkError>? {
var results = categories
if budgetId != nil {
results = categories.filter { $0.budgetId == budgetId }
}
if results.isEmpty {
return nil
}
let sortedResults = results.sorted { $0.title < $1.title }
// TODO: Figure out why this crashes on transaction editing screens
// return Result.Publisher(.success(sortedResults.slice(count: count, page: page))).eraseToAnyPublisher()
return nil
}
func getCategory(_ id: Int) -> AnyPublisher<Category, NetworkError>? {
guard let category = categories.first(where: { $0.id == id }) else {
return nil
}
return Result.Publisher(.success(category)).eraseToAnyPublisher()
}
func getCategoryBalance(_ id: Int) -> AnyPublisher<Int, NetworkError>? {
return nil
}
func addCategories(_ categories: [Category]) {
categories.forEach { addCategory($0) }
}
func addCategory(_ category: Category) {
self.categories.insert(category)
}
// MARK: Users
func getUser(id: Int) -> AnyPublisher<User, NetworkError>? {
return nil
}
func getUsers(count: Int? = nil, page: Int? = nil) -> AnyPublisher<[User], NetworkError>? {
return nil
}
}
/**
* Determines which slice of the array should be returned based on the count and page parameters
*/
private func calculateStartAndEndIndices(count: Int, page: Int?) -> (start: Int, end: Int?) {
let end = count * (page ?? 1)
let start = max(end - count, 0)
return (start, end)
}
extension Array {
func slice(count: Int?, page: Int?) -> Array<Element> {
if count == nil {
return self
}
let indices: (Int, Int?) = calculateStartAndEndIndices(count: count!, page: page)
return Array(self[indices.0..<Swift.min((indices.1 ?? self.count), self.count)])
}
}

View file

@ -0,0 +1,52 @@
//
// ProfileView.swift
// BudgetApp
//
// Created by Billy Brawner on 10/17/19.
// Copyright © 2019 William Brawner. All rights reserved.
//
import SwiftUI
struct ProfileView: View {
let currentUser: User
var body: some View {
NavigationView {
VStack(spacing: 10) {
Image(systemName: "person.circle.fill")
.frame(width: 100, height: 100, alignment: .center)
.scaledToFill()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 5)
Text(currentUser.username)
NavigationLink(destination: EmptyView()) {
Text("change_password")
}
NavigationLink(destination: EmptyView()) {
Text("change_email")
}
NavigationLink(destination: EmptyView()) {
Text("delete_account")
.foregroundColor(.red)
}
}
.navigationBarTitle("profile")
}
}
let dataStoreProvider: DataStoreProvider
init(_ dataStoreProvider: DataStoreProvider) {
self.dataStoreProvider = dataStoreProvider
self.currentUser = try! dataStoreProvider.authenticationDataStore().currentUser.get()
}
}
#if DEBUG
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
ProfileView(MockDataStoreProvider())
}
}
#endif

View file

@ -10,22 +10,23 @@ import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
#if DEBUG
// Uncomment this for local development
// static let baseUrl = "http://localhost:8080"
static let baseUrl = "https://budget-api.intra.wbrawner.com"
#else
static let baseUrl = "https://budget-api.intra.wbrawner.com"
#endif
var window: UIWindow?
let dataStoreProvider: DataStoreProvider
override init() {
// TODO: Dependency injection?
#if DEBUG
// Uncomment this for local development
// let baseUrl = "http://localhost:8080"
let baseUrl = "https://budget-api.intra.wbrawner.com"
#else
let baseUrl = "https://budget-api.intra.wbrawner.com"
#endif
let requestHelper = RequestHelper(baseUrl)
let apiService = BudgetApiService(requestHelper)
let budgetRepository = NetworkBudgetRepository(apiService)
let categoryRepository = NetworkCategoryRepository(apiService)
let requestHelper = RequestHelper(SceneDelegate.baseUrl)
let cacheService = BudgetAppInMemoryCacheService()
let apiService = BudgetAppApiService(requestHelper)
let budgetRepository = NetworkBudgetRepository(apiService, cacheService: cacheService)
let categoryRepository = NetworkCategoryRepository(apiService, cacheService: cacheService)
let transactionRepository = NetworkTransactionRepository(apiService)
let userRepository = NetworkUserRepository(apiService)
dataStoreProvider = DataStoreProvider(
@ -41,7 +42,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// 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(dataStoreProvider)
// Use a UIHostingController as window root view controller.

View file

@ -25,12 +25,9 @@ struct TabbedBudgetView: View {
self.isAddingTransaction = true
}) {
Image(systemName: "plus")
.padding()
}
)
.sheet(isPresented: $isAddingTransaction, content: {
AddTransactionView(self.dataStoreProvider)
.navigationBarTitle("add_transaction")
})
}
.tabItem {
Image(systemName: "dollarsign.circle.fill")
@ -42,17 +39,24 @@ struct TabbedBudgetView: View {
Text("budgets")
}
Text("Profile here").tabItem {
ProfileView(dataStoreProvider).tabItem {
Image(systemName: "person.circle.fill")
Text("profile")
}
}.edgesIgnoringSafeArea(.top)
.sheet(isPresented: $isAddingTransaction, content: {
AddTransactionView(self.dataStoreProvider)
.navigationBarTitle("add_transaction")
})
}
let dataStoreProvider: DataStoreProvider
init (_ userData: AuthenticationDataStore, dataStoreProvider: DataStoreProvider) {
self.userData = userData
self.dataStoreProvider = dataStoreProvider
// Warm up the caches
self.dataStoreProvider.budgetsDataStore().getBudgets()
self.dataStoreProvider.categoryDataStore().getCategories()
}
}
//

View file

@ -90,6 +90,7 @@ struct CategoryPicker: View {
var stateContent: AnyView {
switch self.categoryDataStore.categories {
case .success(let categories):
print("Using returned categories")
return AnyView(
Picker("prompt_category", selection: self.categoryId) {
ForEach(categories) { category in
@ -99,9 +100,7 @@ struct CategoryPicker: View {
)
default:
return AnyView(
Picker("prompt_category", selection: self.categoryId) {
Text("")
}
EmptyView()
)
}
}
@ -113,7 +112,10 @@ struct CategoryPicker: View {
@ObservedObject var categoryDataStore: CategoryDataStore
init(_ dataStoreProvider: DataStoreProvider, budgetId: Binding<Int?>, categoryId: Binding<Int?>) {
let categoryDataStore = dataStoreProvider.categoryDataStore()
categoryDataStore.getCategories(budgetId: budgetId.wrappedValue, count: nil, page: nil)
print("Requesting categories")
if let id = budgetId.wrappedValue {
categoryDataStore.getCategories(budgetId: id, count: nil, page: nil)
}
self.categoryDataStore = categoryDataStore
self.categoryId = categoryId
}

View file

@ -9,7 +9,7 @@
import Foundation
import SwiftUI
struct Transaction: Identifiable, Codable {
struct Transaction: Identifiable, Hashable, Codable {
let id: Int?
let title: String
let description: String?

View file

@ -163,8 +163,10 @@ struct UserLineItem: View {
}
}
#if DEBUG
struct TransactionDetailsView_Previews: PreviewProvider {
static var previews: some View {
TransactionDetailsView(MockDataStoreProvider(), transactionId: 2)
}
}
#endif

View file

@ -82,8 +82,10 @@ struct TransactionEditView: View {
}
}
#if DEBUG
struct TransactionEditView_Previews: PreviewProvider {
static var previews: some View {
TransactionEditView(MockDataStoreProvider(), transaction: MockTransactionRepository.transaction, shouldNavigateUp: .constant(false))
}
}
#endif

View file

@ -44,6 +44,7 @@ struct TransactionListView: View {
self.dataStoreProvider = dataStoreProvider
self.transactionDataStore = dataStoreProvider.transactionDataStore()
self.category = category
self.transactionDataStore.getTransactions(self.category)
}
}

View file

@ -18,9 +18,9 @@ protocol TransactionRepository {
}
class NetworkTransactionRepository: TransactionRepository {
let apiService: BudgetApiService
let apiService: BudgetAppApiService
init(_ apiService: BudgetApiService) {
init(_ apiService: BudgetAppApiService) {
self.apiService = apiService
}

View file

@ -33,6 +33,9 @@ class AuthenticationDataStore: ObservableObject {
self.currentUser = .failure(.failedAuthentication)
}
}) { (user) in
if let sessionCookie = HTTPCookieStorage.shared.cookies(for: URL(string: SceneDelegate.baseUrl)!)?.first(where: { $0.name == SESSION_KEY }) {
UserDefaults.standard.set(sessionCookie.value, forKey: SESSION_KEY)
}
self.currentUser = .success(user)
}
}
@ -62,8 +65,34 @@ class AuthenticationDataStore: ObservableObject {
}
}
private func loadFromExistingSession() {
self.currentUser = .failure(.authenticating)
_ = self.userRepository.getProfile()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { (status) in
switch status {
case .finished:
return
case .failure(_):
self.currentUser = .failure(.unauthenticated)
}
}) { (user) in
self.currentUser = .success(user)
}
}
init(_ userRepository: UserRepository) {
self.userRepository = userRepository
if let sessionKey = UserDefaults.standard.string(forKey: SESSION_KEY) {
HTTPCookieStorage.shared.setCookie(HTTPCookie(properties: [
HTTPCookiePropertyKey.name: SESSION_KEY,
HTTPCookiePropertyKey.value: sessionKey,
HTTPCookiePropertyKey.domain: URL(string: SceneDelegate.baseUrl)!.host!,
HTTPCookiePropertyKey.path: "/"
])!)
loadFromExistingSession()
}
}
// Needed since the default implementation is currently broken
@ -71,6 +100,8 @@ class AuthenticationDataStore: ObservableObject {
private let userRepository: UserRepository
}
private let SESSION_KEY = "SESSION"
enum UserStatus: Error, Equatable {
case unauthenticated
case authenticating
@ -78,3 +109,12 @@ enum UserStatus: Error, Equatable {
case authenticated
case passwordMismatch // Passwords don't match
}
#if DEBUG
class MockAuthenticationDataStore: AuthenticationDataStore {
override init(_ userRepository: UserRepository) {
super.init(userRepository)
self.currentUser = .success(User(id: 1, username: "test_user", email: "test@localhost.loc", avatar: nil))
}
}
#endif

View file

@ -14,12 +14,13 @@ protocol UserRepository {
func searchUsers(_ withUsername: String) -> AnyPublisher<[User], NetworkError>
func login(username: String, password: String) -> AnyPublisher<User, NetworkError>
func register(username: String, email: String, password: String) -> AnyPublisher<User, NetworkError>
func getProfile() -> AnyPublisher<User, NetworkError>
}
class NetworkUserRepository: UserRepository {
let apiService: BudgetApiService
let apiService: BudgetAppApiService
init(_ apiService: BudgetApiService) {
init(_ apiService: BudgetAppApiService) {
self.apiService = apiService
}
@ -38,6 +39,10 @@ class NetworkUserRepository: UserRepository {
func register(username: String, email: String, password: String) -> AnyPublisher<User, NetworkError> {
return apiService.register(username: username, email: email, password: password)
}
func getProfile() -> AnyPublisher<User, NetworkError> {
return apiService.getProfile()
}
}
#if DEBUG
@ -64,6 +69,10 @@ class MockUserRepository: UserRepository {
return Result<User, NetworkError>.Publisher(MockUserRepository.user)
.eraseToAnyPublisher()
}
func getProfile() -> AnyPublisher<User, NetworkError> {
return Result.Publisher(MockUserRepository.user).eraseToAnyPublisher()
}
}
#endif