Compare commits

5 Commits

Author SHA1 Message Date
947555a816 🚧 Add files 2025-03-31 23:45:37 +02:00
6a53982a82 🐛 Delete is now possible 2024-07-02 18:33:37 +02:00
af2af3d07a 🐛 Decimal Numbers are now possible 2024-07-02 18:31:49 +02:00
0573b07e1c 🐛 Decimal Numbers are now possible 2024-07-02 17:59:47 +02:00
de58ebd79c Add Swift Data 2024-07-02 17:51:49 +02:00
9 changed files with 128 additions and 54 deletions

View File

@@ -14,6 +14,7 @@
D40CCAEF2C2DC5D8007C4A9F /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40CCAEE2C2DC5D8007C4A9F /* Subscription.swift */; }; D40CCAEF2C2DC5D8007C4A9F /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40CCAEE2C2DC5D8007C4A9F /* Subscription.swift */; };
D40CCAF32C2EE305007C4A9F /* AddSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40CCAF22C2EE304007C4A9F /* AddSubscriptionView.swift */; }; D40CCAF32C2EE305007C4A9F /* AddSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40CCAF22C2EE304007C4A9F /* AddSubscriptionView.swift */; };
D426C55A2C2F0F150057455D /* PaymentCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D426C5592C2F0F150057455D /* PaymentCalendarView.swift */; }; D426C55A2C2F0F150057455D /* PaymentCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D426C5592C2F0F150057455D /* PaymentCalendarView.swift */; };
D43587412C3450F300DD321B /* Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43587402C3450F300DD321B /* Helper.swift */; };
D4544FEF2C320AF30090E311 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4544FEE2C320AF30090E311 /* HomeView.swift */; }; D4544FEF2C320AF30090E311 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4544FEE2C320AF30090E311 /* HomeView.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -27,6 +28,7 @@
D40CCAEE2C2DC5D8007C4A9F /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; }; D40CCAEE2C2DC5D8007C4A9F /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; };
D40CCAF22C2EE304007C4A9F /* AddSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSubscriptionView.swift; sourceTree = "<group>"; }; D40CCAF22C2EE304007C4A9F /* AddSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSubscriptionView.swift; sourceTree = "<group>"; };
D426C5592C2F0F150057455D /* PaymentCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentCalendarView.swift; sourceTree = "<group>"; }; D426C5592C2F0F150057455D /* PaymentCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentCalendarView.swift; sourceTree = "<group>"; };
D43587402C3450F300DD321B /* Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helper.swift; sourceTree = "<group>"; };
D4544FEE2C320AF30090E311 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; }; D4544FEE2C320AF30090E311 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -66,6 +68,7 @@
D40CCAE32C2DC5AA007C4A9F /* Assets.xcassets */, D40CCAE32C2DC5AA007C4A9F /* Assets.xcassets */,
D40CCAE52C2DC5AA007C4A9F /* AboTracker.entitlements */, D40CCAE52C2DC5AA007C4A9F /* AboTracker.entitlements */,
D40CCAE62C2DC5AA007C4A9F /* Preview Content */, D40CCAE62C2DC5AA007C4A9F /* Preview Content */,
D43587402C3450F300DD321B /* Helper.swift */,
); );
path = AboTracker; path = AboTracker;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -165,6 +168,7 @@
D4544FEF2C320AF30090E311 /* HomeView.swift in Sources */, D4544FEF2C320AF30090E311 /* HomeView.swift in Sources */,
D426C55A2C2F0F150057455D /* PaymentCalendarView.swift in Sources */, D426C55A2C2F0F150057455D /* PaymentCalendarView.swift in Sources */,
D40CCAE02C2DC5A9007C4A9F /* AboTrackerApp.swift in Sources */, D40CCAE02C2DC5A9007C4A9F /* AboTrackerApp.swift in Sources */,
D43587412C3450F300DD321B /* Helper.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "21F11B35-51F5-45D2-B767-D9510D0FF6A9"
type = "1"
version = "2.0">
</Bucket>

View File

@@ -13,5 +13,6 @@ struct AboTrackerApp: App {
WindowGroup { WindowGroup {
ContentView() ContentView()
} }
.modelContainer(for: [Subscription.self, Payment.self])
} }
} }

17
AboTracker/Helper.swift Normal file
View File

@@ -0,0 +1,17 @@
import Foundation
/*func getMockedSubs() -> [Subscription] {
return [
Subscription(name: "Test", payments: [Payment(amount: 9.99, intervall: .monthly, startDate: getDate(from: "2023-01-01"))], color: .blue),
Subscription(name: "Fitness First", payments: [
Payment(amount: 7.9, intervall: .weekly, startDate: getDate(from: "2023-01-23")),
Payment(amount: 29, intervall: .quarter, startDate: getDate(from: "2023-04-03"))
], color: .red)
]
}
private func getDate(from dateString: String) -> Date {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter.date(from: dateString) ?? Date()
}*/

View File

@@ -1,16 +1,38 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import SwiftData
import UIKit
final class Subscription: Identifiable { @Model final class Subscription: Identifiable {
public let id = UUID() @Attribute(.unique) public let id = UUID()
var name: String var name: String
var payments: [Payment] @Relationship(deleteRule: .cascade, minimumModelCount: 1) var payments: [Payment]
var color: Color var colorData: Data?
init(name: String, payments: [Payment], color: Color) { init(name: String, payments: [Payment], color: Color) {
self.name = name self.name = name
self.payments = payments self.payments = payments
self.color = color self.colorData = try? NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: false)
}
func setColor(c: Color) {
colorData = try? NSKeyedArchiver.archivedData(withRootObject: c, requiringSecureCoding: false)
}
func getColor() -> Color? {
guard let colorData = self.colorData else { return nil }
do {
if let color = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: colorData) {
return Color(color)
}
} catch {
print("Error unarchiving color data: \(error)")
}
return nil
}
func getColorOrAccent() -> Color {
return getColor() ?? Color.accentColor
} }
func getMonthlyAmount() -> Float { func getMonthlyAmount() -> Float {
@@ -94,8 +116,8 @@ final class Subscription: Identifiable {
} }
} }
final class Payment: Identifiable { @Model final class Payment: Identifiable {
public let id = UUID() @Attribute(.unique) public let id = UUID()
var amount: Float var amount: Float
var intervall: PaymentIntervall var intervall: PaymentIntervall
var startDate: Date var startDate: Date
@@ -107,7 +129,7 @@ final class Payment: Identifiable {
} }
} }
enum Currency: CustomStringConvertible { enum Currency: Codable, CustomStringConvertible {
case euro case euro
case dollar case dollar
@@ -121,7 +143,7 @@ enum Currency: CustomStringConvertible {
} }
} }
enum PaymentIntervall: CustomStringConvertible { enum PaymentIntervall: Codable, CustomStringConvertible {
case weekly case weekly
case monthly case monthly
case quarter case quarter
@@ -140,3 +162,22 @@ enum PaymentIntervall: CustomStringConvertible {
} }
} }
} }
extension Color {
var hexString: String? {
guard let components = UIColor(self).cgColor.components, components.count >= 3 else {
return nil
}
let r = Float(components[0])
let g = Float(components[1])
let b = Float(components[2])
let a = Float(components.count == 4 ? components[3] : 1.0)
return String(format: "%02lX%02lX%02lX%02lX",
lroundf(r * 255),
lroundf(g * 255),
lroundf(b * 255),
lroundf(a * 255))
}
}

View File

@@ -1,8 +1,10 @@
import SwiftUI import SwiftUI
import SwiftData
struct AddSubscriptionView: View { struct AddSubscriptionView: View {
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@Binding var subs: [Subscription] @Environment(\.modelContext) private var modelContext
@Binding var subscription: Subscription?
@State private var name: String = "" @State private var name: String = ""
@State private var payments: [Payment] = [Payment(amount: 0, intervall: .monthly, startDate: Date())] @State private var payments: [Payment] = [Payment(amount: 0, intervall: .monthly, startDate: Date())]
@@ -22,7 +24,7 @@ struct AddSubscriptionView: View {
ForEach($payments) { $payment in ForEach($payments) { $payment in
Section(header: Text("Payment")) { Section(header: Text("Payment")) {
HStack { HStack {
TextField("Amount", value: $payment.amount, formatter: NumberFormatter()) TextField("Amount", value: $payment.amount, format: .number)
Spacer() Spacer()
Text("\(Currency.euro.description)") Text("\(Currency.euro.description)")
} }
@@ -47,14 +49,27 @@ struct AddSubscriptionView: View {
} }
Section { Section {
Button("Add Subscription") { Button(subscription == nil ? "Add Subscription" : "Save Subscription") {
let newSubscription = Subscription(name: name, payments: payments, color: color) if let existingSubscription = subscription {
subs.append(newSubscription) existingSubscription.name = name
existingSubscription.payments = payments
existingSubscription.setColor(c: color)
} else {
let newSubscription = Subscription(name: name, payments: payments, color: color)
modelContext.insert(newSubscription)
}
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
} }
} }
} }
.navigationTitle("Add Subscription") .navigationTitle(subscription == nil ? "Add Subscription" : "Edit Subscription")
.onAppear {
if let existingSubscription = subscription {
name = existingSubscription.name
payments = existingSubscription.payments
color = existingSubscription.getColor() ?? .blue
}
}
#if os(iOS) #if os(iOS)
.navigationBarItems(trailing: Button("Cancel") { .navigationBarItems(trailing: Button("Cancel") {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()

View File

@@ -1,27 +1,18 @@
import SwiftUI import SwiftUI
import SwiftData
struct ContentView: View { struct ContentView: View {
@State private var subs: [Subscription] = [] @Query var subs: [Subscription]
init() {
self._subs = State(initialValue: [
Subscription(name: "Test", payments: [Payment(amount: 9.99, intervall: .monthly, startDate: getDate(from: "2023-01-01"))], color: .blue),
Subscription(name: "Fitness First", payments: [
Payment(amount: 7.9, intervall: .weekly, startDate: getDate(from: "2023-01-23")),
Payment(amount: 29, intervall: .quarter, startDate: getDate(from: "2023-04-03"))
], color: .red)
])
}
var body: some View { var body: some View {
TabView { TabView {
HomeView(subs: $subs) HomeView()
.tabItem { .tabItem {
Image(systemName: "house.fill") Image(systemName: "house.fill")
Text("Home") Text("Home")
} }
PaymentCalendarView(subs: $subs) PaymentCalendarView()
.tabItem { .tabItem {
Image(systemName: "calendar") Image(systemName: "calendar")
Text("Calendar") Text("Calendar")
@@ -30,15 +21,11 @@ struct ContentView: View {
.background(Color.gray.opacity(0.2)) .background(Color.gray.opacity(0.2))
} }
private func getDate(from dateString: String) -> Date {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter.date(from: dateString) ?? Date()
}
} }
struct ContentView_Previews: PreviewProvider { struct ContentView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ContentView() ContentView()
.modelContainer(for: [Subscription.self, Payment.self])
} }
} }

View File

@@ -1,8 +1,11 @@
import SwiftUI import SwiftUI
import SwiftData
struct HomeView: View { struct HomeView: View {
@Binding var subs: [Subscription] @Environment(\.modelContext) private var modelContext
@Query var subs: [Subscription]
@State private var showAddSubscriptionSheet = false @State private var showAddSubscriptionSheet = false
@State private var selectedSubscription: Subscription?
var body: some View { var body: some View {
NavigationView { NavigationView {
@@ -25,8 +28,12 @@ struct HomeView: View {
} }
.padding(.vertical, 8) .padding(.vertical, 8)
} }
.listRowBackground(sub.color.opacity(0.2)) .listRowBackground(sub.getColorOrAccent().opacity(0.2))
.cornerRadius(8) .cornerRadius(8)
.onTapGesture {
selectedSubscription = sub
showAddSubscriptionSheet = true
}
} }
.onDelete(perform: deleteSubscription) .onDelete(perform: deleteSubscription)
@@ -54,6 +61,7 @@ struct HomeView: View {
} }
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { Button(action: {
selectedSubscription = nil
showAddSubscriptionSheet.toggle() showAddSubscriptionSheet.toggle()
}) { }) {
Image(systemName: "plus") Image(systemName: "plus")
@@ -62,6 +70,7 @@ struct HomeView: View {
#elseif os(macOS) #elseif os(macOS)
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {
Button(action: { Button(action: {
selectedSubscription = nil
showAddSubscriptionSheet.toggle() showAddSubscriptionSheet.toggle()
}) { }) {
Image(systemName: "plus") Image(systemName: "plus")
@@ -70,14 +79,17 @@ struct HomeView: View {
#endif #endif
} }
.sheet(isPresented: $showAddSubscriptionSheet) { .sheet(isPresented: $showAddSubscriptionSheet) {
AddSubscriptionView(subs: $subs) AddSubscriptionView(subscription: $selectedSubscription)
} }
} }
} }
} }
func deleteSubscription(at offsets: IndexSet) { func deleteSubscription(at offsets: IndexSet) {
subs.remove(atOffsets: offsets) for index in offsets {
let itemToDelete = subs[index]
modelContext.delete(itemToDelete)
}
} }
func getMonthlyTotal(subs: [Subscription]) -> Float { func getMonthlyTotal(subs: [Subscription]) -> Float {
@@ -99,12 +111,7 @@ struct HomeView: View {
struct HomeView_Previews: PreviewProvider { struct HomeView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
HomeView(subs: .constant([ HomeView()
Subscription(name: "Test", payments: [Payment(amount: 9.99, intervall: .monthly, startDate: Date())], color: .blue), .modelContainer(for: [Subscription.self, Payment.self])
Subscription(name: "Fitness First", payments: [
Payment(amount: 7.9, intervall: .weekly, startDate: Date()),
Payment(amount: 29, intervall: .quarter, startDate: Date())
], color: .red)
]))
} }
} }

View File

@@ -1,7 +1,8 @@
import SwiftUI import SwiftUI
import SwiftData
struct PaymentCalendarView: View { struct PaymentCalendarView: View {
@Binding var subs: [Subscription] @Query var subs: [Subscription]
let calendar = Calendar.current let calendar = Calendar.current
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
@@ -95,7 +96,7 @@ struct PaymentCalendarView: View {
for sub in subs { for sub in subs {
for payment in sub.payments { for payment in sub.payments {
if isPaymentDue(payment: payment, for: date) { if isPaymentDue(payment: payment, for: date) {
return sub.color.opacity(0.3) return sub.getColorOrAccent().opacity(0.3)
} }
} }
} }
@@ -152,12 +153,7 @@ struct PaymentCalendarView: View {
struct PaymentCalendarView_Previews: PreviewProvider { struct PaymentCalendarView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
PaymentCalendarView(subs: .constant([ PaymentCalendarView()
Subscription(name: "Test", payments: [Payment(amount: 9.99, intervall: .monthly, startDate: Date())], color: .blue), .modelContainer(for: [Subscription.self, Payment.self])
Subscription(name: "Fitness First", payments: [
Payment(amount: 7.9, intervall: .weekly, startDate: Date()),
Payment(amount: 29, intervall: .quarter, startDate: Date())
], color: .red)
]))
} }
} }