0

Summary

Hi! I’m looking for the best way to get a Core Data entity – selected by the user – everywhere in my SwiftUI app. The app has a fairly complex view structure with multiple tabs, modals and navigation stack levels.

As you can see below, the model is structured in a way that all entities are in some way related to one entity (Home). It’s possible to have multiple Home entities, though. The user can then select which Home they want to see and the whole app reloads it’s content based on that selection. The selection is also saved in User Defaults to be persisted during launches.

So, as I basically need the selected Home in (almost) every view, I don’t want to pass it down as a parameter of every single view that is loaded.

Core Data Structure

enter image description here

Code Structure

Currently I’m using a DataHandler() manager class that (creates) and holds the selected Home as a @Published variable. This variable is an optional which allows me to handle the onboarding if users haven’t created a Home yet.

@main
struct MyApp: App {
    let persistenceController = PersistenceController.shared
    
    // My custom manager class
    @StateObject var dataHandler = DataHandler()

    var body: some Scene {
        WindowGroup {
            ContentView()
                    .environment(\.managedObjectContext, persistenceController.container.viewContext)
                    .environmentObject(dataHandler)
        }
    }
}
class DataHandler: ObservableObject {
    // Variable that holds the user selected Home entity
    @Published var selectedHome: Home?
    
    // User Defaults
    @AppStorage("homeID") private var homeID: UUID?
    
    
    init() {
        // Check if User Default exists
        if homeID != nil {
            // Search for Home with User Default ID
            selectedHome = fetchHomeBy(id: homeID!)
            
            // Check if Home with ID exists
            if selectedHome == nil {
                loadFallbackHome()
            }
        } else {
            // No UserDefault
            loadFallbackHome()
        }
    }


    // more stuff …
}

To access the selected Home in the given view I’m then using an @EnvironmentObject. I then want to filter my other entity types for this very selection in a @FetchRequest or @SectionedFetchRequest. The fetch requests may also filter for additional variables or custom sorting.

struct EventsTabView: View {
    @EnvironmentObject var dataHandler: DataHandler

    @SectionedFetchRequest private var events: SectionedFetchResults<String, Event>
    
    init() {
        // Fetching Events of selected Home that aren’t archived
        _events = SectionedFetchRequest(entity: Event.entity(),
                                        sectionIdentifier: \.startYear,
                                        sortDescriptors: [NSSortDescriptor(keyPath: \Event.startDate, ascending: false)],
                                        predicate: NSPredicate(format: "eventHome == %@, isArchived != true", DataHandler().selectedHome!))
    }
    
    var body: some View {
        // …
    }
}

Problem

The problem I’m currently running into is that I can’t access the manager class in the @FetchRequest. I need this to filter the Meters and Events for the selected Home and additional filter parameters, though!

I’ve learned that @EnvironmentObjects can’t be used in a custom init(). And the way I currently handle the predicate of the fetch request isn’t great either as it always creates a new instance of DataHandler().

I’ve tried putting the fetch requests in computed variables that returns an array of the entities I need. The problem then is that my UI doesn’t correctly update when adding/deleting/editing the data.

I also thought about using a derived Home.id attribute in every other entity. This way I could only store the selected Home ID in the @Published var. I guess that’s better performance wise? Though that still begs the question how to access this variable in the @FetchRequests then?

So my questions would be:

  1. How can I access an app wide variable in the dynamic fetch requests of my init()?
  2. Is there a better design to handle a user selection like this instead of loading the whole entity into a @Published variable of an @EnvironmentObject class?
  3. Is using computed variables to store the fetch request results worse (performance wise) than doing the fetch request in the views init()?

PS: I’m fairly new to SwiftUI, so I would be so thankful if someone can point me into a direction or can point out a better solution. :)

Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
alexkaessner
  • 1,966
  • 1
  • 14
  • 39
  • 1
    I still think an EnvironmentObject is the way to go and you don’t have to do everything in the view init, you could use the onAppear modifier for instance to set the correct predicate for your fetch request – Joakim Danielson Apr 05 '23 at 10:41
  • @JoakimDanielson So I’d do the whole fetch request in `.onAppear` then? I’ve never used it before, so does this also update the fetch request when things get added/deleted/changed in the Core Data model? – alexkaessner Apr 05 '23 at 10:52
  • No you declare it as normal, it’s the predicate you set in onAppear. – Joakim Danielson Apr 05 '23 at 10:54
  • @JoakimDanielson I’ve tried this now: `.onAppear { events.nsPredicate = NSPredicate(format: "eventHome == %@", dataHandler.selectedHome!) }`. The problem is that it shows all unfiltered Events for a second when showing the view. Also changing my Home selection doesn’t update the fetch request. I wanted to use `.onChange`, but that won’t let me listen to my `@Published` variable of my manager class. – alexkaessner Apr 05 '23 at 13:53
  • You can fix the first problem by giving the fetch request a default predicate, `NSPredicate(value: false)` – Joakim Danielson Apr 05 '23 at 13:58
  • @JoakimDanielson Hmm, now my empty view gets show for a split second, because I’m checking if the fetched results are empty to show it. Also I’m still not getting changes when I change the Home selection. Is there maybe another way to do this? – alexkaessner Apr 05 '23 at 14:16
  • FetchRequest is far from perfect, maybe the best way forward is to move away from it and use NSFetchRequest instead in a separate class (like DataHandler) so you have more control over how and when the request is called. – Joakim Danielson Apr 05 '23 at 14:21

1 Answers1

1

If I have an object that I want to access anywhere in a complex app, I've adopted the approach of creating an extension on NSApplication. I then set up the wanted object as a property on NSApplication.

This means that anywhere in my code, I can access the wanted object by just going through NSApp which, of course is a globally available NSApplication object.

To my mind, it's the cleanest, simplest approach.
Dave

Dave Jewell
  • 190
  • 1
  • 7
  • Isn’t the `NSApplication` an AppKit class? My app is running on iOS using SwiftUI, though. Do you know an adopted approach for this? – alexkaessner Apr 05 '23 at 10:50
  • @alexkaessner If you want an adopted solution for this then create a singleton class and add your property to it. – Joakim Danielson Apr 05 '23 at 10:59
  • If you want to adopt this approach to iOS, you can surely just add an extension to UIApplication which you would then reference via UIApplication.shared. If you put, (eg) Text(UIApplication.shared.description) into your ContentView, you're essentially just referencing the UIApplication singleton. By using an extension, you can hang whatever you like off that singleton. – Dave Jewell Apr 05 '23 at 14:43
  • @DaveJewell I’ve now created a singleton for my manager class by adding `static let shared = DataHandler()`. Should also work instead of UIApplication , right? Though, if I then use `DataHandler.shared.selectedHome`in the `NSPredicate` from above, it doesn’t update the fetch request if something changes in the UI. Am I missing something? – alexkaessner Apr 05 '23 at 16:40
  • @alexkaessner Hi Alex. I was only addressing the issue of how to cleanly access a global from anywhere within an app. As I mentioned, one can easily add extensions to UIApplication or even - if necessary - subclass UIApplication. But if you have special requirements, such as working with ObservableObject, then a custom class + singleton is probably a better solution. I'm sorry I can't help with your UI update issue. I can only suggest using breakpoints or print statements to narrow down the problem. – Dave Jewell Apr 06 '23 at 18:08