Calendar with Correlating Data. Please Help!!!

Basically I need a view with a calendar that will show data attributes from the item. I've tried two different approaches both have their listed problems. There must be a better way to do something like this. Surely it's not ideal to create a new item every time a date is opened or constantly check if something is there, but I don't know any other way.

Actual View:

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) var managedObjContext
    @Environment(\.calendar) var calenda
    @Environment(\.dismiss) var dismiss
    @FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "timestamp == %@", Date.now as CVarArg)) var items: FetchedResults<Item>
    
    @State private var date = Date.now
    
    var body: some View {
        NavigationView{
            VStack{
                DatePicker("Calendar", selection: $date, in: Date.now...,displayedComponents: [.date])
                    .datePickerStyle(.graphical)
                    .onAppear(perform: {
                        if (items.isEmpty){
                            PersistenceController().addItem(date: date, context: managedObjContext)
                        }
                    })
                    .onChange(of: date){ value in
                        items.nsPredicate=NSPredicate(format: "timestamp == %@", date as CVarArg)
                        if (items.isEmpty){
                            PersistenceController().addItem(date: date, context: managedObjContext)
                        }
                    }
                if (!items.isEmpty){
//This is the only difference in the two approaches. I just put either one of the next two blocks of code in here
                }
            }
            .navigationBarTitle("My Planner")
        }
    }
    
    func getTitle(date: Date)->String{
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return formatter.string(from: date)
    }
}

First (looks correct, but doesn't show the changes live):

PlannedMealsView(item: items[0])
Spacer()

//And then this is added at the bottom
struct PlannedMealsView: View {
    @Environment(\.managedObjectContext) var managedObjContext

    @State var item: Item
    var body: some View {
            VStack{
                Text(item.timestamp ?? Date.now, style: .date)
                    .font(.title2)
                    .bold()
                Section("Word"){
                    if(item.word != nil){
                        HStack{
                            Spacer()
                            Text(item.word!)
                            Spacer()
                            Button(action: {
                                PersistenceController().removeFromItem(item: item, context: managedObjContext)
                            }){
                                Image(systemName: "minus.circle").bold()
                            }
                            Spacer()
                        }
                    } else {
                        Button(action: {
                            PersistenceController().addToItem(item: item, context: managedObjContext)
                        }){
                            Image(systemName: "plus.circle").bold()
                                .padding(.vertical, 10)
                                .padding(.horizontal, 20)
                        }
                    }
                }
                Spacer()
            }
            .frame(height:200)
    }
}

Second (allows direct access to the objects data, but bugs after 5 or 6 date changes):

 VStack{
                            Text(items[0].timestamp ?? Date.now, style: .date)
                                .font(.title2)
                                .bold()
                            Section("Word"){
                                if(items[0].word != nil){
                                    HStack{
                                        Spacer()
                                        Text(items[0].word!)
                                        Spacer()
                                        Button(action: {
                                            PersistenceController().removeFromItem(item: items[0], context: managedObjContext)
                                        }){
                                            Image(systemName: "minus.circle").bold()
                                        }
                                        Spacer()
                                    }
                                } else {
                                    Button(action: {
                                        PersistenceController().addToItem(item: items[0], context: managedObjContext)
                                    }){
                                        Image(systemName: "plus.circle").bold()
                                            .padding(.vertical, 10)
                                            .padding(.horizontal, 20)
                                    }
                                }
                            }
                        Spacer()
                    }
                    .frame(height:200)

Unchanged Files:

Persistence-

import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "Test")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
    
    func addItem(date: Date, context: NSManagedObjectContext){
        let item = Item(context: context)
        item.timestamp = date
        item.word = nil
        
        save(context: context)
    }
    
    func addToItem(item: Item, context: NSManagedObjectContext){
        item.word = "Test"
        
        save(context: context)
    }
    
    func removeFromItem(item: Item, context: NSManagedObjectContext){
        item.word = nil
        
        save(context: context)
    }
    
    func save(context: NSManagedObjectContext){
        do {
            try context.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }
}

Data Model-

If you have any questions I'll be happy to answer. Any help is greatly appreciated. All the best!

Accepted Reply

Main Struct

@main
struct TestForWeagleWeagleApp: App {
    let persistence = PersistenceController.shared  // initiates the CoreData stack
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Persistence

import Foundation
import CoreData

class PersistenceController : ObservableObject {
    static let shared = PersistenceController()
    let container: NSPersistentContainer
    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "Test")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }

    func addItem(date: Date){
        let context = container.viewContext
        let item = Item(context: context)
        item.timestamp = date
        item.word = nil
        contextSave()
    }

    func addToItem(item: Item) {
        item.word = "Test"
        contextSave()
    }

    func removeFromItem(item: Item){
        item.word = nil
        contextSave()
    }

    func contextSave() {
        let context = container.viewContext
        if context.hasChanges {
            do {
                try context.save()
                self.objectWillChange.send()
            } catch {
                print("**** ERROR: Unable to save context \(error)")
            }
        }
    }

    func getItemsFor(_ date: Date) -> [Item] {
        let context = container.viewContext
        var request = NSFetchRequest<Item>()
        request = Item.fetchRequest()
        //request.fetchLimit = 1
        request.entity = NSEntityDescription.entity(forEntityName: "Item", in: context)
        request.predicate = NSPredicate(format: "timestamp >= %@ and timestamp <= %@", Calendar.current.startOfDay(for: date) as CVarArg, Calendar.current.startOfDay(for: date).addingTimeInterval(86399.0) as CVarArg)
        do {
            let items = try context.fetch(request)
            if items.count == 0 { return []}      
            return items.sorted(by: {$0.timestamp! > $1.timestamp!})
        } catch {
            print("**** ERROR: items fetch failed \(error)")
            return []
        }
    }
}

ContentView

import SwiftUI
struct ContentView: View {
    @ObservedObject var persistence = PersistenceController.shared
    @State private var items = PersistenceController.shared.getItemsFor(Date())
    @State private var date = Date.now
    var body: some View {
        NavigationView{
                VStack {
                    DatePicker("Calendar", selection: $date, in: Date.now...,displayedComponents: [.date])
                    .datePickerStyle(.graphical)
                    .onAppear(perform: {
                        if items.isEmpty {
                            persistence.addItem(date: date)
                            items = persistence.getItemsFor(date)
                        }
                    })

                if !(items.isEmpty) {
                    PlannedMealsView(item: items.last!)
                    Spacer()
                }
            }     
            .navigationBarTitle("My Planner")
        }
        .onChange(of: date){ newDate in
            items = persistence.getItemsFor(newDate)
            if items.isEmpty {
                persistence.addItem(date: newDate)
                items = persistence.getItemsFor(newDate)
            }
        }
    }
    func getTitle(date: Date)->String{
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return formatter.string(from: date)
    }
}

PerformanceMealsView

import SwiftUI
struct PlannedMealsView: View {
    @ObservedObject var persistence = PersistenceController.shared
    var item: Item
    @State private var forceRefresh : Bool = false
    var body: some View {
        VStack{
            Text(item.timestamp!, style: .date)
                .font(.title2)
                .bold()
            Section("Word"){
                if(item.word != nil){
                    HStack{
                        Spacer()
                        Text(item.word!)
                        Spacer()
                        Button(action: {
                            persistence.removeFromItem(item: item)
                        }){
                            Image(systemName: "minus.circle").bold()
                        }
                        Spacer()
                    }
                } else {
                    Button(action: {
                        persistence.addToItem(item: item)
                        forceRefresh.toggle()
                    }){
                        Image(systemName: "plus.circle").bold()
                            .padding(.vertical, 10)
                            .padding(.horizontal, 20)
                    }
                }
            }
            Spacer()
        }
    }
}

This works on my system, except that (probably unwisely) I used my development environment, which uses beta Xcode and beta iOS. I couldn't backwards convert the Xcode project (new format) to test on my production equipment without redoing everything.

I hope this works for you too!!! Regards, Michaela

  • AHHHH!!! This works beautifully! Well done!

Add a Comment

Replies

As per my comment on your other post, I've had a good look at your code and make the following observations:

  1. Your PersistenceController is a struct and you reference it a number of times in your Views e.g. PersistenceController().removeFromItem(item: items[0], context: managedObjContext), which means that your CoreData stack is being recreated each time (ie numerous copies) - with unpredictable results. The PersistenceController needs to be an ObservableObject class singleton, i.e. with let shared =PersistenceController(), and then refer to the shared instance.
  2. the date that you set from the calendar picker is the time of picking, i.e. date and time, so your predicate, which also uses the current date and time, will probably never match. I assume that you're looking for an item (or items) that occur on a selected day (date only, not time). The predicate therefore needs to search for a timestamp that occurs within the start and end of a day (date).
  3. It's not clear where, or if, you created the @StateObject for the @Environment(\.managedObjectContext) var managedObjContext, without which the @FetchedResults are unlikely to work (plus the problem of multiple CoreData stack instances).
  4. When the above issues are resolved, there remains the problem of getting SwiftUI to re-execute the Fetch on a change of date i.e. a dynamic predicate with immediate effect.

I've created a working version of your code, but without the FetchRequest and Results in ContentView: I use a fetch function in PersistenceController to return Items with a timestamp that falls within the specified day (midnight to 11:59:59pm). When the selected date changes, the function gets called to return the item(s) for that date. That's the item (or item array) that then gets used in your existing code.

I'll post the full solution tomorrow morning (about 00:00 UTC Sunday 26 June ) after I've further tested it.

Regards, Michaela

  • I use apples offered construction of core data so in the app file I have this: struct ReciPlannerApp: App {     let persistenceController = PersistenceController.shared     var body: some Scene {         WindowGroup {             ContentView()                 .environment(\.managedObjectContext, persistenceController.container.viewContext)         }     } }
  • I tested my code and the calendar only contains date and no time so when I use to to create and object it does it at exactly midnight and that is also what is fetched. I used to use a range in my predicates that cover the whole day but I didn't like how it looked so I just figured out what time of day is stored when you don't give it time and its midnight or something. I tested this with print by printing the date variable and the items[0].date and they matched perfectly
  • And yeah I believe 4. is the defining problem, unfortunately. I'm very curious to see what you've done. To solve this.

Add a Comment

Main Struct

@main
struct TestForWeagleWeagleApp: App {
    let persistence = PersistenceController.shared  // initiates the CoreData stack
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Persistence

import Foundation
import CoreData

class PersistenceController : ObservableObject {
    static let shared = PersistenceController()
    let container: NSPersistentContainer
    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "Test")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }

    func addItem(date: Date){
        let context = container.viewContext
        let item = Item(context: context)
        item.timestamp = date
        item.word = nil
        contextSave()
    }

    func addToItem(item: Item) {
        item.word = "Test"
        contextSave()
    }

    func removeFromItem(item: Item){
        item.word = nil
        contextSave()
    }

    func contextSave() {
        let context = container.viewContext
        if context.hasChanges {
            do {
                try context.save()
                self.objectWillChange.send()
            } catch {
                print("**** ERROR: Unable to save context \(error)")
            }
        }
    }

    func getItemsFor(_ date: Date) -> [Item] {
        let context = container.viewContext
        var request = NSFetchRequest<Item>()
        request = Item.fetchRequest()
        //request.fetchLimit = 1
        request.entity = NSEntityDescription.entity(forEntityName: "Item", in: context)
        request.predicate = NSPredicate(format: "timestamp >= %@ and timestamp <= %@", Calendar.current.startOfDay(for: date) as CVarArg, Calendar.current.startOfDay(for: date).addingTimeInterval(86399.0) as CVarArg)
        do {
            let items = try context.fetch(request)
            if items.count == 0 { return []}      
            return items.sorted(by: {$0.timestamp! > $1.timestamp!})
        } catch {
            print("**** ERROR: items fetch failed \(error)")
            return []
        }
    }
}

ContentView

import SwiftUI
struct ContentView: View {
    @ObservedObject var persistence = PersistenceController.shared
    @State private var items = PersistenceController.shared.getItemsFor(Date())
    @State private var date = Date.now
    var body: some View {
        NavigationView{
                VStack {
                    DatePicker("Calendar", selection: $date, in: Date.now...,displayedComponents: [.date])
                    .datePickerStyle(.graphical)
                    .onAppear(perform: {
                        if items.isEmpty {
                            persistence.addItem(date: date)
                            items = persistence.getItemsFor(date)
                        }
                    })

                if !(items.isEmpty) {
                    PlannedMealsView(item: items.last!)
                    Spacer()
                }
            }     
            .navigationBarTitle("My Planner")
        }
        .onChange(of: date){ newDate in
            items = persistence.getItemsFor(newDate)
            if items.isEmpty {
                persistence.addItem(date: newDate)
                items = persistence.getItemsFor(newDate)
            }
        }
    }
    func getTitle(date: Date)->String{
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return formatter.string(from: date)
    }
}

PerformanceMealsView

import SwiftUI
struct PlannedMealsView: View {
    @ObservedObject var persistence = PersistenceController.shared
    var item: Item
    @State private var forceRefresh : Bool = false
    var body: some View {
        VStack{
            Text(item.timestamp!, style: .date)
                .font(.title2)
                .bold()
            Section("Word"){
                if(item.word != nil){
                    HStack{
                        Spacer()
                        Text(item.word!)
                        Spacer()
                        Button(action: {
                            persistence.removeFromItem(item: item)
                        }){
                            Image(systemName: "minus.circle").bold()
                        }
                        Spacer()
                    }
                } else {
                    Button(action: {
                        persistence.addToItem(item: item)
                        forceRefresh.toggle()
                    }){
                        Image(systemName: "plus.circle").bold()
                            .padding(.vertical, 10)
                            .padding(.horizontal, 20)
                    }
                }
            }
            Spacer()
        }
    }
}

This works on my system, except that (probably unwisely) I used my development environment, which uses beta Xcode and beta iOS. I couldn't backwards convert the Xcode project (new format) to test on my production equipment without redoing everything.

I hope this works for you too!!! Regards, Michaela

  • AHHHH!!! This works beautifully! Well done!

Add a Comment