Browse Source

Initial commit

Andrea Franceschini 4 years ago
parent
commit
1215419d34

+ 90 - 0
.gitignore

@@ -0,0 +1,90 @@
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+build/
+DerivedData/
+*.moved-aside
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+
+## Obj-C/Swift specific
+*.hmap
+
+## App packaging
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+# *.xcodeproj
+#
+# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
+# hence it is not needed unless you have added a package configuration file to your project
+# .swiftpm
+
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build/
+
+# Accio dependency management
+Dependencies/
+.accio/
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+# Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/

+ 31 - 1
ydnab.xcodeproj/project.pbxproj

@@ -7,15 +7,20 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		A32CA1FC251FC0000070AF05 /* GridStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = A32CA1FB251FC0000070AF05 /* GridStack.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 */; };
 		A36CCED8251807AF00C00647 /* ydnab.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = A36CCED6251807AF00C00647 /* ydnab.xcdatamodeld */; };
+		A36E59E3251A79F0003DA5B3 /* Budget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36E59E2251A79F0003DA5B3 /* Budget.swift */; };
+		A37B9449251B1B3C00B3B45F /* BudgetsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A37B9448251B1B3C00B3B45F /* BudgetsListView.swift */; };
+		A37B944D251B2FA100B3B45F /* BudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A37B944C251B2FA100B3B45F /* BudgetView.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
+		A32CA1FB251FC0000070AF05 /* GridStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridStack.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>"; };
@@ -24,6 +29,10 @@
 		A36CCED4251807AF00C00647 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
 		A36CCED7251807AF00C00647 /* ydnab.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ydnab.xcdatamodel; sourceTree = "<group>"; };
 		A36CCED9251807AF00C00647 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		A36E59E2251A79F0003DA5B3 /* Budget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Budget.swift; sourceTree = "<group>"; };
+		A36E59E5251AB1BD003DA5B3 /* ydnab.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ydnab.entitlements; sourceTree = "<group>"; };
+		A37B9448251B1B3C00B3B45F /* BudgetsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetsListView.swift; sourceTree = "<group>"; };
+		A37B944C251B2FA100B3B45F /* BudgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetView.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -56,13 +65,17 @@
 		A36CCECA251807AE00C00647 /* ydnab */ = {
 			isa = PBXGroup;
 			children = (
+				A36E59E5251AB1BD003DA5B3 /* ydnab.entitlements */,
+				A36E59E1251A799E003DA5B3 /* Models */,
 				A36CCECB251807AE00C00647 /* ydnabApp.swift */,
 				A36CCECD251807AE00C00647 /* ContentView.swift */,
 				A36CCECF251807AF00C00647 /* Assets.xcassets */,
 				A36CCED4251807AF00C00647 /* Persistence.swift */,
 				A36CCED9251807AF00C00647 /* Info.plist */,
-				A36CCED6251807AF00C00647 /* ydnab.xcdatamodeld */,
 				A36CCED1251807AF00C00647 /* Preview Content */,
+				A37B9448251B1B3C00B3B45F /* BudgetsListView.swift */,
+				A37B944C251B2FA100B3B45F /* BudgetView.swift */,
+				A32CA1FB251FC0000070AF05 /* GridStack.swift */,
 			);
 			path = ydnab;
 			sourceTree = "<group>";
@@ -75,6 +88,15 @@
 			path = "Preview Content";
 			sourceTree = "<group>";
 		};
+		A36E59E1251A799E003DA5B3 /* Models */ = {
+			isa = PBXGroup;
+			children = (
+				A36CCED6251807AF00C00647 /* ydnab.xcdatamodeld */,
+				A36E59E2251A79F0003DA5B3 /* Budget.swift */,
+			);
+			path = Models;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -146,7 +168,11 @@
 			files = (
 				A36CCED8251807AF00C00647 /* ydnab.xcdatamodeld in Sources */,
 				A36CCED5251807AF00C00647 /* Persistence.swift in Sources */,
+				A36E59E3251A79F0003DA5B3 /* Budget.swift in Sources */,
+				A37B9449251B1B3C00B3B45F /* BudgetsListView.swift in Sources */,
+				A37B944D251B2FA100B3B45F /* BudgetView.swift in Sources */,
 				A36CCECE251807AE00C00647 /* ContentView.swift in Sources */,
+				A32CA1FC251FC0000070AF05 /* GridStack.swift in Sources */,
 				A36CCECC251807AE00C00647 /* ydnabApp.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -275,6 +301,7 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = ydnab/ydnab.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				DEVELOPMENT_ASSET_PATHS = "\"ydnab/Preview Content\"";
 				DEVELOPMENT_TEAM = 4P5WJRFVXA;
@@ -287,6 +314,7 @@
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = net.morpheu5.ydnab;
 				PRODUCT_NAME = "$(TARGET_NAME)";
+				SUPPORTS_MACCATALYST = NO;
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
@@ -297,6 +325,7 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_ENTITLEMENTS = ydnab/ydnab.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				DEVELOPMENT_ASSET_PATHS = "\"ydnab/Preview Content\"";
 				DEVELOPMENT_TEAM = 4P5WJRFVXA;
@@ -309,6 +338,7 @@
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = net.morpheu5.ydnab;
 				PRODUCT_NAME = "$(TARGET_NAME)";
+				SUPPORTS_MACCATALYST = NO;
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};

+ 17 - 0
ydnab/Assets.xcassets/AccentColor.colorset/Contents.json

@@ -1,6 +1,23 @@
 {
   "colors" : [
     {
+      "color" : {
+        "platform" : "ios",
+        "reference" : "systemBlueColor"
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "platform" : "ios",
+        "reference" : "systemTealColor"
+      },
       "idiom" : "universal"
     }
   ],

+ 38 - 0
ydnab/Assets.xcassets/negativeAmount.colorset/Contents.json

@@ -0,0 +1,38 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.000",
+          "green" : "0.067",
+          "red" : "0.581"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.474",
+          "green" : "0.493",
+          "red" : "1.000"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 38 - 0
ydnab/Assets.xcassets/positiveAmount.colorset/Contents.json

@@ -0,0 +1,38 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.319",
+          "green" : "0.563",
+          "red" : "0.000"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.574",
+          "green" : "0.981",
+          "red" : "0.000"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 216 - 0
ydnab/BudgetView.swift

@@ -0,0 +1,216 @@
+//
+//  BudgetView.swift
+//  ydnab
+//
+//  Created by Andrea Franceschini on 23/09/2020.
+//
+
+import SwiftUI
+
+struct BudgetCategoryCell: View {
+    @State var category: BudgetCategory
+    @State var amountText: String
+    @State var color: Color
+
+    var body: some View {
+        HStack {
+            Text(category.name)
+            Spacer()
+            Text(amountText)
+                .bold()
+                .foregroundColor(color)
+        }
+    }
+}
+
+struct BudgetSectionCell: View {
+    @State var section: BudgetSection
+    @State var amountText: String
+    @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)
+    }
+}
+
+struct YearAndMonthPicker: View {
+    @Binding var month: Int
+    @Binding var year: Int
+    @State var locale: Locale
+
+    private var monthNames: [String] {
+        var c = Calendar(identifier: .gregorian)
+        c.locale = locale
+        return c.monthSymbols
+    }
+
+    var body: some View {
+        VStack {
+            HStack {
+                Button(action: { print(self) }) {
+                    Image(systemName: "chevron.backward.square")
+                        .imageScale(.large)
+                }
+                Spacer()
+                Text("\(year)").bold()
+                Spacer()
+                Button(action: {}) {
+                    Image(systemName: "chevron.forward.square")
+                        .imageScale(.large)
+                }
+            }
+
+//            GridStack(rows: 4, columns: 3) { row, col in
+//                Button(monthNames[row+col], action: { month = row+col+1 })
+//            }
+
+//            ScrollView(.horizontal) {
+                LazyHGrid(rows: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], alignment: .top, spacing: nil, pinnedViews: []) {
+                    ForEach(0..<monthNames.count) { i in
+                        Button(monthNames[i], action: { month = i+1 }).padding(40)
+                    }
+//                }
+            }
+        }
+        .padding(.bottom, 11)
+    }
+}
+
+struct BudgetViewSummary: View {
+    @Binding var budget: BudgetInfo
+    @Binding var month: Int
+    @Binding var year: Int
+
+    @State private var showMonthPicker: Bool = true
+
+    var body: some View {
+        VStack {
+            HStack {
+                Button("\(month)", action: { withAnimation() { showMonthPicker.toggle() } } )
+                Spacer()
+            }
+            .padding(.vertical, 11)
+
+            if showMonthPicker {
+                YearAndMonthPicker(month: $month, year: $year, locale: Locale(identifier: budget.localeIdentifier))
+            }
+
+            HStack {
+                Text("To budget") // Or "overbudgeted"
+                Spacer()
+                Text("$ 0.00")
+            }
+        }
+        .padding(.vertical, 11)
+    }
+}
+
+struct BudgetView: View {
+    @State var budget: BudgetInfo
+    @Binding var currentBudgetId: UUID?
+    @State private var month: Int = 0
+    @State private var year: Int = 0
+
+    @AppStorage("lastLoadedBudgetId") private var lastLoadedBudgetId = ""
+
+    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, year: $year)
+                .padding([.leading, .trailing], 16)
+
+            List {
+                ForEach(budget.sections) { section in
+                    if !section.hidden {
+                        Section(header: BudgetSectionCell(section: section,
+                                                          amountText: formatAmount(0) ?? "--",
+                                                          color: 0 < 0 ? Color("negativeAmount") : Color("positiveAmount"))
+                        ) {
+                            ForEach(section.categories) { category in
+                                if !category.hidden {
+                                    BudgetCategoryCell(category: category,
+                                                       amountText: formatAmount(0) ?? "--",
+                                                       color: 0 < 0 ? Color("negativeAmount") : Color("positiveAmount")
+                                    )
+                                }
+                            }
+                            .onMove { (indexSet, s) in
+                                print(indexSet, s)
+                            }
+                        }
+                    }
+                }
+                .onMove { (indexSet, s) in
+                    print(indexSet, s)
+                }
+            }
+            .listStyle(PlainListStyle())
+            .navigationBarTitle(budget.name, displayMode: .inline)
+            .toolbar {
+                EditButton()
+            }
+            .onAppear() {
+                print("BudgetView Appears")
+                currentBudgetId = budget.id
+                lastLoadedBudgetId = budget.id.uuidString
+            }
+        }
+    }
+}
+
+struct BudgetView_Previews: PreviewProvider {
+    static let budget: [BudgetSection] = [
+        BudgetSection(name: "Everyday Expenses", categories: [
+            BudgetCategory(name: "Groceries"),
+            BudgetCategory(name: "Eating out"),
+            BudgetCategory(name: "Medical"),
+            BudgetCategory(name: "Clothing"),
+            BudgetCategory(name: "Household goods")
+        ]),
+        BudgetSection(name: "Travel", categories: [
+            BudgetCategory(name: "Transport"),
+            BudgetCategory(name: "Fuel"),
+            BudgetCategory(name: "Accommodation")
+        ]),
+        BudgetSection(name: "Rainy Days", categories: [
+            BudgetCategory(name: "Emergencies"),
+            BudgetCategory(name: "Car insurance")
+        ]),
+        BudgetSection(name: "Monthly Expenses", categories: [
+            BudgetCategory(name: "Rent"),
+            BudgetCategory(name: "Mobile")
+        ]),
+        BudgetSection(name: "Savings Goals", categories: [
+            BudgetCategory(name: "Savings"),
+            BudgetCategory(name: "Holidays")
+        ])
+    ]
+    @State static var currentBudgetId: UUID?
+
+    static var previews: some View {
+        NavigationView {
+            BudgetView(budget: BudgetInfo(name: "Default Budget", sections: budget),
+                       currentBudgetId: $currentBudgetId)
+        }
+        .preferredColorScheme(.light)
+        //.environment(\.layoutDirection, .rightToLeft)
+    }
+}
+

+ 98 - 0
ydnab/BudgetsListView.swift

@@ -0,0 +1,98 @@
+//
+//  BudgetsListView.swift
+//  ydnab
+//
+//  Created by Andrea Franceschini on 23/09/2020.
+//
+
+import SwiftUI
+
+class BudgetsListViewModel: ObservableObject {
+
+    @Published var items: BudgetsList
+
+    init() {
+        let applicationSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
+        guard !applicationSupportURL.path.isEmpty else {
+            print("Couldn't find Application Support path.")
+            items = []
+            return
+        }
+        let budgetsListURL = URL(fileURLWithPath: "Budgets.json", relativeTo: applicationSupportURL)
+        guard FileManager.default.fileExists(atPath: budgetsListURL.path) else {
+            print("Couldn't find the budgets list in Application Support.")
+            print(budgetsListURL.path)
+            items = []
+            return
+        }
+        do {
+            items = try JSONDecoder().decode(BudgetsList.self, from: Data.init(contentsOf: budgetsListURL))
+        } catch {
+            print("Couldn't open the budgets list.")
+            print("Error: \(error)")
+            items = []
+            return
+        }
+    }
+}
+
+struct BudgetsListView: View {
+    @State var currentBudgetId: UUID?
+    @ObservedObject var budgetsList = BudgetsListViewModel()
+
+    init(currentBudgetId: UUID?, budgetsList: BudgetsListViewModel = BudgetsListViewModel()) {
+        self.currentBudgetId = currentBudgetId
+        self.budgetsList = budgetsList
+    }
+
+    func createBudget() {
+        print("Creating new budget...")
+
+        print("... done creating new budget.")
+    }
+
+    var body: some View {
+        VStack {
+            List {
+                ForEach(budgetsList.items) { budget in
+                    NavigationLink(destination: BudgetView(budget: budget, currentBudgetId: $currentBudgetId),
+                                   tag: budget.id,
+                                   selection: $currentBudgetId) {
+                        Text(budget.name)
+                    }
+                }
+                .onDelete(perform: { indexSet in
+                    print(indexSet)
+                })
+                .onMove(perform: { indices, newOffset in
+                    print("\(indices) :: \(newOffset)")
+                })
+            }
+
+            Text(currentBudgetId?.uuidString ?? "--")
+        }
+        .navigationTitle("Budgets")
+        .toolbar {
+            #if os(iOS)
+            ToolbarItem(placement: .primaryAction) {
+                EditButton()
+            }
+            #endif
+
+            ToolbarItem(placement: .navigationBarLeading) {
+                Button("Add") {
+                    print(self)
+                }
+            }
+        }
+    }
+}
+
+//struct BudgetsListView_Previews: PreviewProvider {
+//    static var previews: some View {
+//        NavigationView {
+//            BudgetsListView()
+//        }
+//    }
+//}
+

+ 10 - 13
ydnab/ContentView.swift

@@ -9,8 +9,12 @@ import SwiftUI
 import CoreData
 
 struct ContentView: View {
-    @Environment(\.managedObjectContext) private var viewContext
 
+    @State var budgetSections: [BudgetSection] = []
+
+    @State var currenBudgetName = ""
+
+    @Environment(\.managedObjectContext) private var viewContext
     @FetchRequest(
         sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
         animation: .default)
@@ -18,20 +22,12 @@ struct ContentView: View {
 
     var body: some View {
         List {
-            ForEach(items) { item in
-                Text("Item at \(item.timestamp!, formatter: itemFormatter)")
+            ForEach(budgetSections, id: \.id) { section in
+                Text(section.name)
             }
             .onDelete(perform: deleteItems)
         }
-        .toolbar {
-            #if os(iOS)
-            EditButton()
-            #endif
-
-            Button(action: addItem) {
-                Label("Add Item", systemImage: "plus")
-            }
-        }
+        .navigationTitle(currenBudgetName)
     }
 
     private func addItem() {
@@ -75,6 +71,7 @@ private let itemFormatter: DateFormatter = {
 
 struct ContentView_Previews: PreviewProvider {
     static var previews: some View {
-        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
+        ContentView()
+            .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
     }
 }

+ 25 - 0
ydnab/GridStack.swift

@@ -0,0 +1,25 @@
+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
+    }
+}

+ 9 - 1
ydnab/Info.plist

@@ -28,7 +28,15 @@
 	<key>UIApplicationSupportsIndirectInputEvents</key>
 	<true/>
 	<key>UILaunchScreen</key>
-	<dict/>
+	<dict>
+		<key>UIColorName</key>
+		<string>Background</string>
+		<key>UINavigationBar</key>
+		<dict>
+			<key>UIImageName</key>
+			<string></string>
+		</dict>
+	</dict>
 	<key>UIRequiredDeviceCapabilities</key>
 	<array>
 		<string>armv7</string>

+ 32 - 0
ydnab/Models/Budget.swift

@@ -0,0 +1,32 @@
+//
+//  Budget.swift
+//  ydnab
+//
+//  Created by Andrea Franceschini on 22/09/2020.
+//
+
+import Foundation
+
+struct BudgetCategory: Codable, Identifiable {
+    var id: UUID = UUID()
+    var name: String = ""
+    var hidden: Bool = false
+}
+
+struct BudgetSection: Codable, Identifiable {
+    var id: UUID = UUID()
+    var name: String = ""
+    var hidden: Bool = false
+    var categories: [BudgetCategory] = []
+}
+
+//typealias BudgetStructure = [BudgetSection]
+
+struct BudgetInfo: Codable, Identifiable {
+    var id = UUID()
+    var name: String = ""
+    var localeIdentifier: String = ""
+    var sections: [BudgetSection] = []
+}
+
+typealias BudgetsList = [BudgetInfo]

+ 0 - 0
ydnab/ydnab.xcdatamodeld/.xccurrentversion → ydnab/Models/ydnab.xcdatamodeld/.xccurrentversion


+ 9 - 0
ydnab/Models/ydnab.xcdatamodeld/ydnab.xcdatamodel/contents

@@ -0,0 +1,9 @@
+<?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>
+    <elements>
+        <element name="Item" positionX="-63" positionY="-18" width="128" height="58"/>
+    </elements>
+</model>

+ 10 - 0
ydnab/ydnab.entitlements

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.app-sandbox</key>
+	<true/>
+	<key>com.apple.security.network.client</key>
+	<true/>
+</dict>
+</plist>

+ 0 - 9
ydnab/ydnab.xcdatamodeld/ydnab.xcdatamodel/contents

@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
-    <entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
-        <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
-    </entity>
-    <elements>
-        <element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
-    </elements>
-</model>

+ 75 - 2
ydnab/ydnabApp.swift

@@ -5,16 +5,89 @@
 //  Created by Andrea Franceschini on 20/09/2020.
 //
 
+import UIKit
 import SwiftUI
+import os.log
+
+class AppDelegate : NSObject, UIApplicationDelegate {
+    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
+        let applicationSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
+        #if DEBUG
+        print(applicationSupportURL.path)
+        #endif
+        guard !applicationSupportURL.path.isEmpty else {
+            print("Couldn't find Application Support path... something is wrong here.")
+            // 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
+            let budget: [BudgetSection] = [
+                BudgetSection(name: "Everyday Expenses", categories: [
+                    BudgetCategory(name: "Groceries"),
+                    BudgetCategory(name: "Eating out"),
+                    BudgetCategory(name: "Medical"),
+                    BudgetCategory(name: "Clothing"),
+                    BudgetCategory(name: "Household goods")
+                ]),
+                BudgetSection(name: "Travel", categories: [
+                    BudgetCategory(name: "Transport"),
+                    BudgetCategory(name: "Fuel"),
+                    BudgetCategory(name: "Accommodation")
+                ]),
+                BudgetSection(name: "Rainy Days", categories: [
+                    BudgetCategory(name: "Emergencies"),
+                    BudgetCategory(name: "Car insurance")
+                ]),
+                BudgetSection(name: "Monthly Expenses", categories: [
+                    BudgetCategory(name: "Rent"),
+                    BudgetCategory(name: "Mobile")
+                ]),
+                BudgetSection(name: "Savings Goals", categories: [
+                    BudgetCategory(name: "Savings"),
+                    BudgetCategory(name: "Holidays")
+                ])
+            ]
+            let budgetsList: BudgetsList = [
+                BudgetInfo(name: "Default Budget",
+                           localeIdentifier: "en-us",
+                           sections: budget)
+            ]
+
+            let jsonBudget: Data
+            do {
+                jsonBudget = try JSONEncoder().encode(budgetsList)
+                try jsonBudget.write(to: budgetsListURL)
+            } catch {
+                print("Couldn't encode budgets list.")
+                print("Error: \(error)")
+                // TODO Crash?
+                return false
+            }
+        }
+        return true
+    }
+}
 
 @main
 struct ydnabApp: App {
+
     let persistenceController = PersistenceController.shared
 
+    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+
+    @Environment(\.scenePhase) private var scenePhase
+    @AppStorage("lastLoadedBudgetId") private var lastLoadedBudgetId = ""
+
     var body: some Scene {
         WindowGroup {
-            ContentView()
-                .environment(\.managedObjectContext, persistenceController.container.viewContext)
+            NavigationView {
+                BudgetsListView(currentBudgetId: UUID(uuidString: lastLoadedBudgetId))
+            }
+        }
+        .onChange(of: scenePhase) { phase in
+            print(phase)
         }
     }
 }