mallow
mallow

Reputation: 2856

Core Data in SwiftUI - How to use @AppStorage as a value in predicate?

How to add @AppStorage variable to init() to use it as a key in @FetchRequest predicate?

Here is a working example, where I filter results using predicate with fixed keyword "A".

ContentView.swift

import SwiftUI
import CoreData

struct Settings {
    static let filter = "filter"
}

struct ContentView: View {
    @AppStorage(wrappedValue: "B", Settings.filter) var filter
    
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest private var items: FetchedResults<Item>
    init() {
        let request: NSFetchRequest<Item> = Item.fetchRequest()
        
        // this works
        request.predicate = NSPredicate(format: "name == %@", "A")
        
        // this gives an error "Variable 'self.items' used before being initialized"
        //request.predicate = NSPredicate(format: "name == %@", filter)
        
        request.sortDescriptors = [NSSortDescriptor(keyPath: \Item.name, ascending: true)]
        _items = FetchRequest(fetchRequest: request)
    }
    
    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    Text(item.name ?? "x")
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
            Text("Select an item")
            
        }
    }
    
    private func addItem() {
        withAnimation {
            let newItem1 = Item(context: viewContext)
            newItem1.name = "A"
            
            let newItem2 = Item(context: viewContext)
            newItem2.name = "B"
            
            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
    
    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)
            
            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

Persistence.swift

import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        let newItem1 = Item(context: viewContext)
        newItem1.name = "A"
        let newItem2 = Item(context: viewContext)
        newItem2.name = "B"
        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    let container: NSPersistentContainer

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

I have one Entity: Item. With "name" - a string type attribute. Everything works when predicate is a fixed "A" keyword. But when I'm trying to use filter variable (from @AppStorage) I am getting and error:

Variable 'self.items' used before being initialized

It's when I'm using this:

request.predicate = NSPredicate(format: "name == %@", filter)

instead of this:

request.predicate = NSPredicate(format: "name == %@", "A")

Upvotes: 2

Views: 618

Answers (2)

malhal
malhal

Reputation: 30582

You can use Apples recommended pattern to separate the fetch from the results and that allows you to adjust the predicate and sort just before results are requested, eg

@AppStorage("B") var filter = Settings.filter

private let request = FetchRequest<Item>()
private var items: FetchedResults<Item> {
    request.predicate = NSPredicate(format: "name == %@", filter)
    request.sortDescriptors = [NSSortDescriptor(keyPath: \Item.name, ascending: true)]
    return request.wrappedValue
}

Upvotes: 0

Asperi
Asperi

Reputation: 257749

Move everything into separated view and inject filter in body. Here is a schema:

struct ContentView: View {
    @AppStorage(wrappedValue: "B", Settings.filter) var filter

    // Variant to use UserDefaults in init
    init() {
        let storedFilter: String = UserDefaults.standard.string(forKey: Settings.filter) ?? "B"
           // do something here
    }

    var body: some View {
       // Move everything into `FilteredContentView` and then `filter`
       // will be available in `FilteredContentView.init` where you
       // inject it into `predicate`
       FilteredContentView(filter: filter)   // << here !!
    }
}

Upvotes: 1

Related Questions