'Modifying a @FetchRequest
Xcode beta 5 introduced @FetchRequest for SwiftUI.
I have a View, which has a @FetchRequest. The NSFetchRequest is created within a manager that makes a static instance of itself available to property wrappers (they cannot use self). The same instance is passed to the view at creation.
What I want is for the FetchRequest to be updated, when self.manager.reverse.toggle() is called, in order for the view to change its ordering of objects. Unfortunately it seems like Manager.fetchRequest() is only called once and then never again, even when I create new objects and transition between different views.
I am looking for suggestions on how to modify a fetch request that is made with the new property wrapper, so that I can resort my objects based on user actions.
struct MainView: some View {
@ObservedObject var manager: Manager
@FetchRequest(fetchRequest: Manager.shared.fetchRequest())
var ts: FetchedResults
var body: some View {
NavigationView {
List(ts, id: \.self) { t in
Text(t.name)
}
}.navigationBarItems(trailing:
Button(action: {
self.manager.reverse.toggle()
}) { Text("Reverse")}
}
}
final class Manager: ObservableObject {
@Published var reverse: Bool = false
// Since property wrappers cannot use self, we make a shared instance
// available statically. This same instance is also given to the view.
public static let shared = Manager()
func fetchRequest(reverse: Bool = false) -> NSFetchRequest<T> {
let request: NSFetchRequest<T> = T.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(
key: "name",
ascending: !self.reverse
)
]
return request
}
}
Solution 1:[1]
This is a very good question! I have been trying to do this and other very similar things with little success. I'm really hoping that this issue will get a suitable resolution in an upcoming beta. However, in the meantime, it is possible to switch between two @FetchRequests to accomplish what you wish to do.
Below is working code from a sample App I'm working on. I use a Button to toggle a Boolean @State, then use that state to pick the appropriate @FetchResults. I don't really like it, but it actually works quite well.
import SwiftUI
import CoreData
struct EntityListView : View {
@Environment(\.editMode) var editMode
@State var sortAscending: Bool = true
@Environment(\.managedObjectContext) var context: NSManagedObjectContext
@FetchRequest(entity: Entity.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Entity.order, ascending: true)])
var ascendingEntities: FetchedResults<Entity>
@FetchRequest(entity: Entity.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Entity.order, ascending: false)])
var descendingEntities: FetchedResults<Entity>
var body: some View {
NavigationView {
List() {
Section(header: Text("All Entities".uppercased()))
{
ForEach(sortAscending ? ascendingEntities : descendingEntities) { entity in
NavigationLink(destination: EntityEditView(entity: entity)) {
HStack {
Text(entity.name)
.font(.headline)
Spacer()
Text(String(entity.order))
.font(.subheadline)
.foregroundColor(.gray)
}
}
}
.onMove(perform: self.move)
.onDelete(perform: self.delete)
}
HStack {
Spacer()
Button(action: { self.sortAscending.toggle() }) { Text("Reverse Sort") }
Spacer()
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(Text("Entities"), displayMode: .large)
.navigationBarItems(trailing: EditButton() )
}
}
}
Solution 2:[2]
To solve your problem you should wrap your MainView in a SuperView. The super view should observe the manager. When the manager change, the super view's body will be invoked and create a new instance of your MainView. It is when that instance is created you get a new @FeatchRequest.
struct SuperView: some View {
@ObservedObject var manager: Manager
var body: some View {
MainView()
}
}
I hope it works, I have not been able to test the code.
Solution 3:[3]
SwiftUI uses immutable structs which means the fetch request property cannot be changed. You need to pass the sort param to the struct init, and thus create a whole new struct when you want to change it.
I learned this here: How to fix “Cannot assign to property: 'self' is immutable”
SwiftUI’s views should be structs, which means they are immutable by default. If this were our own code we could mark methods using mutating to tell Swift they will change values, but we can’t do that in SwiftUI because it uses a computed property.
let fetchRequest: FetchRequest<Item>
var items: FetchedResults<Item> { fetchRequest.wrappedValue }
Now you can pass a FetchRequest struct configured with params into your View's init. This code can be seen in the SectionedFetchRequester header.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Chuck H |
| Solution 2 | ragnarius |
| Solution 3 |
