reset date button, simplified addEvent(), refactored EventListView, reindenting

This commit is contained in:
neon443
2025-04-18 16:14:38 +05:30
parent 88bb9dfaab
commit 0e1ad52f89
9 changed files with 285 additions and 351 deletions

View File

@@ -25,6 +25,7 @@
A979F6142D270AF90094C0B3 /* NearFutureWidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A979F6022D270AF00094C0B3 /* NearFutureWidgetsExtension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; A979F6142D270AF90094C0B3 /* NearFutureWidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A979F6022D270AF00094C0B3 /* NearFutureWidgetsExtension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
A979F6182D2714310094C0B3 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C28B2D24011400E4F9B1 /* Item.swift */; }; A979F6182D2714310094C0B3 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C28B2D24011400E4F9B1 /* Item.swift */; };
A985104E2DB256430013D5FF /* iCloudSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985104D2DB256430013D5FF /* iCloudSettingsView.swift */; }; A985104E2DB256430013D5FF /* iCloudSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985104D2DB256430013D5FF /* iCloudSettingsView.swift */; };
A98510502DB263F00013D5FF /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985104F2DB263F00013D5FF /* EventListView.swift */; };
A9FC7EEA2D2823920020D75B /* NearFutureWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */; }; A9FC7EEA2D2823920020D75B /* NearFutureWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -87,6 +88,7 @@
A980FC302D920097006A778F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; A980FC302D920097006A778F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A980FC372D93FB2B006A778F /* NearFutureTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NearFutureTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A980FC372D93FB2B006A778F /* NearFutureTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NearFutureTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
A985104D2DB256430013D5FF /* iCloudSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iCloudSettingsView.swift; sourceTree = "<group>"; }; A985104D2DB256430013D5FF /* iCloudSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iCloudSettingsView.swift; sourceTree = "<group>"; };
A985104F2DB263F00013D5FF /* EventListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventListView.swift; sourceTree = "<group>"; };
A9C05E412D2805D7007DC497 /* NearFutureWidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NearFutureWidgetsExtension.entitlements; sourceTree = "<group>"; }; A9C05E412D2805D7007DC497 /* NearFutureWidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NearFutureWidgetsExtension.entitlements; sourceTree = "<group>"; };
A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgets.swift; sourceTree = "<group>"; }; A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgets.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -150,6 +152,7 @@
A979F57E2D26B1300094C0B3 /* EditEventView.swift */, A979F57E2D26B1300094C0B3 /* EditEventView.swift */,
A920C2B72D2401A300E4F9B1 /* AddEventView.swift */, A920C2B72D2401A300E4F9B1 /* AddEventView.swift */,
A920C2C02D2403CA00E4F9B1 /* ContentView.swift */, A920C2C02D2403CA00E4F9B1 /* ContentView.swift */,
A985104F2DB263F00013D5FF /* EventListView.swift */,
A93BC0932D2B18A3002E8BBD /* StatsView.swift */, A93BC0932D2B18A3002E8BBD /* StatsView.swift */,
A920C2B42D2401A100E4F9B1 /* SettingsView.swift */, A920C2B42D2401A100E4F9B1 /* SettingsView.swift */,
A985104D2DB256430013D5FF /* iCloudSettingsView.swift */, A985104D2DB256430013D5FF /* iCloudSettingsView.swift */,
@@ -350,6 +353,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A920C2BB2D2401A400E4F9B1 /* AddEventView.swift in Sources */, A920C2BB2D2401A400E4F9B1 /* AddEventView.swift in Sources */,
A98510502DB263F00013D5FF /* EventListView.swift in Sources */,
A920C2C12D2403CA00E4F9B1 /* ContentView.swift in Sources */, A920C2C12D2403CA00E4F9B1 /* ContentView.swift in Sources */,
A920C2B82D2401A300E4F9B1 /* SettingsView.swift in Sources */, A920C2B82D2401A300E4F9B1 /* SettingsView.swift in Sources */,
A920C28C2D24011400E4F9B1 /* Item.swift in Sources */, A920C28C2D24011400E4F9B1 /* Item.swift in Sources */,

View File

@@ -16,7 +16,7 @@ struct AddEventView: View {
@Binding var eventCompleteDesc: String @Binding var eventCompleteDesc: String
@Binding var eventSymbol: String @Binding var eventSymbol: String
@Binding var eventColor: Color @Binding var eventColor: Color
@Binding var eventDescription: String @Binding var eventNotes: String
@Binding var eventDate: Date @Binding var eventDate: Date
@Binding var eventTime: Bool @Binding var eventTime: Bool
@Binding var eventRecurrence: Event.RecurrenceType @Binding var eventRecurrence: Event.RecurrenceType
@@ -28,7 +28,7 @@ struct AddEventView: View {
private enum Field { private enum Field {
case Name, Description case Name, Description
} }
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var body: some View { var body: some View {
@@ -79,22 +79,33 @@ struct AddEventView: View {
// dscription // dscription
ZStack { ZStack {
TextField("Event Description", text: $eventDescription) TextField("Event Description", text: $eventNotes)
.textFieldStyle(RoundedBorderTextFieldStyle()) .textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.trailing, eventDescription.isEmpty ? 0 : 30) .padding(.trailing, eventNotes.isEmpty ? 0 : 30)
.animation(.spring, value: eventDescription) .animation(.spring, value: eventNotes)
.focused($focusedField, equals: Field.Description) .focused($focusedField, equals: Field.Description)
.submitLabel(.done) .submitLabel(.done)
.onSubmit { .onSubmit {
focusedField = nil focusedField = nil
} }
MagicClearButton(text: $eventDescription) MagicClearButton(text: $eventNotes)
} }
// date picker // date picker
DatePicker("", selection: $eventDate, displayedComponents: .date) HStack {
.datePickerStyle(WheelDatePickerStyle()) DatePicker("", selection: $eventDate, displayedComponents: .date)
.datePickerStyle(WheelDatePickerStyle())
Button() {
eventDate = Date()
} label: {
Image(systemName: "arrow.uturn.left")
.resizable()
.scaledToFit()
}
.buttonStyle(BorderlessButtonStyle())
.frame(width: 20)
}
Toggle("Schedule a Time", isOn: $eventTime) Toggle("Schedule a Time", isOn: $eventTime)
if eventTime { if eventTime {
@@ -124,15 +135,17 @@ struct AddEventView: View {
if adding { if adding {
Button { Button {
viewModel.addEvent( viewModel.addEvent(
name: eventName, newEvent: Event(
complete: eventComplete, name: eventName,
completedDesc: eventCompleteDesc, complete: eventComplete,
symbol: eventSymbol, completeDesc: eventCompleteDesc,
color: ColorCodable(eventColor), symbol: eventSymbol,
description: eventDescription, color: ColorCodable(eventColor),
date: eventDate, notes: eventNotes,
time: eventTime, date: eventDate,
recurrence: eventRecurrence time: eventTime,
recurrence: eventRecurrence
)
) )
resetAddEventView() resetAddEventView()
} label: { } label: {
@@ -171,16 +184,8 @@ struct AddEventView: View {
//reset addeventView //reset addeventView
eventName = "" eventName = ""
eventSymbol = "star" eventSymbol = "star"
eventColor = [ eventColor = randomColor()
Color.red, eventNotes = ""
Color.orange,
Color.yellow,
Color.green,
Color.blue,
Color.indigo,
Color.purple
].randomElement() ?? Color.red
eventDescription = ""
eventDate = Date() eventDate = Date()
eventRecurrence = .none eventRecurrence = .none
dismiss() dismiss()
@@ -215,7 +220,7 @@ struct MagicClearButton: View {
eventCompleteDesc: .constant(""), eventCompleteDesc: .constant(""),
eventSymbol: .constant("star"), eventSymbol: .constant("star"),
eventColor: .constant(Color.red), eventColor: .constant(Color.red),
eventDescription: .constant("A very special day"), eventNotes: .constant("A very special day"),
eventDate: .constant(Date()), eventDate: .constant(Date()),
eventTime: .constant(true), eventTime: .constant(true),
eventRecurrence: .constant(.monthly), eventRecurrence: .constant(.monthly),

View File

@@ -18,16 +18,8 @@ struct ContentView: View {
@State private var eventComplete = false @State private var eventComplete = false
@State private var eventCompleteDesc = "" @State private var eventCompleteDesc = ""
@State private var eventSymbol = "star" @State private var eventSymbol = "star"
@State private var eventColor: Color = [ @State private var eventColor: Color = randomColor()
Color.red, @State private var eventNotes = ""
Color.orange,
Color.yellow,
Color.green,
Color.blue,
Color.indigo,
Color.purple
].randomElement() ?? Color.red
@State private var eventDescription = ""
@State private var eventDate = Date() @State private var eventDate = Date()
@State private var eventTime = false @State private var eventTime = false
@State private var eventRecurrence: Event.RecurrenceType = .none @State private var eventRecurrence: Event.RecurrenceType = .none
@@ -39,7 +31,7 @@ struct ContentView: View {
} else { } else {
return viewModel.events.filter { return viewModel.events.filter {
$0.name.localizedCaseInsensitiveContains(searchInput) || $0.name.localizedCaseInsensitiveContains(searchInput) ||
$0.description.localizedCaseInsensitiveContains(searchInput) $0.notes.localizedCaseInsensitiveContains(searchInput)
} }
} }
} }
@@ -125,7 +117,7 @@ struct ContentView: View {
eventCompleteDesc: $eventCompleteDesc, eventCompleteDesc: $eventCompleteDesc,
eventSymbol: $eventSymbol, eventSymbol: $eventSymbol,
eventColor: $eventColor, eventColor: $eventColor,
eventDescription: $eventDescription, eventNotes: $eventNotes,
eventDate: $eventDate, eventDate: $eventDate,
eventTime: $eventTime, eventTime: $eventTime,
eventRecurrence: $eventRecurrence, eventRecurrence: $eventRecurrence,
@@ -160,128 +152,6 @@ struct ContentView: View {
} }
} }
struct EventListView: View {
@ObservedObject var viewModel: EventViewModel
@State var event: Event
var body: some View {
NavigationLink() {
EditEventView(
viewModel: viewModel,
event: $event
)
} label: {
HStack {
RoundedRectangle(cornerRadius: 5)
.frame(width: 5)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
)
.padding(.leading, -10)
.padding(.vertical, 5)
.animation(.spring, value: event.complete)
VStack(alignment: .leading) {
HStack {
Image(systemName: event.symbol)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
)
.animation(.spring, value: event.complete)
Text("\(event.name)")
.font(.headline)
.strikethrough(event.complete)
// .foregroundStyle(
// event.complete ? .gray : .primary
// )
.animation(.spring, value: event.complete)
}
if !event.description.isEmpty {
Text(event.description)
.font(.subheadline)
.foregroundColor(.gray)
}
Text(
event.date.formatted(
date: .long,
time: event.time ? .standard : .omitted
)
)
.font(.subheadline)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
)
.animation(.spring, value: event.complete)
if event.recurrence != .none {
Text("Recurs \(event.recurrence.rawValue)")
.font(.subheadline)
.foregroundStyle(
.primary.opacity(
event.complete ? 0.5 : 1
)
)
.animation(.spring, value: event.complete)
}
}
Spacer()
VStack {
Text("\(daysUntilEvent(event.date, short: false))")
.font(.subheadline)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
)
.animation(.spring, value: event.complete)
}
Button() {
withAnimation(.spring) {
event.complete.toggle()
}
let eventToModify = viewModel.events.firstIndex() { currEvent in
currEvent.id == event.id
}
if let eventToModify = eventToModify {
viewModel.events[eventToModify] = event
viewModel.saveEvents()
viewModel.loadEvents()
}
} label: {
if event.complete {
ZStack {
Circle()
.foregroundStyle(.green)
Image(systemName: "checkmark")
.resizable()
.foregroundStyle(.white)
.scaledToFit()
.frame(width: 15)
}
} else {
Image(systemName: "circle")
.resizable()
.scaledToFit()
.foregroundStyle(event.color.color)
}
}
.buttonStyle(.borderless)
.frame(maxWidth: 25, maxHeight: 25)
.animation(.spring, value: event.complete)
}
}
}
}
struct SearchHelp: View { struct SearchHelp: View {
@Binding var searchInput: String @Binding var searchInput: String
@FocusState var focusedField: Field? @FocusState var focusedField: Field?
@@ -294,7 +164,7 @@ struct SearchHelp: View {
.padding(.trailing) .padding(.trailing)
Text("Can't find what you're looking for?") Text("Can't find what you're looking for?")
} }
Text("Tip: The Search bar searches event names and descriptions") Text("Tip: The Search bar searches event names and notes")
Button() { Button() {
searchInput = "" searchInput = ""
focusedField = nil focusedField = nil
@@ -311,22 +181,3 @@ struct SearchHelp: View {
#Preview { #Preview {
ContentView() ContentView()
} }
#Preview("EventListView") {
EventListView(
viewModel: EventViewModel(),
event:
Event(
name: "event",
complete: false,
completeDesc: "dofajiof",
symbol: "star",
color: ColorCodable(.orange),
description: "lksdjfakdflkasjlkjl",
date: Date(),
time: true,
recurrence: .daily
// )
)
)
}

View File

@@ -17,7 +17,7 @@ struct EditEventView: View {
@State private var eventCompleteDesc: String @State private var eventCompleteDesc: String
@State private var eventSymbol: String @State private var eventSymbol: String
@State private var eventColor: Color @State private var eventColor: Color
@State private var eventDescription: String @State private var eventNotes: String
@State private var eventDate: Date @State private var eventDate: Date
@State private var eventTime: Bool @State private var eventTime: Bool
@State private var eventRecurrence: Event.RecurrenceType @State private var eventRecurrence: Event.RecurrenceType
@@ -30,7 +30,7 @@ struct EditEventView: View {
_eventCompleteDesc = State(initialValue: event.wrappedValue.completeDesc) _eventCompleteDesc = State(initialValue: event.wrappedValue.completeDesc)
_eventSymbol = State(initialValue: event.wrappedValue.symbol) _eventSymbol = State(initialValue: event.wrappedValue.symbol)
_eventColor = State(initialValue: event.wrappedValue.color.color) _eventColor = State(initialValue: event.wrappedValue.color.color)
_eventDescription = State(initialValue: event.wrappedValue.description) _eventNotes = State(initialValue: event.wrappedValue.notes)
_eventDate = State(initialValue: event.wrappedValue.date) _eventDate = State(initialValue: event.wrappedValue.date)
_eventTime = State(initialValue: event.wrappedValue.time) _eventTime = State(initialValue: event.wrappedValue.time)
_eventRecurrence = State(initialValue: event.wrappedValue.recurrence) _eventRecurrence = State(initialValue: event.wrappedValue.recurrence)
@@ -40,7 +40,7 @@ struct EditEventView: View {
event.name = eventName event.name = eventName
event.symbol = eventSymbol event.symbol = eventSymbol
event.color = ColorCodable(eventColor) event.color = ColorCodable(eventColor)
event.description = eventDescription event.notes = eventNotes
event.date = eventDate event.date = eventDate
event.recurrence = eventRecurrence event.recurrence = eventRecurrence
@@ -57,32 +57,30 @@ struct EditEventView: View {
} }
var body: some View { var body: some View {
// NavigationStack { AddEventView(
AddEventView( viewModel: viewModel,
viewModel: viewModel, eventName: $eventName,
eventName: $eventName, eventComplete: $eventComplete,
eventComplete: $eventComplete, eventCompleteDesc: $eventCompleteDesc,
eventCompleteDesc: $eventCompleteDesc, eventSymbol: $eventSymbol,
eventSymbol: $eventSymbol, eventColor: $eventColor,
eventColor: $eventColor, eventNotes: $eventNotes,
eventDescription: $eventDescription, eventDate: $eventDate,
eventDate: $eventDate, eventTime: $eventTime,
eventTime: $eventTime, eventRecurrence: $eventRecurrence,
eventRecurrence: $eventRecurrence, adding: false //bc we editing existing event
adding: false //bc we editing existing event )
) .navigationTitle("Edit Event")
.navigationTitle("Edit Event") .toolbar {
.toolbar { ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .topBarTrailing) { Button() {
Button() { saveEdits()
saveEdits() } label: {
} label: { Text("Done")
Text("Done")
}
.disabled(eventName == "")
} }
.disabled(eventName == "")
} }
// } }
} }
} }
@@ -90,17 +88,7 @@ struct EditEventView: View {
EditEventView( EditEventView(
viewModel: EventViewModel(), viewModel: EventViewModel(),
event: .constant( event: .constant(
Event( EventViewModel().example
name: "Birthday",
complete: false,
completeDesc: "",
symbol: "gear",
color: ColorCodable(.red),
description: "an event",
date: Date(),
time: true,
recurrence: .yearly
)
) )
) )
} }

View File

@@ -0,0 +1,136 @@
//
// EventListView.swift
// NearFuture
//
// Created by neon443 on 18/04/2025.
//
import SwiftUI
import SwiftData
struct EventListView: View {
@ObservedObject var viewModel: EventViewModel
@State var event: Event
var body: some View {
NavigationLink() {
EditEventView(
viewModel: viewModel,
event: $event
)
} label: {
HStack {
RoundedRectangle(cornerRadius: 5)
.frame(width: 5)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
)
.padding(.leading, -10)
.padding(.vertical, 5)
.animation(.spring, value: event.complete)
VStack(alignment: .leading) {
HStack {
Image(systemName: event.symbol)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
)
.animation(.spring, value: event.complete)
Text("\(event.name)")
.font(.headline)
.strikethrough(event.complete)
.animation(.spring, value: event.complete)
}
if !event.notes.isEmpty {
Text(event.notes)
.font(.subheadline)
.foregroundColor(.gray)
}
Text(
event.date.formatted(
date: .long,
time: event.time ? .standard : .omitted
)
)
.font(.subheadline)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
)
.animation(.spring, value: event.complete)
if event.recurrence != .none {
Text("Recurs \(event.recurrence.rawValue)")
.font(.subheadline)
.foregroundStyle(
.primary.opacity(
event.complete ? 0.5 : 1
)
)
.animation(.spring, value: event.complete)
}
}
Spacer()
VStack {
Text("\(daysUntilEvent(event.date, short: false))")
.font(.subheadline)
.foregroundStyle(
event.color.color.opacity(
event.complete ? 0.5 : 1
)
)
.animation(.spring, value: event.complete)
}
Button() {
withAnimation(.spring) {
event.complete.toggle()
}
let eventToModify = viewModel.events.firstIndex() { currEvent in
currEvent.id == event.id
}
if let eventToModify = eventToModify {
viewModel.events[eventToModify] = event
viewModel.saveEvents()
viewModel.loadEvents()
}
} label: {
if event.complete {
ZStack {
Circle()
.foregroundStyle(.green)
Image(systemName: "checkmark")
.resizable()
.foregroundStyle(.white)
.scaledToFit()
.frame(width: 15)
}
} else {
Image(systemName: "circle")
.resizable()
.scaledToFit()
.foregroundStyle(event.color.color)
}
}
.buttonStyle(.borderless)
.frame(maxWidth: 25, maxHeight: 25)
.animation(.spring, value: event.complete)
}
}
}
}
#Preview("EventListView") {
EventListView(
viewModel: EventViewModel(),
event:
EventViewModel().example
)
}

View File

@@ -26,11 +26,11 @@ struct Event: Identifiable, Codable {
var completeDesc: String var completeDesc: String
var symbol: String var symbol: String
var color: ColorCodable var color: ColorCodable
var description: String var notes: String
var date: Date var date: Date
var time: Bool var time: Bool
var recurrence: RecurrenceType var recurrence: RecurrenceType
enum RecurrenceType: String, Codable, CaseIterable { enum RecurrenceType: String, Codable, CaseIterable {
case none, daily, weekly, monthly, yearly case none, daily, weekly, monthly, yearly
} }
@@ -44,16 +44,16 @@ struct ColorCodable: Codable {
//for the brainrot kids: alpha is the opacity/transparency of the color, //for the brainrot kids: alpha is the opacity/transparency of the color,
//alpha == 0 completely transparent //alpha == 0 completely transparent
//alpha == 1 completely opaque //alpha == 1 completely opaque
var color: Color { var color: Color {
Color(red: red, green: green, blue: blue, opacity: alpha) Color(red: red, green: green, blue: blue, opacity: alpha)
} }
init(_ color: Color) { init(_ color: Color) {
let uiColor = UIColor(color) let uiColor = UIColor(color)
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) uiColor.getRed(&r, green: &g, blue: &b, alpha: &a)
self.red = Double(r) self.red = Double(r)
self.green = Double(g) self.green = Double(g)
self.blue = Double(b) self.blue = Double(b)
@@ -92,22 +92,34 @@ func daysUntilEvent(_ eventDate: Date, short: Bool, sepLines: Bool = false) -> S
class EventViewModel: ObservableObject { class EventViewModel: ObservableObject {
@Published var events: [Event] = [] @Published var events: [Event] = []
@Published var icloudData: [Event] = [] @Published var icloudData: [Event] = []
@Published var example: Event = Event(
name: "event",
complete: false,
completeDesc: "dofajiof",
symbol: "star",
color: ColorCodable(.orange),
notes: "lksdjfakdflkasjlkjl",
date: Date(),
time: true,
recurrence: .daily
)
@Published var lastSync: Date? = nil @Published var lastSync: Date? = nil
@Published var icloudEventCount: Int = 0 @Published var icloudEventCount: Int = 0
@Published var localEventCount: Int = 0 @Published var localEventCount: Int = 0
@Published var syncStatus: String = "Not Synced" @Published var syncStatus: String = "Not Synced"
init() { init() {
loadEvents() loadEvents()
} }
//appgroup or regular userdefaults //appgroup or regular userdefaults
let appGroupUserDefaults = UserDefaults(suiteName: "group.com.neon443.NearFuture") ?? UserDefaults.standard let appGroupUserDefaults = UserDefaults(suiteName: "group.com.neon443.NearFuture") ?? UserDefaults.standard
//icloud store //icloud store
let icloudStore = NSUbiquitousKeyValueStore.default let icloudStore = NSUbiquitousKeyValueStore.default
// load from icloud or local // load from icloud or local
func loadEvents() { func loadEvents() {
//load icloud 1st //load icloud 1st
@@ -118,7 +130,7 @@ class EventViewModel: ObservableObject {
self.events = decodedIcEvents self.events = decodedIcEvents
} }
} }
if events.isEmpty, let savedData = appGroupUserDefaults.data(forKey: "events") { if events.isEmpty, let savedData = appGroupUserDefaults.data(forKey: "events") {
let decoder = JSONDecoder() let decoder = JSONDecoder()
if let decodedEvents = try? decoder.decode([Event].self, from: savedData) { if let decodedEvents = try? decoder.decode([Event].self, from: savedData) {
@@ -127,77 +139,55 @@ class EventViewModel: ObservableObject {
} }
updateSyncStatus() updateSyncStatus()
} }
// save to local and icloud // save to local and icloud
func saveEvents() { func saveEvents() {
let encoder = JSONEncoder() let encoder = JSONEncoder()
if let encoded = try? encoder.encode(events) { if let encoded = try? encoder.encode(events) {
appGroupUserDefaults.set(encoded, forKey: "events") appGroupUserDefaults.set(encoded, forKey: "events")
//sync //sync
icloudStore.set(encoded, forKey: "events") icloudStore.set(encoded, forKey: "events")
icloudStore.synchronize() icloudStore.synchronize()
updateSyncStatus() updateSyncStatus()
loadEvents() loadEvents()
WidgetCenter.shared.reloadAllTimelines()//reload all widgets when saving events WidgetCenter.shared.reloadAllTimelines()//reload all widgets when saving events
} }
} }
private func updateSyncStatus() { private func updateSyncStatus() {
lastSync = Date() lastSync = Date()
icloudEventCount = icloudData.count icloudEventCount = icloudData.count
localEventCount = events.count localEventCount = events.count
if icloudEventCount == localEventCount { if icloudEventCount == localEventCount {
syncStatus = "Successful" syncStatus = "Successful"
} else { } else {
syncStatus = "Pending" syncStatus = "Pending"
} }
} }
func addEvent( func addEvent(newEvent: Event) {
name: String,
complete: Bool,
completedDesc: String,
symbol: String,
color: ColorCodable,
description: String,
date: Date,
time: Bool,
recurrence: Event.RecurrenceType
) {
let newEvent = Event(
name: name,
complete: complete,
completeDesc: completedDesc,
symbol: symbol,
color: color,
description: description,
date: date,
time: time,
recurrence: recurrence
)
events.append(newEvent) events.append(newEvent)
saveEvents() //sync with icloud saveEvents() //sync with icloud
} }
func removeEvent(at index: IndexSet) { func removeEvent(at index: IndexSet) {
events.remove(atOffsets: index) events.remove(atOffsets: index)
saveEvents() //sync local and icl saveEvents() //sync local and icl
} }
func hasUbiquitousKeyValueStore() -> Bool { func hasUbiquitousKeyValueStore() -> Bool {
let icloud = NSUbiquitousKeyValueStore.default let icloud = NSUbiquitousKeyValueStore.default
let key = "com.neon443.NearFuture.testkey" let key = "com.neon443.NearFuture.testkey"
let value = "testValue" let value = "testValue"
icloud.set(value, forKey: key) icloud.set(value, forKey: key)
icloud.synchronize() icloud.synchronize()
if icloud.string(forKey: key) != nil { if icloud.string(forKey: key) != nil {
// print("has UbiquitousKeyValueStore")
icloud.removeObject(forKey: key) icloud.removeObject(forKey: key)
icloud.synchronize() icloud.synchronize()
return true return true
@@ -208,36 +198,36 @@ class EventViewModel: ObservableObject {
return false return false
} }
} }
func sync() { func sync() {
NSUbiquitousKeyValueStore.default.synchronize() NSUbiquitousKeyValueStore.default.synchronize()
loadEvents() loadEvents()
} }
func replaceLocalWithiCloudData() { func replaceLocalWithiCloudData() {
icloudStore.synchronize() icloudStore.synchronize()
self.events = self.icloudData self.events = self.icloudData
saveEvents() saveEvents()
} }
func replaceiCloudWithLocalData() { func replaceiCloudWithLocalData() {
icloudStore.synchronize() icloudStore.synchronize()
self.icloudData = self.events self.icloudData = self.events
saveEvents() saveEvents()
} }
func exportEvents() -> String? { func exportEvents() -> String? {
let encoder = JSONEncoder() let encoder = JSONEncoder()
// Custom date encoding strategy to handle date formatting // Custom date encoding strategy to handle date formatting
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
encoder.dateEncodingStrategy = .formatted(dateFormatter) encoder.dateEncodingStrategy = .formatted(dateFormatter)
do { do {
// Encode the events array to JSON data // Encode the events array to JSON data
let encodedData = try encoder.encode(events) let encodedData = try encoder.encode(events)
// Convert the JSON data to a string // Convert the JSON data to a string
if let jsonString = String(data: encodedData, encoding: .utf8) { if let jsonString = String(data: encodedData, encoding: .utf8) {
return jsonString return jsonString
@@ -250,26 +240,26 @@ class EventViewModel: ObservableObject {
return nil return nil
} }
} }
func importEvents(_ imp: String) { func importEvents(_ imp: String) {
guard let impData = imp.data(using: .utf8) else { guard let impData = imp.data(using: .utf8) else {
print("Failed to convert string to data") print("Failed to convert string to data")
return return
} }
// Create a JSONDecoder // Create a JSONDecoder
let decoder = JSONDecoder() let decoder = JSONDecoder()
// Add a custom date formatter for decoding the date string // Add a custom date formatter for decoding the date string
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" // Adjust this to the date format you're using dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" // Adjust this to the date format you're using
decoder.dateDecodingStrategy = .formatted(dateFormatter) decoder.dateDecodingStrategy = .formatted(dateFormatter)
do { do {
// Attempt to decode the events from the provided data // Attempt to decode the events from the provided data
let decoded = try decoder.decode([Event].self, from: impData) let decoded = try decoder.decode([Event].self, from: impData)
print("Successfully decoded events: \(decoded)") print("Successfully decoded events: \(decoded)")
// Save and reload after importing events // Save and reload after importing events
self.events = decoded self.events = decoded
saveEvents() saveEvents()
@@ -279,7 +269,7 @@ class EventViewModel: ObservableObject {
print("Failed to decode events: \(error.localizedDescription)") print("Failed to decode events: \(error.localizedDescription)")
} }
} }
//MARK: Danger Zone //MARK: Danger Zone
func dangerClearLocalData() { func dangerClearLocalData() {
UserDefaults.standard.removeObject(forKey: "events") UserDefaults.standard.removeObject(forKey: "events")
@@ -287,29 +277,29 @@ class EventViewModel: ObservableObject {
events.removeAll() events.removeAll()
updateSyncStatus() updateSyncStatus()
} }
func dangerCleariCloudData() { func dangerCleariCloudData() {
icloudStore.removeObject(forKey: "events") icloudStore.removeObject(forKey: "events")
icloudStore.synchronize() icloudStore.synchronize()
icloudData.removeAll() icloudData.removeAll()
updateSyncStatus() updateSyncStatus()
} }
func dangerResetLocalData() { func dangerResetLocalData() {
let userDFDict = UserDefaults.standard.dictionaryRepresentation() let userDFDict = UserDefaults.standard.dictionaryRepresentation()
for key in userDFDict.keys { for key in userDFDict.keys {
UserDefaults.standard.removeObject(forKey: key) UserDefaults.standard.removeObject(forKey: key)
} }
let appGUSDDict = appGroupUserDefaults.dictionaryRepresentation() let appGUSDDict = appGroupUserDefaults.dictionaryRepresentation()
for key in appGUSDDict.keys { for key in appGUSDDict.keys {
appGroupUserDefaults.removeObject(forKey: key) appGroupUserDefaults.removeObject(forKey: key)
} }
events.removeAll() events.removeAll()
updateSyncStatus() updateSyncStatus()
} }
func dangerResetiCloud() { func dangerResetiCloud() {
let icloudDict = icloudStore.dictionaryRepresentation let icloudDict = icloudStore.dictionaryRepresentation
for key in icloudDict.keys { for key in icloudDict.keys {
@@ -324,7 +314,7 @@ class EventViewModel: ObservableObject {
func describeOccurrence(date: Date, recurrence: Event.RecurrenceType) -> String { func describeOccurrence(date: Date, recurrence: Event.RecurrenceType) -> String {
let dateString = date.formatted(date: .long, time: .omitted) let dateString = date.formatted(date: .long, time: .omitted)
let recurrenceDescription: String let recurrenceDescription: String
switch recurrence { switch recurrence {
case .none: case .none:
recurrenceDescription = "Occurs once on" recurrenceDescription = "Occurs once on"
@@ -337,6 +327,26 @@ func describeOccurrence(date: Date, recurrence: Event.RecurrenceType) -> String
case .yearly: case .yearly:
recurrenceDescription = "Repeats every year from" recurrenceDescription = "Repeats every year from"
} }
return "\(recurrenceDescription) \(dateString)" return "\(recurrenceDescription) \(dateString)"
} }
func randomRainbowColor() -> Color {
return [
Color.red,
Color.orange,
Color.yellow,
Color.green,
Color.blue,
Color.indigo,
Color.purple
].randomElement()!
}
func randomColor() -> Color {
let r = Double.random(in: 0...1)
let g = Double.random(in: 0...1)
let b = Double.random(in: 0...1)
let a = Double.random(in: 0...1)
return Color(red: r, green: g, blue: b, opacity: a)
}

View File

@@ -23,7 +23,7 @@ struct StatsView: View {
Text("\(pastEvents.count) past event\(pastEvents.count == 1 ? "" : "s")") Text("\(pastEvents.count) past event\(pastEvents.count == 1 ? "" : "s")")
.foregroundStyle(.gray) .foregroundStyle(.gray)
} }
Section("Events by Month") { Section("Events by Month") {
let eventsByMonth = Dictionary(grouping: viewModel.events, by: { $0.date }) let eventsByMonth = Dictionary(grouping: viewModel.events, by: { $0.date })
ForEach(eventsByMonth.keys.sorted(), id: \.self) { month in ForEach(eventsByMonth.keys.sorted(), id: \.self) { month in

View File

@@ -70,7 +70,7 @@ struct iCloudSettingsView: View {
} message: { } message: {
Text("This will replace Events stored in iCloud with Events stored locally.") Text("This will replace Events stored in iCloud with Events stored locally.")
} }
Button() { Button() {
viewModel.sync() viewModel.sync()
updateStatus() updateStatus()

View File

@@ -112,12 +112,6 @@ struct EventWidgetView: View {
} }
if isLarge { if isLarge {
if !event.description.isEmpty {
Text(event.description)
.font(.caption2)
.foregroundColor(.gray)
.padding(.top, -5)
}
Text(event.date.formatted(date: .long, time: .omitted)) Text(event.date.formatted(date: .long, time: .omitted))
.font(.caption2) .font(.caption2)
.foregroundColor(event.color.color) .foregroundColor(event.color.color)
@@ -141,8 +135,6 @@ struct EventWidgetView: View {
.foregroundColor(event.color.color) .foregroundColor(event.color.color)
.padding(.trailing, -12) .padding(.trailing, -12)
} }
} else {
/*@START_MENU_TOKEN@*/EmptyView()/*@END_MENU_TOKEN@*/
} }
} }
Spacer() Spacer()
@@ -150,7 +142,6 @@ struct EventWidgetView: View {
let xMoreEvents = events.count - showedEventsNum let xMoreEvents = events.count - showedEventsNum
Text("+\(xMoreEvents) more event\(xMoreEvents == 1 ? "" : "s")") Text("+\(xMoreEvents) more event\(xMoreEvents == 1 ? "" : "s")")
.font(.caption2) .font(.caption2)
// .foregroundStyle(.gray)
.padding(.top, -5) .padding(.top, -5)
.padding(.bottom, -15) .padding(.bottom, -15)
} }
@@ -162,61 +153,10 @@ struct EventWidgetView: View {
struct Widget_Previews: PreviewProvider { struct Widget_Previews: PreviewProvider {
static var events = [ static var events = [
Event( EventViewModel().example,
name: "Event Name", EventViewModel().example,
complete: false, EventViewModel().example,
completeDesc: "", EventViewModel().example
symbol: "gear",
color: ColorCodable(.blue),
description: "Event description",
date: Date.distantFuture,
time: false,
recurrence: .yearly
),
Event(
name: "distant past",
complete: false,
completeDesc: "",
symbol: "star",
color: ColorCodable(.orange),
description: "description",
date: Date.distantPast,
time: false,
recurrence: .daily
),
Event(
name: "event",
complete: false,
completeDesc: "",
symbol: "star",
color: ColorCodable(.purple),
description: "description",
date: Date(),
time: false,
recurrence: .daily
),
Event(
name: "An event",
complete: false,
completeDesc: "",
symbol: "star",
color: ColorCodable(.green),
description: "description",
date: Date(),
time: false,
recurrence: .daily
),
Event(
name: "time event",
complete: true,
completeDesc: "",
symbol: "clock",
color: ColorCodable(.brown),
description: "an event with a time",
date: Date(),
time: true,
recurrence: .none
)
] ]
static var previews: some View { static var previews: some View {
Group { Group {