mallow
mallow

Reputation: 2836

SwiftUI: How to sum CoreData with dynamic filters?

In another question I have posted a solution for summary of CoreData with filters. The problem is: I cannot make those filters dynamic.

I have a CoreData entity: NPTransaction with attributes: date (Date) and value (Integer 64).

Working code for sum with static filter:

import SwiftUI
import CoreData

struct DashboardView: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    // FetchRequest with predicate set to "after now" 
    @FetchRequest(entity: NPTransaction.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \NPTransaction.value, ascending: false)], predicate: NSPredicate(format: "date > %@", Date() as NSDate)) var fetchRequest: FetchedResults<NPTransaction>

    // sum results using reduce
    var sum: Int64 {
        fetchRequest.reduce(0) { $0 + $1.value }
    }

    var body: some View {
        NavigationView {
            VStack(alignment: .leading) {

                HStack {
                    VStack(alignment: .leading) {
                        Text("sum")
                        Text("\(sum)")
                            .font(.largeTitle)

                    }
                    Spacer()
                }
                .padding()

                Spacer()
            }.navigationBarTitle("Title")
        }
    }

}

struct DashboardView_Previews: PreviewProvider {
    static var previews: some View {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        return DashboardView().environment(\.managedObjectContext, context)
    }
}

Now if I try to add selectedDate variable like this:

// (...)

struct DashboardView: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @State var selectedDate = Date()

    @FetchRequest(entity: NPTransaction.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \NPTransaction.value, ascending: false)], predicate: NSPredicate(format: "date > %@", selectedDate as NSDate)) var fetchRequest: FetchedResults<NPTransaction>

// (...)

I am getting an error:

Cannot use instance member 'selectedDate' within property initializer; property initializers run before 'self' is available

So I have tried to move FetchRequest to init later in the code like this:

import SwiftUI
import CoreData

struct DashboardView: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @State var selectedDate = Date()

    var fetchRequest: FetchRequest<NPTransaction>

    var body: some View {
        NavigationView {
            VStack(alignment: .leading) {

                HStack {
                    VStack(alignment: .leading) {
                        Text("sum")
                        Text("\(sum)")
                            .font(.largeTitle)

                    }
                    Spacer()
                }
                .padding()

                Spacer()
            }.navigationBarTitle("Title")
        }
    }

    init() {
        fetchRequest = FetchRequest<NPTransaction>(entity: NPTransaction.entity(), sortDescriptors: [
            NSSortDescriptor(keyPath: \NPTransaction.value, ascending: false)
        ], predicate: NSPredicate(format: "date >= %@", Date() as NSDate))

        var sum: Int64 {
            fetchRequest.reduce(0) { $0 + $1.value }
        }
    }

}

But now I am getting 2 errors. One on the line with reduce:

Value of type 'FetchRequest' has no member 'reduce'

And one on the line with Text("(sum)") :

Use of unresolved identifier 'sum'

How to solve this problem? How to sum CoreData FetchRequest with dynamic filters that users could change?


SOLUTION Based on answers from Asperi and Aspid, here is the final code that made dynamic filters work with sum of CoreData values:

DashboardView.swift

import SwiftUI

struct DashboardView: View {
    @State var selectedDate = Date()

    var body: some View {
        NavigationView {
            VStack(alignment: .leading) {

                // selectedDate change is controlled in another DateView
                DateView(selectedDate: $selectedDate)

                // selectedDate is passed to init in DashboardViewSum
                DashboardViewSum(selectedDate: selectedDate)

                Spacer()
            }.navigationBarTitle("Title")
        }
    }
}

struct DashboardView_Previews: PreviewProvider {
    static var previews: some View {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        return DashboardView().environment(\.managedObjectContext, context)
    }
}

DashboardViewSum.swift

import SwiftUI

struct DashboardViewSum: View {

    @Environment(\.managedObjectContext) var managedObjectContext   
    var fetchRequest: FetchRequest<NPTransaction>
    var sum: Int64 {
        fetchRequest.wrappedValue.reduce(0) { $0 + $1.value }
    }

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text("sum")
                Text("\(sum)")
                    .font(.largeTitle)

            }
            Spacer()
        }
        .padding()
    }

    // selectedDate is passed from DashboardView and makes predicate dynamic. Each time it is changed, DashboardViewSum is reinitialized
    init(selectedDate: Date) {
        fetchRequest = FetchRequest<NPTransaction>(entity: NPTransaction.entity(), sortDescriptors: [
            NSSortDescriptor(keyPath: \NPTransaction.value, ascending: false)
        ], predicate: NSPredicate(format: "date >= %@", selectedDate as NSDate))
    }

}

struct DashboardViewSum_Previews: PreviewProvider {
    static var previews: some View {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        return DashboardViewSum(selectedDate: Date()).environment(\.managedObjectContext, context)
    }
}

Upvotes: 1

Views: 1250

Answers (2)

Asperi
Asperi

Reputation: 257593

I assume it was meant to make sum as computable DashboardView property (out of init), as

var sum: Int64 {
    fetchRequest.wrappedValue.reduce(0) { $0 + $1.value }
}

...
init() {
   ...
}

Upvotes: 1

Aspid
Aspid

Reputation: 699

If you want to perform fetch in Init, you need to get context first. @Environment(\.managedObjectContext) var managedObjectContext does not help you, becouse its not ready yet. @FetchRequest is a wrapper that allows you to do fatches when you need result, but it doesnt perferm fetch when you define it. This code works for me, but im not realy sure whether getting context this way is realy correct and works good in all the cases:

init(){
    let context = (UIApplication.shared.delegate as! AppDelegate).persistentContaner.viewContext
    let fetchRequest: NSFetchRequest<SomeType> = SomeType.fetchRequest
    //... some order and filter
    if let result = try? context.fetch(fetchRequest){
        //result is [someClass] - do what you need
    }
}

But there is an alternative. You can extract Sum view in subView, and pass date variable through init, where you use predicate. When you change @State date, subView will initialize again and fetch will perform with new date. That looks much better.

Upvotes: 1

Related Questions