Edit 9/10/2023: I have a new answer that is simpler because does not require the extra View and the predicate is still not lost even if the View containing the fetch is re-init, e.g.
struct MessageView: View {
let team: Team // or @ObservedObject if you want to show its properties.
private var request = FetchRequest<Message>(
sortDescriptors: [SortDescriptor(\.createdAt)],
predicate: NSPredicate(value: false),
animation: .default)
private var messages: FetchedResults<Message> {
request.wrappedValue.nsPredicate = NSPredicate(format: "team = %@", team) // setting the same predicate again does not seem to cause a refetch (according to SQL debug output)
// here you could also update the sort from a state.
return request.wrappedValue
}
...
This pattern of splitting the fetch and results is actually documented in the header. However applying a predicate is not documented, but it is non-mutating so should be acceptable.
Old answer:
With SwiftUI it is important that the View struct does not appear to be changed otherwise body
will be called needlessly which in the case of @FetchRequest
also hits the database. SwiftUI checks for changes in View structs simply using equality and calls body
if not equal, i.e. if any property has changed. On iOS 14, even if @FetchRequest
is recreated with the same parameters, it results in a View struct that is different thus fails SwiftUI's equality check and causes the body to be recomputed when normally it wouldn’t be. @AppStorage
and @SceneStorage
also have this problem so I find it strange that @State
which most people probably learn first does not! Anyway, we can workaround this with a wrapper View with properties that do not change, which can stop SwiftUI's diffing algorithm in its tracks:
struct ContentView: View {
@State var teamName "Team" // source of truth, could also be @AppStorage if would like it to persist between app launches.
@State var counter = 0
var body: some View {
VStack {
ChatView(teamName:teamName) // its body will only run if teamName is different, so not if counter being changed was the reason for this body to be called.
Text("Count \(counter)")
}
}
}
struct ChatView: View {
let teamName: String
var body: some View {
// ChatList body will be called every time but this ChatView body is only run when there is a new teamName so that's ok.
ChatList(messages: FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Message.createdAt, ascending: true)], predicate: NSPredicate(format: "team.name = %@", teamName)))
}
}
struct ChatList : View {
@FetchRequest var messages: FetchedResults<Message>
var body: some View {
ForEach(messages) { message in
Text("Message at \(message.createdAt!, formatter: messageFormatter)")
}
}
}
Edit: it might be possible to achieve the same thing using EquatableView
instead of the wrapper View
to allow SwiftUI to do its diffing on the teamName
only and not the FetchRequest
var. More info here:
https://swiftwithmajid.com/2020/01/22/optimizing-views-in-swiftui-using-equatableview/