Эх сурвалжийг харах

Trying to get FetchRequests to update is hard

Andrea Franceschini 4 жил өмнө
parent
commit
d73a5bcf7d

+ 26 - 14
ydnab.xcodeproj/project.pbxproj

@@ -7,9 +7,10 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
-		A32CA1FC251FC0000070AF05 /* GridStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = A32CA1FB251FC0000070AF05 /* GridStack.swift */; };
+		A332A05A2528C73A0096684D /* BudgetCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = A332A0592528C73A0096684D /* BudgetCells.swift */; };
+		A332A05E2528C9F30096684D /* YearAndMonthPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A332A05D2528C9F30096684D /* YearAndMonthPicker.swift */; };
+		A332A0612528CA300096684D /* BudgetViewSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = A332A0602528CA300096684D /* BudgetViewSummary.swift */; };
 		A36CCECC251807AE00C00647 /* ydnabApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36CCECB251807AE00C00647 /* ydnabApp.swift */; };
-		A36CCECE251807AE00C00647 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36CCECD251807AE00C00647 /* ContentView.swift */; };
 		A36CCED0251807AF00C00647 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A36CCECF251807AF00C00647 /* Assets.xcassets */; };
 		A36CCED3251807AF00C00647 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A36CCED2251807AF00C00647 /* Preview Assets.xcassets */; };
 		A36CCED5251807AF00C00647 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36CCED4251807AF00C00647 /* Persistence.swift */; };
@@ -20,10 +21,11 @@
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
-		A32CA1FB251FC0000070AF05 /* GridStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridStack.swift; sourceTree = "<group>"; };
+		A332A0592528C73A0096684D /* BudgetCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetCells.swift; sourceTree = "<group>"; };
+		A332A05D2528C9F30096684D /* YearAndMonthPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YearAndMonthPicker.swift; sourceTree = "<group>"; };
+		A332A0602528CA300096684D /* BudgetViewSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetViewSummary.swift; sourceTree = "<group>"; };
 		A36CCEC8251807AE00C00647 /* ydnab.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ydnab.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		A36CCECB251807AE00C00647 /* ydnabApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ydnabApp.swift; sourceTree = "<group>"; };
-		A36CCECD251807AE00C00647 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
 		A36CCECF251807AF00C00647 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		A36CCED2251807AF00C00647 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
 		A36CCED4251807AF00C00647 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
@@ -46,6 +48,18 @@
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
+		A332A0582528C71D0096684D /* Views */ = {
+			isa = PBXGroup;
+			children = (
+				A332A0592528C73A0096684D /* BudgetCells.swift */,
+				A37B9448251B1B3C00B3B45F /* BudgetsListView.swift */,
+				A37B944C251B2FA100B3B45F /* BudgetView.swift */,
+				A332A0602528CA300096684D /* BudgetViewSummary.swift */,
+				A332A05D2528C9F30096684D /* YearAndMonthPicker.swift */,
+			);
+			path = Views;
+			sourceTree = "<group>";
+		};
 		A36CCEBF251807AE00C00647 = {
 			isa = PBXGroup;
 			children = (
@@ -66,16 +80,13 @@
 			isa = PBXGroup;
 			children = (
 				A36E59E5251AB1BD003DA5B3 /* ydnab.entitlements */,
-				A36E59E1251A799E003DA5B3 /* Models */,
+				A36CCED9251807AF00C00647 /* Info.plist */,
+				A36CCED4251807AF00C00647 /* Persistence.swift */,
 				A36CCECB251807AE00C00647 /* ydnabApp.swift */,
-				A36CCECD251807AE00C00647 /* ContentView.swift */,
 				A36CCECF251807AF00C00647 /* Assets.xcassets */,
-				A36CCED4251807AF00C00647 /* Persistence.swift */,
-				A36CCED9251807AF00C00647 /* Info.plist */,
+				A36E59E1251A799E003DA5B3 /* Models */,
 				A36CCED1251807AF00C00647 /* Preview Content */,
-				A37B9448251B1B3C00B3B45F /* BudgetsListView.swift */,
-				A37B944C251B2FA100B3B45F /* BudgetView.swift */,
-				A32CA1FB251FC0000070AF05 /* GridStack.swift */,
+				A332A0582528C71D0096684D /* Views */,
 			);
 			path = ydnab;
 			sourceTree = "<group>";
@@ -91,8 +102,8 @@
 		A36E59E1251A799E003DA5B3 /* Models */ = {
 			isa = PBXGroup;
 			children = (
-				A36CCED6251807AF00C00647 /* ydnab.xcdatamodeld */,
 				A36E59E2251A79F0003DA5B3 /* Budget.swift */,
+				A36CCED6251807AF00C00647 /* ydnab.xcdatamodeld */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -170,9 +181,10 @@
 				A36CCED5251807AF00C00647 /* Persistence.swift in Sources */,
 				A36E59E3251A79F0003DA5B3 /* Budget.swift in Sources */,
 				A37B9449251B1B3C00B3B45F /* BudgetsListView.swift in Sources */,
+				A332A05A2528C73A0096684D /* BudgetCells.swift in Sources */,
 				A37B944D251B2FA100B3B45F /* BudgetView.swift in Sources */,
-				A36CCECE251807AE00C00647 /* ContentView.swift in Sources */,
-				A32CA1FC251FC0000070AF05 /* GridStack.swift in Sources */,
+				A332A0612528CA300096684D /* BudgetViewSummary.swift in Sources */,
+				A332A05E2528C9F30096684D /* YearAndMonthPicker.swift in Sources */,
 				A36CCECC251807AE00C00647 /* ydnabApp.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

+ 0 - 77
ydnab/ContentView.swift

@@ -1,77 +0,0 @@
-//
-//  ContentView.swift
-//  ydnab
-//
-//  Created by Andrea Franceschini on 20/09/2020.
-//
-
-import SwiftUI
-import CoreData
-
-struct ContentView: View {
-
-    @State var budgetSections: [BudgetSection] = []
-
-    @State var currenBudgetName = ""
-
-    @Environment(\.managedObjectContext) private var viewContext
-    @FetchRequest(
-        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
-        animation: .default)
-    private var items: FetchedResults<Item>
-
-    var body: some View {
-        List {
-            ForEach(budgetSections, id: \.id) { section in
-                Text(section.name)
-            }
-            .onDelete(perform: deleteItems)
-        }
-        .navigationTitle(currenBudgetName)
-    }
-
-    private func addItem() {
-        withAnimation {
-            let newItem = Item(context: viewContext)
-            newItem.timestamp = Date()
-
-            do {
-                try viewContext.save()
-            } catch {
-                // Replace this implementation with code to handle the error appropriately.
-                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
-                let nsError = error as NSError
-                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
-            }
-        }
-    }
-
-    private func deleteItems(offsets: IndexSet) {
-        withAnimation {
-            offsets.map { items[$0] }.forEach(viewContext.delete)
-
-            do {
-                try viewContext.save()
-            } catch {
-                // Replace this implementation with code to handle the error appropriately.
-                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
-                let nsError = error as NSError
-                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
-            }
-        }
-    }
-}
-
-private let itemFormatter: DateFormatter = {
-    let formatter = DateFormatter()
-    formatter.dateStyle = .short
-    formatter.timeStyle = .medium
-    return formatter
-}()
-
-struct ContentView_Previews: PreviewProvider {
-    static var previews: some View {
-        ContentView()
-            .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
-    }
-}

+ 0 - 25
ydnab/GridStack.swift

@@ -1,25 +0,0 @@
-import SwiftUI
-
-struct GridStack<Content: View>: View {
-    let rows: Int
-    let columns: Int
-    let content: (Int, Int) -> Content
-
-    var body: some View {
-        VStack {
-            ForEach(0 ..< rows, id: \.self) { row in
-                HStack {
-                    ForEach(0 ..< self.columns, id: \.self) { column in
-                        self.content(row, column)
-                    }
-                }
-            }
-        }
-    }
-
-    init(rows: Int, columns: Int, @ViewBuilder content: @escaping (Int, Int) -> Content) {
-        self.rows = rows
-        self.columns = columns
-        self.content = content
-    }
-}

+ 0 - 2
ydnab/Models/Budget.swift

@@ -20,8 +20,6 @@ struct BudgetSection: Codable, Identifiable {
     var categories: [BudgetCategory] = []
 }
 
-//typealias BudgetStructure = [BudgetSection]
-
 struct BudgetInfo: Codable, Identifiable {
     var id = UUID()
     var name: String = ""

+ 7 - 3
ydnab/Models/ydnab.xcdatamodeld/ydnab.xcdatamodel/contents

@@ -1,9 +1,13 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17192" systemVersion="19G2021" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
-    <entity name="Item" representedClassName=".Item" syncable="YES" codeGenerationType="class">
-        <attribute name="timestamp" attributeType="Date" defaultDateTimeInterval="622331820" usesScalarValueType="NO"/>
+    <entity name="BudgetCategoryEntry" representedClassName="BudgetCategoryEntry" syncable="YES" codeGenerationType="class">
+        <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="budgetCategoryId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
+        <attribute name="budgetId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
+        <attribute name="month" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
+        <attribute name="year" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
     </entity>
     <elements>
-        <element name="Item" positionX="-63" positionY="-18" width="128" height="58"/>
+        <element name="BudgetCategoryEntry" positionX="-63" positionY="-9" width="128" height="118"/>
     </elements>
 </model>

+ 19 - 22
ydnab/Persistence.swift

@@ -10,23 +10,23 @@ import CoreData
 struct PersistenceController {
     static let shared = PersistenceController()
 
-    static var preview: PersistenceController = {
-        let result = PersistenceController(inMemory: true)
-        let viewContext = result.container.viewContext
-        for _ in 0..<10 {
-            let newItem = Item(context: viewContext)
-            newItem.timestamp = Date()
-        }
-        do {
-            try viewContext.save()
-        } catch {
-            // Replace this implementation with code to handle the error appropriately.
-            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
-            let nsError = error as NSError
-            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
-        }
-        return result
-    }()
+//    static var preview: PersistenceController = {
+//        let result = PersistenceController(inMemory: true)
+//        let viewContext = result.container.viewContext
+//        for _ in 0..<10 {
+//            let newItem = Item(context: viewContext)
+//            newItem.timestamp = Date()
+//        }
+//        do {
+//            try viewContext.save()
+//        } catch {
+//            // Replace this implementation with code to handle the error appropriately.
+//            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
+//            let nsError = error as NSError
+//            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+//        }
+//        return result
+//    }()
 
     let container: NSPersistentContainer
 
@@ -35,11 +35,8 @@ struct PersistenceController {
         if inMemory {
             container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
         }
-        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
+        container.loadPersistentStores { (storeDescription, error) in
             if let error = error as NSError? {
-                // Replace this implementation with code to handle the error appropriately.
-                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
-
                 /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
@@ -50,6 +47,6 @@ struct PersistenceController {
                 */
                 fatalError("Unresolved error \(error), \(error.userInfo)")
             }
-        })
+        }
     }
 }

+ 53 - 0
ydnab/Views/BudgetCells.swift

@@ -0,0 +1,53 @@
+//
+//  BudgetCells.swift
+//  ydnab
+//
+//  Created by Andrea Franceschini on 03/10/2020.
+//
+
+import SwiftUI
+
+/// Budget category, allows to view and edit a budget category.
+struct BudgetCategoryCell: View {
+    /// The budget category being represented
+    @State var category: BudgetCategory
+    /// A formatted string of the amount budgeted for the category
+    @State var amountText: String
+    /// A positive/negative color to apply to the `amountText`
+    @State var color: Color
+
+    var body: some View {
+        HStack {
+            Text(category.name)
+            Spacer()
+            Text(amountText)
+                .bold()
+                .foregroundColor(color)
+        }
+    }
+}
+
+/// Budget section, aggregates the budgeted amounts for this section.
+struct BudgetSectionCell: View {
+    /// The budget section being represented
+    @State var section: BudgetSection
+    /// A formatted string of the aggregated amount budgeted in this section
+    @State var amountText: String
+    /// A positive/negative color to apply to the `amountText`
+    @State var color: Color
+
+    @Environment(\.editMode) var editMode
+
+    var body: some View {
+        HStack {
+            Text(section.name)
+                .textCase(.none)
+            Spacer()
+            if editMode?.wrappedValue == .inactive {
+                Text(amountText)
+                    .foregroundColor(color)
+            }
+        }
+        .padding(.vertical, 11)
+    }
+}

+ 126 - 143
ydnab/BudgetView.swift → ydnab/Views/BudgetView.swift

@@ -6,113 +6,88 @@
 //
 
 import SwiftUI
+import Combine
 
-struct BudgetCategoryCell: View {
-    @State var category: BudgetCategory
-    @State var amountText: String
-    @State var color: Color
+/// Shows the budget values for the selected budget, month, and year.
+struct BudgetView: View {
+    @State var budget: BudgetInfo
+    @Binding var currentBudgetId: UUID?
+    @State private var month: Int = 0
+    @State private var monthName: String
+    @State private var year: Int = Calendar(identifier: .gregorian).component(.year, from: Date())
+    @State private var showMonthPicker: Bool = false
+    @State private var showNotImplemented: Bool = false
+    @State private var showQuickBudget: Bool = false
+    @State private var showEditCategoryEntry: Bool = false
+    @State private var editingCategoryEntry: BudgetCategoryEntry? = nil
 
-    var body: some View {
-        HStack {
-            Text(category.name)
-            Spacer()
-            Text(amountText)
-                .bold()
-                .foregroundColor(color)
-        }
-    }
-}
+    @AppStorage("lastLoadedBudgetId") private var lastLoadedBudgetId = ""
 
-struct BudgetSectionCell: View {
-    @State var section: BudgetSection
-    @State var amountText: String
-    @State var color: Color
+    @Environment(\.managedObjectContext) var managedObjectContext
 
-    @Environment(\.editMode) var editMode
+//    private var budgetEntriesRequest: FetchRequest<BudgetCategoryEntry>
+//    private var budgetEntries: FetchedResults<BudgetCategoryEntry> { budgetEntriesRequest.wrappedValue }
 
-    var body: some View {
-        HStack {
-            Text(section.name)
-                .textCase(.none)
-            Spacer()
-            if editMode?.wrappedValue == .inactive {
-                Text(amountText)
-                    .foregroundColor(color)
-            }
-        }
-        .padding(.vertical, 11)
-    }
-}
+    @FetchRequest<BudgetCategoryEntry> private var budgetEntries: FetchedResults<BudgetCategoryEntry>
 
-struct YearAndMonthPicker: View {
-    @Binding var month: Int
-    @Binding var monthName: String
-    @Binding var year: Int
-    @State var locale: Locale
+    init(budget: BudgetInfo, currentBudgetId: Binding<UUID?>) {
+        _budget = .init(initialValue: budget)
+        _currentBudgetId = currentBudgetId
+        var cal = Calendar(identifier: .gregorian)
+        cal.locale = Locale(identifier: self._budget.wrappedValue.localeIdentifier)
+        let now = Date()
+        _month = .init(initialValue: cal.component(.month, from: now))
+        _monthName = .init(initialValue: cal.monthSymbols[self._month.wrappedValue])
+        _year = .init(initialValue: cal.component(.year, from: now))
 
-    private var monthNames: [String] {
-        var c = Calendar(identifier: .gregorian)
-        c.locale = locale
-        return c.monthSymbols
+        _budgetEntries = FetchRequest<BudgetCategoryEntry>(entity: BudgetCategoryEntry.entity(),
+                                                           sortDescriptors: [],
+                                                           predicate: NSPredicate(format: "(budgetId == %@) AND (month == %d) AND (year == %d)",
+                                                                                  budget.id as CVarArg, _month.wrappedValue, _year.wrappedValue))
+
+//        budgetEntriesRequest = FetchRequest<BudgetCategoryEntry>(entity: BudgetCategoryEntry.entity(),
+//                                                                 sortDescriptors: [],
+//                                                                 predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
+//                                                                    NSPredicate(format: "budgetId == %@", budget.id as CVarArg),
+//                                                                    NSPredicate(format: "month == %i", month),
+//                                                                    NSPredicate(format: "year == %i", year)
+//                                                                 ])
+//        )
     }
 
-    var columns: [GridItem] = [
-        GridItem(spacing: 8),
-        GridItem(spacing: 8),
-        GridItem(spacing: 8)
-    ]
+    func formatAmount(_ amount: NSNumber) -> String? {
+        let f = NumberFormatter()
+        f.locale = Locale(identifier: budget.localeIdentifier)
+        f.numberStyle = .currency
+        return f.string(from: amount)
+    }
 
-    var body: some View {
-        VStack {
-            HStack {
-                Button(action: { year -= 1 }) {
-                    Image(systemName: "chevron.backward.square")
-                        .imageScale(.large)
-                }
-                Spacer()
-                Text(String(year)).bold()
-                Spacer()
-                Button(action: { year += 1 }) {
-                    Image(systemName: "chevron.forward.square")
-                        .imageScale(.large)
+    private func findBudgetCategoryEntry(withId: UUID) -> BudgetCategoryEntry? {
+        return budgetEntries.first(where: { $0.budgetCategoryId == withId })
+    }
+
+    private func zeroBudgetEntries() -> Void {
+        for section in budget.sections {
+            for category in section.categories {
+                if let entry = findBudgetCategoryEntry(withId: category.id) {
+                    entry.amount = 0
+                } else {
+                    let newEntry = BudgetCategoryEntry(context: managedObjectContext)
+                    newEntry.budgetId = budget.id
+                    newEntry.budgetCategoryId = category.id
+                    newEntry.month = Int64(month)
+                    newEntry.year = Int64(year)
+                    newEntry.amount = 0
                 }
-            }
 
-            GeometryReader { geometry in
-                LazyVGrid(columns: columns, spacing: 4) {
-                    ForEach(monthNames.indices) { i in
-                        Button(action: {
-                                month = i
-                                monthName = monthNames[i]
-                        }, label: {
-                            Text(monthNames[i])
-                                .frame(minWidth: geometry.size.width / 3,
-                                       idealWidth: geometry.size.width / 3,
-                                       maxWidth: geometry.size.width / 3,
-                                       minHeight: geometry.size.height / 4,
-                                       idealHeight: geometry.size.height / 4,
-                                       maxHeight: geometry.size.height / 4,
-                                       alignment: .center)
-                        })
-                        .background(month == i ? Color("bgActive") : Color("bgInactive"))
-                        .foregroundColor(month == i ? Color.white : Color("AccentColor"))
-                    }
+                do {
+                    try managedObjectContext.save()
+                } catch {
+                    print(error)
                 }
             }
         }
-        .padding(.bottom, 22)
     }
-}
-
-struct BudgetViewSummary: View {
-    @Binding var budget: BudgetInfo
-    @Binding var month: Int
-    @Binding var monthName: String
-    @Binding var year: Int
-
-    @State private var showMonthPicker: Bool = false
-
-    @State private var showNotImplemented: Bool = false
 
     var body: some View {
         VStack {
@@ -122,62 +97,23 @@ struct BudgetViewSummary: View {
                     Image(systemName: showMonthPicker ? "chevron.up" : "chevron.down")
                 }
                 Spacer()
-                Button(action: { showNotImplemented.toggle() }) { // TODO: Implement this
+                Button(action: { showQuickBudget.toggle() }) { // TODO: Implement this
                     Image(systemName: "bolt.fill")
                 }
                 Button(action: { showNotImplemented.toggle() }) { // TODO: Implement this
                     Image(systemName: "gearshape.fill")
                 }.padding(.leading, 11)
             }
-            .padding(.vertical, 11)
+            .padding(11)
 
             if showMonthPicker {
-                YearAndMonthPicker(month: $month, monthName: $monthName, year: $year, locale: Locale(identifier: budget.localeIdentifier))
-            }
-
-            HStack {
-                Text("To budget") // Or "overbudgeted"
-                Spacer()
-                Text("$ 0.00")
-            }
-
-            .alert(isPresented: $showNotImplemented) {
-                Alert(title: Text("Not Implemented!"))
+                YearAndMonthPicker(month: $month,
+                                   monthName: $monthName,
+                                   year: $year,
+                                   locale: Locale(identifier: budget.localeIdentifier)
+                )
             }
-        }
-        .padding(.vertical, 11)
-    }
-}
-
-struct BudgetView: View {
-    @State var budget: BudgetInfo
-    @Binding var currentBudgetId: UUID?
-    @State private var month: Int = 0
-    @State private var monthName: String
-    @State private var year: Int = Calendar(identifier: .gregorian).component(.year, from: Date())
-
-    @AppStorage("lastLoadedBudgetId") private var lastLoadedBudgetId = ""
-
-    init(budget: BudgetInfo, currentBudgetId: Binding<UUID?>) {
-        _budget = .init(initialValue: budget)
-        _currentBudgetId = currentBudgetId
-        var cal = Calendar(identifier: .gregorian)
-        cal.locale = Locale(identifier: self._budget.wrappedValue.localeIdentifier)
-        let now = Date()
-        _month = .init(initialValue: cal.component(.month, from: now))
-        _monthName = .init(initialValue: cal.monthSymbols[self._month.wrappedValue])
-        _year = .init(initialValue: cal.component(.year, from: now))
-    }
-
-    func formatAmount(_ amount: NSNumber) -> String? {
-        let f = NumberFormatter()
-        f.locale = Locale(identifier: budget.localeIdentifier)
-        f.numberStyle = .currency
-        return f.string(from: amount)
-    }
-
-    var body: some View {
-        VStack {
+            
             BudgetViewSummary(budget: $budget, month: $month, monthName: $monthName, year: $year)
                 .padding([.leading, .trailing], 16)
 
@@ -185,15 +121,19 @@ struct BudgetView: View {
                 ForEach(budget.sections) { section in
                     if !section.hidden {
                         Section(header: BudgetSectionCell(section: section,
-                                                          amountText: formatAmount(0) ?? "--",
+                                                          amountText: "", //formatAmount(0) ?? "--",
                                                           color: 0 < 0 ? Color("negativeAmount") : Color("positiveAmount"))
                         ) {
                             ForEach(section.categories) { category in
                                 if !category.hidden {
                                     BudgetCategoryCell(category: category,
-                                                       amountText: formatAmount(0) ?? "--",
+                                                       amountText: formatAmount(budgetEntries.first(where: { $0.budgetCategoryId == category.id })?.amount ?? 0) ?? "--",
                                                        color: 0 < 0 ? Color("negativeAmount") : Color("positiveAmount")
                                     )
+                                    .onLongPressGesture {
+                                        editingCategoryEntry = findBudgetCategoryEntry(withId: category.id)
+                                        showEditCategoryEntry.toggle()
+                                    }
                                 }
                             }
                             .onMove { (indexSet, s) in
@@ -212,10 +152,55 @@ struct BudgetView: View {
                 EditButton()
             }
             .onAppear() {
-                print("BudgetView Appears")
                 currentBudgetId = budget.id
                 lastLoadedBudgetId = budget.id.uuidString
             }
+            .onChange(of: month) { e in
+
+            }
+            .onReceive(budgetEntries.publisher) { o in
+                print(o.month)
+            }
+            .actionSheet(isPresented: $showQuickBudget) {
+                ActionSheet(
+                    title: Text("Quick budget"),
+                    message: Text("How would you like to set up your budget for \(monthName) \(String(year))?"),
+                    buttons: [
+                        .cancel { print(self.showQuickBudget) },
+                        .default(Text("All categories to zero"), action: zeroBudgetEntries),
+                        .default(Text("Values used the previous month"), action: { print(self) })
+                    ]
+                )
+            }
+            .popover(isPresented: $showEditCategoryEntry) {
+//                BudgetCategoryEntryEditor(amount: $editingCategoryEntry.wrappedValue?.amount)
+            }
+
+            .alert(isPresented: $showNotImplemented) {
+                Alert(title: Text("Not Implemented!"))
+            }
+        }
+    }
+}
+
+struct BudgetCategoryEntryEditor: View {
+    @Binding var amount: Decimal?
+    @Binding var locale: Locale
+    @State private var amountText = ""
+
+    var body: some View {
+        VStack {
+            TextField("Amount", text: $amountText)
+                .keyboardType(.numbersAndPunctuation)
+                .onReceive(Just(amountText)) { newValue in
+                    let filtered = newValue.filter { "0123456789.,".contains($0) }
+                    if filtered != newValue {
+                        amount = Decimal(string: filtered, locale: locale)
+                    }
+                }
+        }
+        .onAppear {
+            amountText = "\(amount ?? -69)"
         }
     }
 }
@@ -255,8 +240,6 @@ struct BudgetView_Previews: PreviewProvider {
                        currentBudgetId: $currentBudgetId)
         }
         .preferredColorScheme(.light)
-
-        //.environment(\.layoutDirection, .rightToLeft)
     }
 }
 

+ 25 - 0
ydnab/Views/BudgetViewSummary.swift

@@ -0,0 +1,25 @@
+//
+//  BudgetViewSummary.swift
+//  ydnab
+//
+//  Created by Andrea Franceschini on 03/10/2020.
+//
+
+import SwiftUI
+
+/// Shows a summary of the budget (to budget, overbudgeted) and allows to change month and year.
+struct BudgetViewSummary: View {
+    @Binding var budget: BudgetInfo
+    @Binding var month: Int
+    @Binding var monthName: String
+    @Binding var year: Int
+
+    var body: some View {
+        HStack {
+            Text("To budget") // TODO "overbudgeted"
+            Spacer()
+            Text("$ 0.00")
+        }
+        .padding(.vertical, 11)
+    }
+}

+ 0 - 0
ydnab/BudgetsListView.swift → ydnab/Views/BudgetsListView.swift


+ 69 - 0
ydnab/Views/YearAndMonthPicker.swift

@@ -0,0 +1,69 @@
+//
+//  YearAndMonthPicker.swift
+//  ydnab
+//
+//  Created by Andrea Franceschini on 03/10/2020.
+//
+
+import SwiftUI
+
+/// Allows to chose the month and year to view.
+struct YearAndMonthPicker: View {
+    @Binding var month: Int
+    @Binding var monthName: String
+    @Binding var year: Int
+    @State var locale: Locale
+
+    private var monthNames: [String] {
+        var c = Calendar(identifier: .gregorian)
+        c.locale = locale
+        return c.monthSymbols
+    }
+
+    var columns: [GridItem] = [
+        GridItem(spacing: 8),
+        GridItem(spacing: 8),
+        GridItem(spacing: 8)
+    ]
+
+    var body: some View {
+        VStack {
+            HStack {
+                Button(action: { year -= 1 }) {
+                    Image(systemName: "chevron.backward.square")
+                        .imageScale(.large)
+                }
+                Spacer()
+                Text(String(year)).bold()
+                Spacer()
+                Button(action: { year += 1 }) {
+                    Image(systemName: "chevron.forward.square")
+                        .imageScale(.large)
+                }
+            }
+
+            GeometryReader { geometry in
+                LazyVGrid(columns: columns, spacing: 4) {
+                    ForEach(monthNames.indices) { i in
+                        Button(action: {
+                                month = i
+                                monthName = monthNames[i]
+                        }, label: {
+                            Text(monthNames[i])
+                                .frame(minWidth: geometry.size.width / 3,
+                                       idealWidth: geometry.size.width / 3,
+                                       maxWidth: geometry.size.width / 3,
+                                       minHeight: geometry.size.height / 4,
+                                       idealHeight: geometry.size.height / 4,
+                                       maxHeight: geometry.size.height / 4,
+                                       alignment: .center)
+                        })
+                        .background(month == i ? Color("bgActive") : Color("bgInactive"))
+                        .foregroundColor(month == i ? Color.white : Color("AccentColor"))
+                    }
+                }
+            }
+        }
+        .padding(.bottom, 22)
+    }
+}

+ 21 - 4
ydnab/ydnabApp.swift

@@ -17,12 +17,12 @@ class AppDelegate : NSObject, UIApplicationDelegate {
         #endif
         guard !applicationSupportURL.path.isEmpty else {
             print("Couldn't find Application Support path... something is wrong here.")
-            // TODO Crash
+            // TODO: Crash
             return false
         }
         let budgetsListURL = URL(fileURLWithPath: "Budgets.json", relativeTo: applicationSupportURL)
         if !FileManager.default.fileExists(atPath: budgetsListURL.path) {
-            // TODO Run onboarding, create first budget
+            // TODO: Run onboarding, create first budget
             let budget: [BudgetSection] = [
                 BudgetSection(name: "Everyday Expenses", categories: [
                     BudgetCategory(name: "Groceries"),
@@ -57,12 +57,14 @@ class AppDelegate : NSObject, UIApplicationDelegate {
 
             let jsonBudget: Data
             do {
-                jsonBudget = try JSONEncoder().encode(budgetsList)
+                let encoder = JSONEncoder()
+                encoder.outputFormatting = .prettyPrinted
+                jsonBudget = try encoder.encode(budgetsList)
                 try jsonBudget.write(to: budgetsListURL)
             } catch {
                 print("Couldn't encode budgets list.")
                 print("Error: \(error)")
-                // TODO Crash?
+                // TODO: Crash?
                 return false
             }
         }
@@ -84,10 +86,25 @@ struct ydnabApp: App {
         WindowGroup {
             NavigationView {
                 BudgetsListView(currentBudgetId: UUID(uuidString: lastLoadedBudgetId))
+
             }
+            .environment(\.managedObjectContext, persistenceController.container.viewContext)
         }
         .onChange(of: scenePhase) { phase in
             print(phase)
+            switch phase {
+            case .background:
+                let context = persistenceController.container.viewContext
+                if context.hasChanges {
+                    do {
+                        try context.save()
+                    } catch {
+                        print(error)
+                    }
+                }
+            default:
+                print(phase)
+            }
         }
     }
 }