TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: Trying to access managed object context through a model

Forums > Swift

Hi guys,

I am trying to seperate the code in my View into a View and a model, and came up with the following model:

extension TaskView {
    @MainActor class TaskModel : ObservableObject {
        @Environment(\.dismiss) var dismiss
        @Environment(\.managedObjectContext) var moc

        @Published var taskName : String = ""
        @Published var taskDate : Date = Date()
        @Published var taskTag : String = ""
        @Published var showingAlert = false

        func saveTask() {
            if taskName != "" {
                let newTask = Task(context: moc)
                newTask.name = taskName
                newTask.date = taskDate
                newTask.tag = taskTag
                try? moc.save()
                dismiss()
            }
            else {
                showingAlert.toggle()
            }
        }
    }
}

However when I modify TaskView to use this model as a @StateObject, I get the error "Accessing Environment<NSManagedObjectContext>'s value outside of being installed on a View. This will always read the default value and will not update." What does this error mean, and what do I need to do to resolve it? My full repo is here: https://github.com/aabagdi/TaskMan/

Thanks for any help!

3      

Hi again!

Let’s start with the idea of Environment. When you create and start your very first View in SwiftUI, the framework generates Environment for it. SwiftUI creates it automatically, and we don’t need to do something. SwiftUI uses Environment to pass system-wide settings like ContentSizeCategory, LayoutDirection, ColorScheme, etc. Environment also contains app-specific stuff like UndoManager and NSManagedObjectContext. So you cannot create Environment vars from classes, because Environment belongs to Views.

Also, Xcode autogenerates Task class from your data model (from file TaskDataModel) and those data objects inherit ObservableObject. This means you see changes immediately published to your UI when data changes. So you don't need to create again TaskModel as it already generated by XCode as Task class behind the scenes.

Also these items belong to the view not Task, as they are connected to fields on the View itself where you insert your data. So they should stay @State var in View itself.

@Published var taskName : String = ""
@Published var taskDate : Date = Date()
@Published var taskTag : String = ""
@Published var showingAlert = false

You may want to create extension to Task instead to make it more view ready like so. Create a file Task extension:

extension Task {
    var viewName: String {
        name ?? "No task name"
    }

    var viewDate: Date {
        date ?? Date()
    }

    var viewTag: String {
        tag ?? ""
    }
}

so you can use those items without need to use nil coalescing.

To simplify your MainView you can also extract some part of code into separate struct or even file like so. So it will be easier for you to read.

struct MainView: View {
    @State var showTask = false
    @FetchRequest(sortDescriptors: []) var taskArray: FetchedResults<Task>

    var body: some View {
        VStack {
                List(taskArray) { task in
                    TaskRow(task: task)
                }

                Button("Add a new task") {
                    showTask.toggle()
                }
                .sheet(isPresented: $showTask) {
                    TaskView()
                        .presentationDetents([.fraction(0.305)])
                        .presentationDragIndicator(.hidden)
                }
                .navigationTitle("Your tasks")
                .navigationBarTitleDisplayMode(.inline)
                .ignoresSafeArea()
        }
    }

}

struct TaskRow: View {
    let task: FetchedResults<Task>.Element
    @Environment(\.managedObjectContext) var moc

    var body: some View {
        HStack {
            Image(systemName: "square")
                .onTapGesture {
                    delete(task: task)
                }
            VStack(alignment: .leading) {
                Text(task.viewName)
                Text(task.viewDate.formatted())
                    .font(.caption)
            }
            .fontWeight(.bold)
            Spacer()
            Text(task.viewTag)
                .font(.caption)
        }
    }

    private func delete(task: FetchedResults<Task>.Element) {
        moc.delete(task)
        try? moc.save()
    }
}

There are many many other options of course. All is up to you how you would like to see you project. Also some logic can be moved into an observable object instead. But this is another huge topic, and for such simple project it might be more pain than gain.

4      

If you wish to move all logic to your observable object you can refactor your code as follows:

  1. Create ObservableObject similar to this. This is where all your logic will work. Import CoreData as well.

    class TaskObservableObject: ObservableObject {
    let moc: NSManagedObjectContext
    @Published var tasks: [Task] = []
    
    init(moc: NSManagedObjectContext) {
        self.moc = moc
    }
    
    // Populate @Published var tasks from core data
    func fetchData() {
        let request = Task.fetchRequest()
        if let tasks = try? moc.fetch(request) {
            self.tasks = tasks
        }
    }
    
    func addTask(name: String, date: Date, tag: String?) {
        let newTask = Task(context: moc)
        newTask.name = name
        newTask.date = date
        newTask.tag = tag
    
        do {
            try moc.save()
            tasks.append(newTask)
        } catch {
            print("Failed to add task")
        }
    }
    
    func deleteTask(task: Task) {
        // first you need to remove from moc
        moc.delete(task)
    
        do {
            try moc.save()
    
            // then you need to remove it from @Published var tasks: [Task] as this is where your MainView use this data
            if let index = tasks.firstIndex(where: { task.id == $0.id }) {
                tasks.remove(at: index)
            }
        } catch {
            print("Error deleting task")
        }
    }
    }
  2. Add this to your TaskManApp file.
@main
struct TaskManApp: App {
    // Instantiate moc upon launch of app
    let moc = DataController().container.viewContext

    var body: some Scene {
        WindowGroup {
            MainView()
                .environmentObject(TaskObservableObject(moc: moc)) // inject into environment and pass moc to observable object
        }
    }
}
  1. MainView needs to use object from Environment like so.
struct MainView: View {
    @EnvironmentObject var viewModel: TaskObservableObject
    @State var showTask = false

    var body: some View {
        VStack {
            List(viewModel.tasks) { task in
                TaskRow(task: task)
            }

            Button("Add a new task") {
                showTask.toggle()
            }
            .sheet(isPresented: $showTask) {
                TaskView()
                    .presentationDetents([.fraction(0.305)])
                    .presentationDragIndicator(.hidden)
            }
            .navigationTitle("Your tasks")
            .navigationBarTitleDisplayMode(.inline)
            .ignoresSafeArea()
        }
        .task {
            viewModel.fetchData()
        }
    }

}

struct TaskRow: View {
    @EnvironmentObject var viewModel: TaskObservableObject
     let task: Task

    var body: some View {
        HStack {
            Image(systemName: "square")
                .onTapGesture {
                    viewModel.deleteTask(task: task)
                }
            VStack(alignment: .leading) {
                Text(task.viewName)
                Text(task.viewDate.formatted())
                    .font(.caption)
            }
            .fontWeight(.bold)
            Spacer()
            Text(task.viewTag)
                .font(.caption)
        }
    }
}
  1. TaskView needs to use object from Environment like so.
struct TaskView : View {
    @Environment(\.dismiss) var dismiss
    @EnvironmentObject var viewModel: TaskObservableObject

    @State var taskName : String = ""
    @State var taskDate : Date = Date()
    @State var taskTag : String = ""
    @State private var showingAlert = false

    var body: some View {
        VStack {
            List {
                TextField("Task name", text: $taskName)

                DatePicker(
                    "Task time",
                    selection: $taskDate,
                    in: Date.now...,
                    displayedComponents: [.date, .hourAndMinute])

                TextField("Tag (Optional)", text: $taskTag)
            }
            Button("Save Task") {
                if taskName != "" {
                    viewModel.addTask(name: taskName, date: taskDate, tag: taskTag)
                    dismiss()
                }
                else {
                    showingAlert.toggle()
                }
            }
            .alert(isPresented: $showingAlert) {
                Alert(title: Text("Hey!"), message: Text("Your task is missing a name!"), dismissButton: .default(Text("Got it!")))
            }
        }
    }
}
  1. TaskExtension stays as in my previous post.
  2. You can remove TaskModel file from your project as you won't need that.

PS. This is not the only way to arrange this. So other approaches are as good as this one :)

4      

Wow, thanks so much for the detailed answer! The stuff with Environment and State makes sense, and thanks so much for the suggested improvments too!

3      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.