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!