1

I'm trying to do a fairly simple View with MVVM to be a good ViewModel citizen. However, the code breaks while accessing the @Enviromnent Core Data in the ViewModel. I created two functions in the ViewModel. One accesses Core Data through the @Environment and one accesses Core Data with the old style - get a reference to AppDelegate and do my own thing. The OldSchool method works. Comment 2 below. The @Environment does not - it breaks at the line indicated below with an error that is not helpful for me. Comment 1. (Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0))

Now if I take the same @Environment code and put it directly into the view it Works. And if I call the same line that breaks the MVVM in a Text in the View I get the correct response. Comment 2

This is the view:

struct UserUtilities: View {

@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(fetchRequest: DermPhoto.getAllDermPhotos()) var dermPhotos: FetchedResults<DermPhoto>

@State private var reminderInterval = 1
@State private var enableReminders = true
@State private var daysSincePhoto: Int = 0

@ObservedObject var userUtilitiesVM = UserUtilitiesViewModel()

var body: some View {
    NavigationView {
        VStack {
            Group { //group 1
            Toggle(isOn: $enableReminders) {
                Text("Enable Reminders")
            }
            .padding(EdgeInsets(top: 50, leading: 50, bottom: 0, trailing: 50))
            Text("Reminders are" + (enableReminders == true ? " On" : " Off"))
            Spacer()

            //Comment 3 - this always works   
            Text("String interpolation of self.dermPhotos.count")
                Text("\(self.dermPhotos.count)")
            }  //group 1

            Group { //group 2
            Text("It has been " + "\(self.daysSincePhoto) " + (daysSincePhoto == 1 ? "day" : "days") + " since a photo was added.")
            .padding(EdgeInsets(top: 0, leading: 50, bottom: 0, trailing: 50))
                //options for the sentence above
                //\(self.userUtilitiesVM.getTheDateInterval())
                //\(self.userUtilitiesVM.getFromEnvironment())
                //\(self.userUtilitiesVM.dateInterval)
            Spacer()
            Stepper("Reminder Interval", value: $reminderInterval, in: 1 ... 30)
            .padding(EdgeInsets(top: 0, leading: 50, bottom: 0, trailing: 50))
            Text("Reminder Interval is: \(reminderInterval)" + (reminderInterval == 1 ? " day" : " days"))
            Spacer()

        }//group 2
        }
        .navigationBarTitle("Reminder Days", displayMode: .inline)
        .navigationBarItems(trailing: Button(action: {
            print("getting the date interval from the nav button")
            //self.daysSincePhoto = self.userUtilitiesVM.getOldSchool()
            self.daysSincePhoto = self.userUtilitiesVM.getFromEnvironment()
            //self.daysSincePhoto = self.getFromEnvironment()
        } , label: { Text("Fetch")
        }))
    }
}

//this always works
func getFromEnvironment() -> Int {

    let numberOfRecords = self.dermPhotos.count

    if numberOfRecords > 0 {
        let now = Date()
        let lastDate = self.dermPhotos.last?.addDate
        //ok to bang - addDate is always added to core data
        let dateInterval = DateInterval(start: lastDate!, end: now)
        let days = Int(dateInterval.duration) / (24 * 3600)
        self.daysSincePhoto = days
        return days
    }
    return 0
}
}

And this is the ViewModel:

class UserUtilitiesViewModel: ObservableObject {
    @Environment(\.managedObjectContext) var managedObjectContext
    @FetchRequest(fetchRequest: DermPhoto.getAllDermPhotos()) var dermPhotos: FetchedResults<DermPhoto>
    @Published var dateInterval: Int = 50

    //Comment 2 this always works
    func getOldSchool() -> Int { 
        let kAppDelegate = UIApplication.shared.delegate as! AppDelegate
        let context = kAppDelegate.persistentContainer.viewContext
        var resultsDermPhotos : [DermPhoto] = []

        let fetchRequest: NSFetchRequest<DermPhoto> = DermPhoto.fetchRequest() as! NSFetchRequest<DermPhoto>
        let sortDescriptor = NSSortDescriptor(key: "addDate", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor]

        do {
            resultsDermPhotos = try context.fetch(fetchRequest)
        } catch {
            print("the fetchRequest error is \(error.localizedDescription)")
        }

        let numberOfRecords = resultsDermPhotos.count
        if numberOfRecords > 0 {
            let now = Date()
            let lastDate = resultsDermPhotos.last?.addDate
            //ok to bang this since addDate is always added to core data
            let di = DateInterval(start: lastDate!, end: now)
            let days = Int(di.duration) / (24 * 3600)
            dateInterval = days
            return days
        }
        return 0
    }

    //this never works
    func getFromEnvironment() -> Int {

        //Comment 1 - this breaks
        let numberOfRecords = self.dermPhotos.count

        if numberOfRecords > 0 {
            let now = Date()
            let lastDate = self.dermPhotos.last?.addDate
            //ok to bang this since addDate is always added to core data
            let di = DateInterval(start: lastDate!, end: now)
            let days = Int(di.duration) / (24 * 3600)
            dateInterval = days

            return days
        }

        return 0
    }
}

Clearly I can use old school or abandon the ViewModel idea but I would like to know how to fix this. Any guidance would be appreciated. Xcode 11.3 (11C29)

JohnSF
  • 3,736
  • 3
  • 36
  • 72

1 Answers1

0

I don't think you can access @Environment from outside View hierarchy.

Your view model does not conform to view. But it won't be view model once it conforms to it.

=> you should abandon view model idea

Jim lai
  • 1,224
  • 8
  • 12