@SectionedFetchRequest filtered using an @AppStorage value

I am learning how to implement the new @SectionedFetchRequest with my Core Data object graph.

Here is my call...

     @SectionedFetchRequest(
        sectionIdentifier: \.sectionDay,
        sortDescriptors: [
            SortDescriptor(\.dateCommences, order: .reverse),
            SortDescriptor(\.timeStampCreated, order: .reverse)],
        animation: .default)
    private var events: SectionedFetchResults<String, PTG_Event>

where PTG_Event is the Core Data entity of results that I am expecting to be returned, sectionDay is a convenience method that is prepared in an extension to PTG_Event.

I have a user setting to display archived records (or not) using the entity attribute isArchived and I am attempting to take advantage of the @AppStorage wrapper as follows...

    @AppStorage("preference_displayArchived") var kDisplayArchived = true

I cannot include a predicate in my sectioned fetch request, e.g.

        predicate: NSPredicate(format: "isArchived == %@", NSNumber(booleanLiteral: kDisplayArchived)),

... because I receive the error ...

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

I understand this, but how do I create a dynamic reference to the app storage wrapper?

I am determined to figure out an easy method to use the user setting value preference_displayArchived in the sectioned fetch request, so I have also attempted to hack a solution together using the method shown in #wwdc21-10017 for preparing a query for the .searchable modifier, but have been so far unsuccessful in figuring out how to squeeze that into my List code.

    @State private var displayArchived = false
    var queryArchived: Binding<Bool> {
        Binding {
            displayArchived
        } set: { newValue in
            displayArchived = newValue
            events.nsPredicate = newValue == false ? nil : NSPredicate(format: "isArchived == %@", NSNumber(booleanLiteral: kDisplayArchived))
        }
    }

Any suggestions?

Accepted Reply

I'm not that pleased with this solution but it works (on iOS only) and until I can find a more elegant method (that also works on macOS), this will have to suffice!

Noting that...

 @AppStorage("preference_displayArchived") var kDisplayArchived = true

Using the same call to @SectionedFetchRequest in the question, I need to complete three tasks...

1 - Update the List when it appears and when the value to the preference changes to include a predicate that will effectively filter the SectionedFetchResults (in this case SectionedFetchResults<String, PTG_Event>):

    var body: some View {
        List() {
            ....
        }
        .onAppear() {
            events.nsPredicate = predicateArchived
        }
        .onChange(of: kDisplayArchived) { _ in
            events.nsPredicate = predicateArchived
        }
    }

2 - Add a computer property for predicateArchived:

    var predicateArchived: NSPredicate {
        kDisplayArchived == true ? NSPredicate(value: true) : NSPredicate(format: "isArchived == %@", NSNumber(booleanLiteral: kDisplayArchived))
    }

3 - Finally I also have to update the search to ensure that this app preference is adopted during the search. Building on the code presented to us in WWDC21-10017 (in presentation skip to time 21:29):

    @State private var searchText = String()
    var query: Binding<String> {
        Binding {
            searchText
        } set: { newValue in
            searchText = newValue
            let predicate01 = NSPredicate(format: "name CONTAINS[cd] %@", newValue)
            let predicate02 = NSPredicate(format: "reference CONTAINS[cd] %@", newValue)
            let predicateArchived = kDisplayArchived == true ? NSPredicate(value: true) : NSPredicate(format: "isArchived == %@", NSNumber(booleanLiteral: kDisplayArchived))
            let predicateOr = NSCompoundPredicate(orPredicateWithSubpredicates: [predicate01, predicate02])
            let predicateAll = NSCompoundPredicate(andPredicateWithSubpredicates: [predicateArchived, predicateOr])
            events.nsPredicate = newValue.isEmpty ? nil : predicateAll
        }
    }

NOTES

Known issue: after cancelling a search, the List refreshes without a predicate applied. Need to develop a workaround for this inconsistency.

  • NOTE: last line of var query: Binding<String> {} should be...

    events.nsPredicate = newValue.isEmpty ? predicateArchived : predicateAll

Add a Comment

Replies

I'm not that pleased with this solution but it works (on iOS only) and until I can find a more elegant method (that also works on macOS), this will have to suffice!

Noting that...

 @AppStorage("preference_displayArchived") var kDisplayArchived = true

Using the same call to @SectionedFetchRequest in the question, I need to complete three tasks...

1 - Update the List when it appears and when the value to the preference changes to include a predicate that will effectively filter the SectionedFetchResults (in this case SectionedFetchResults<String, PTG_Event>):

    var body: some View {
        List() {
            ....
        }
        .onAppear() {
            events.nsPredicate = predicateArchived
        }
        .onChange(of: kDisplayArchived) { _ in
            events.nsPredicate = predicateArchived
        }
    }

2 - Add a computer property for predicateArchived:

    var predicateArchived: NSPredicate {
        kDisplayArchived == true ? NSPredicate(value: true) : NSPredicate(format: "isArchived == %@", NSNumber(booleanLiteral: kDisplayArchived))
    }

3 - Finally I also have to update the search to ensure that this app preference is adopted during the search. Building on the code presented to us in WWDC21-10017 (in presentation skip to time 21:29):

    @State private var searchText = String()
    var query: Binding<String> {
        Binding {
            searchText
        } set: { newValue in
            searchText = newValue
            let predicate01 = NSPredicate(format: "name CONTAINS[cd] %@", newValue)
            let predicate02 = NSPredicate(format: "reference CONTAINS[cd] %@", newValue)
            let predicateArchived = kDisplayArchived == true ? NSPredicate(value: true) : NSPredicate(format: "isArchived == %@", NSNumber(booleanLiteral: kDisplayArchived))
            let predicateOr = NSCompoundPredicate(orPredicateWithSubpredicates: [predicate01, predicate02])
            let predicateAll = NSCompoundPredicate(andPredicateWithSubpredicates: [predicateArchived, predicateOr])
            events.nsPredicate = newValue.isEmpty ? nil : predicateAll
        }
    }

NOTES

Known issue: after cancelling a search, the List refreshes without a predicate applied. Need to develop a workaround for this inconsistency.

  • NOTE: last line of var query: Binding<String> {} should be...

    events.nsPredicate = newValue.isEmpty ? predicateArchived : predicateAll

Add a Comment

IGNORE MY PREVIOUS ANSWER!!!

The following is based upon:

  • a really good answer in StackOverflow to this question "SwiftUI View and @FetchRequest predicate with variable that can change", please refer https://stackoverflow.com/a/64200159/1883707.
  • a well written blog "Swift with Majid" specifically this article https://swiftwithmajid.com/2020/01/22/optimizing-views-in-swiftui-using-equatableview/ and in particular his explanation of "Diffing in SwiftUI".

This is the correct answer for two main reasons that I can currently identify...

  1. This pattern breaks up the larger view into smaller views, which allows SwiftUI to better render and re-render.
  2. This pattern breaks out the properties that are used in the SwiftUI diffing algorithm (as noted in the article by Majid) and therefore the fetch request calls are minimised and the predicate that I need for the @AppStorage property is injected into the child view. (I still can't quite get my head entirely around this, but the pattern works perfectly. If you can better explain it, I'd be grateful for an answer or comment.)

So here is the code...

struct Accounts: View {
    @AppStorage("preference_displayArchived") var kDisplayArchived = true
    var body: some View {
        AccountsView(displayArchived: kDisplayArchived)
    }
}

struct AccountsView: View {
    let displayArchived: Bool
    var body: some View {
        AccountsList(accounts: SectionedFetchRequest(sectionIdentifier: \.sectionTypeName,
                                                     sortDescriptors: [
                                                        SortDescriptor(\.type?.name, order: .forward),
                                                        SortDescriptor(\.sortOrder, order: .forward)
                                                     ],
                                                     predicate: displayArchived == true ? NSPredicate(value: true) : NSPredicate(format: "isArchived == %@", NSNumber(booleanLiteral: displayArchived)),
                                                     animation: .default),
                     displayArchived: displayArchived
        )
    }
}

struct AccountsList : View {
    @SectionedFetchRequest var accounts: SectionedFetchResults<String, PTG_Account>
    let displayArchived: Bool
    
    @State private var searchText = String()
    var query: Binding<String> {
        Binding {
            searchText
        } set: { newValue in
            searchText = newValue
            let predicate01 = NSPredicate(format: "nameTensePresent CONTAINS[cd] %@", newValue)
            let predicate02 = NSPredicate(format: "nameTensePast CONTAINS[cd] %@", newValue)
            let predicateArchived = displayArchived ? NSPredicate(value: true) : NSPredicate(format: "isArchived == %@", NSNumber(booleanLiteral: displayArchived))
            let predicateOr = NSCompoundPredicate(orPredicateWithSubpredicates: [predicate01, predicate02])
            let predicateAll = NSCompoundPredicate(andPredicateWithSubpredicates: [predicateOr, predicateArchived])
            accounts.nsPredicate = newValue.isEmpty ? predicateArchived : predicateAll
        }
    }

    var title: String {
        return "Title For Your View"
    }

    var body: some View {
        VStack(spacing: 0) {
            ZStack(alignment: .bottom) {
                ScrollViewReader { proxy in
                    List {
                        ...
                    }
                    .onChange(of: displayArchived) { _ in
                        searchText = String()
                    }
                }
                ListFooter(countListRows: accounts.reduce(0, {$0 + $1.count}))
            }
        }
        .searchable(text: query)
        .navigationTitle(title)
        .toolbar {
            ToolbarItem(placement: .primaryAction) {
                ... 
            }
        }
    }
}

Where

  • @AppStorage("preference_displayArchived") var kDisplayArchived = true is the user setting to display archived files (in this case, in the Account List()).
  • PTG_Account is the class name for a core data entity Account.
  • .isArchived is the entity attribute of type Bool that is used to archive or unarchive an entity record (in this case, for the entity Account).