0

Scenario

I have implemented a parent view which contains a list of CoreData objects (populated using a predicate and @FetchRequest) and a toolbar. If the user scrolls down in the list, the toolbar is hidden in any case. If the user scrolls up, the toolbar is shown again. As the user can also scroll to the top by tapping the App's title bar (iOS default behaviour), the list in the subview reports "firstElementIsVisible = true" once its first element has appeared. This is my current code:

Main view (parent view)

struct MainViewTabItem: View, Equatable { // Equatable is used for other purposes (identifying tabs)
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@StateObject var modelTab: MainViewTabItemViewModel // ViewModel which contains search predicates

@State var lastHandledMainViewScrollGestureTime: Date?
@State var hideToolBar: Bool = false
@State var firstElementIsVisible: Bool = true

var body: some View {
    GeometryReader {geo in
        ZStack(alignment: .top) {
            VStack{
                // MARK: Toolbar
                if !hideToolBar {
                    MainViewToolbarView(modelTab: modelTab)
                }
                // MARK: - List (subview)
                MainListView(modelMain: MainViewModel.shared,
                             predicate: Card.predicate(searchText:              modelTab.searchText,
                                                       isPinned:                modelTab.filterIsPinned),
                             sortDescriptor: CardSort(sortType: modelTab.cardSortType, sortOrder: modelTab.cardSortOrder).sortDescriptor,
                             firstElementIsVisible: $firstElementIsVisible.onChange(firstElementIsVisibleChanged))
                
                .simultaneousGesture(DragGesture().onChanged({ gesture in
                    // Hide/Show toolbar if scrolled up/down
                    if let lastHandledMainViewScrollGestureTime = lastHandledMainViewScrollGestureTime, gesture.time == lastHandledMainViewScrollGestureTime {
                        // Don't handle gesture as it has been handled before
                    }
                    let isScrollingDown = gesture.location.y > gesture.predictedEndLocation.y
                    hideToolBar = isScrollingDown
                }))
            }
        }
    }
}

func firstElementIsVisibleChanged(to value: Bool) {
    // Uses this great Binding-Extension: https://www.hackingwithswift.com/quick-start/swiftui/how-to-run-some-code-when-state-changes-using-onchange
    if value {
        hideToolBar = false
    }
}}

List (subview)

struct MainListView: View {
@StateObject var modelMain: MainViewModel

@FetchRequest(
    entity: Card.entity(),
    sortDescriptors: [
        NSSortDescriptor(keyPath: \Card.dateCreated, ascending: false)
    ]
)
private var result: FetchedResults<Card>

@Binding var firstElementIsVisible: Bool

init(modelMain: MainViewModel,
     predicate: NSPredicate?,
     sortDescriptor: NSSortDescriptor,
     firstElementIsVisible: Binding<Bool>) {
    
    _modelMain = StateObject(wrappedValue: modelMain)
    
    let fetchRequest = NSFetchRequest<Card>(entityName: Card.entity().name ?? "Card")
    fetchRequest.sortDescriptors = [sortDescriptor]
    
    if let predicate = predicate {
        fetchRequest.predicate = predicate
    }
    _result = FetchRequest(fetchRequest: fetchRequest)        
    _firstElementIsVisible = firstElementIsVisible        
}

var body: some View {ScrollViewReader { proxy in
        ScrollView{
            LazyVStack {
                    ForEach(result) { (card: Card) in                            
                        Text(card.idNumber.description
                        .onAppear{
                            if let firstItem = result.first, firstItem.id == card.id {
                                firstElementIsVisible = true
                            }
                        }                            
                    }
                }
        }
        
    }
}}

Question

The above code works, however, a new FetchRequest fires each time the "hideToolBar" State of the parent view changes despite the MainViewModel's parameters used for the fetch predicate not changing (in some cases even leading to an endless loop). Is there any way to prevent these fetches or is this the way SwiftUI views work (e.g. a FetchRequest fires if the view moves)?

Many thanks!

Sebastian
  • 115
  • 1
  • 8
  • 1
    You recreate FetchRequest manually in `init`, so no surprise, because each time parent body is refreshed MainListView init is called to verify equality (because it is a value), but you construct new fetch request in init, so it is definitely not equal and so forth. – Asperi Jul 24 '22 at 14:05
  • @Asperi Thank you! I amended my code so that 1. the predicate is composed in the ViewModel 2. The. parent view only passes `NSFetchRequest` as a parameter to "MainListView" 3. The fetch result is set using `_result = FetchRequest(fetchRequest: fetchRequest)` in "MainListView"'s `init()`. This works, but I’m not sure if 3. is correct? – Sebastian Jul 24 '22 at 19:08

0 Answers0