'@FetchRequest predicate is ignored if the context changes
I have a simple SwiftUI application with CoreData and two views. One view displays all "Place" objects. You can create new places and you can show the details for the place. Inside the second view you can add "PlaceItem"s to a place.
The problem is that, once a new "PlaceItem" is added to the viewContext, the @NSFetchRequest seems to forget about its additional predicates, which I set in onAppear. Then every place item is shown inside the details view. Once I update the predicate manually (the refresh button), only the items from the selected place are visible again.
Any idea how this can be fixed? Here's the code for my two views:
struct PlaceView: View {
@FetchRequest(sortDescriptors: []) private var places: FetchedResults<Place>
@Environment(\.managedObjectContext) private var viewContext
var body: some View {
NavigationView {
List(places) { place in
NavigationLink {
PlaceItemsView(place: place)
} label: {
Text(place.name ?? "")
}
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
let place = Place(context: viewContext)
place.name = NSUUID().uuidString
try! viewContext.save()
} label: {
Label("Add", systemImage: "plus")
}
}
}
.navigationTitle("Places")
}
}
struct PlaceItemsView: View {
@ObservedObject var place: Place
@State var searchText = ""
@FetchRequest(sortDescriptors: []) private var items: FetchedResults<PlaceItem>
@Environment(\.managedObjectContext) private var viewContext
func updatePredicate() {
var predicates = [NSPredicate]()
predicates.append(NSPredicate(format: "place == %@", place))
if !searchText.isEmpty {
predicates.append(NSPredicate(format: "name CONTAINS %@", searchText))
}
items.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates)
}
var body: some View {
NavigationView {
List(items) { item in
Text(item.name ?? "");
}
}
.onAppear(perform: updatePredicate)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
let item = PlaceItem(context: viewContext)
item.place = place
item.name = NSUUID().uuidString
try! viewContext.save()
} label: {
Label("Add", systemImage: "plus")
}
}
ToolbarItem(placement: .navigationBarLeading) {
Button(action: updatePredicate) {
Label("Refresh", systemImage: "arrow.clockwise")
}
}
}
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search or add articles …"
)
.onAppear(perform: updatePredicate)
.onChange(of: searchText, perform: { _ in
updatePredicate()
})
.navigationTitle(place.name ?? "")
}
}
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
var body: some View {
NavigationView {
PlaceView()
}
}
}
Thanks!
Solution 1:[1]
Rather than relying on onAppear, you might have more luck defining your fetchRequest in PlaceItemView’s initializer, e.g.:
struct PlaceItemsView: View {
@ObservedObject private var place: Place
@FetchRequest private var items: FetchedResults<PlaceItem>
init(place: Place) {
self.place = place
self._items = FetchRequest(
entity: PlaceItem.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "place == %@", place)
)
}
Solution 2:[2]
It's best to break Views up by the data they need, its called having a tighter invalidation and solves your problem of updating the fetch request too! e.g.
struct PlaceItemsView {
@FetchRequest private var items: FetchedResults<PlaceItem>
var body: some View {
List(items) { item in
Text(item.name ?? "")
}
}
}
// body called when either searchText is different or its a different place (not when the place's properties change because no need to)
struct SearchPlaceView {
let searchText: String
let place: Place
var predicate: NSPredicate {
var predicates = [NSPredicate]()
predicates.append(NSPredicate(format: "place == %@", place))
if !searchText.isEmpty {
predicates.append(NSPredicate(format: "name CONTAINS %@", searchText))
}
return NSCompoundPredicate(type: .and, subpredicates: predicates)
}
var body: some View {
PlaceItemsView(items:FetchRequest<PlaceItem>(sortDescriptors:[], predicate:predicate))
}
}
// body called when either the place has a change or search text changes
struct PlaceItemsView: View {
@Environment(\.managedObjectContext) private var viewContext
@ObservedObject var place: Place
@State var searchText = ""
var body: some View {
NavigationView {
SearchPlaceView(searchTest:searchText, place: place)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
let item = PlaceItem(context: viewContext)
item.place = place
item.name = NSUUID().uuidString
try! viewContext.save()
} label: {
Label("Add", systemImage: "plus")
}
}
ToolbarItem(placement: .navigationBarLeading) {
Button(action: updatePredicate) {
Label("Refresh", systemImage: "arrow.clockwise")
}
}
}
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search or add articles …"
)
.navigationTitle(place.name ?? "")
}
}
Always try to break the Views up by the smallest data their body needs, try to not think about breaking them up by screens or areas of a screen which is a very common mistake.
Solution 3:[3]
import UIKit import CoreData
class LoginViewController: UIViewController, UITableViewDataSource, UITableViewDelegate{
//MARK: - Outlets
@IBOutlet weak var LogInDataTableView: UITableView!
@IBOutlet weak var btnLogIn: UIButton!
@IBOutlet weak var btnAdd: UIButton!
//MARK: - Variables
var fetchedCoreData = [NSManagedObject]()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var name = String()
let placeHolderArray = ["username", "password"]
var isLogin = false
//MARK: - Methods and other functionalities
override func viewDidLoad() {
super.viewDidLoad()
btnLogIn.layer.cornerRadius = 20.0
LogInDataTableView.layer.cornerRadius = 20.0
LogInDataTableView.layer.borderWidth = 2.0
LogInDataTableView.layer.borderColor = UIColor.blue.cgColor
let tap = UITapGestureRecognizer(target: self, action: #selector(UIInputViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
}
//MARK: - dismiss keyboard when tap on screen
@objc func dismissKeyboard() {
//Causes the view (or one of its embedded text fields) to resign the first responder status.
view.endEditing(true)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.isNavigationBarHidden = true
fetchRequest()
}
//MARK: - fetch saved data from coredata
func fetchRequest() {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "EntityName")
request.returnsObjectsAsFaults = false
do {
let result = try context.fetch(request)
fetchedCoreData = result as! [NSManagedObject]
} catch {
print("error while fetch data from database")
}
}
//MARK: - login button click with validation
@IBAction func btnLogInClick(_ sender: Any) {
if loginData[0].isEmpty {
let alert = UIAlertController(title: "Alert", message: "Please enter username", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
} else if loginData[1].isEmpty {
let alert = UIAlertController(title: "Alert", message: "Please enter password", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
} else if loginData[2].isEmpty{
let alert = UIAlertController(title: "Alert", message: "Please enter id", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
} else {
let registeredUser = clinicData.contains { objc in
objc.value(forKey: "username") as! String == loginData[0] && objc.value(forKey: "password") as! String == loginData[1]
}
if registeredUser == true {
for data in clinicData {
if data.value(forKey: "username") as! String == loginData[0] && data.value(forKey: "password") as! String == loginData[1] {
clinicName = data.value(forKey: "clinic") as! String
let addClinicVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "HomeScreenViewController") as! HomeScreenViewController
addClinicVC.clinicName = clinicName
self.navigationController?.pushViewController(addClinicVC, animated: true)
}
}
} else {
//MARK: - alert
let alert = UIAlertController(title: "Alert", message: "username and password mismatch", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
}
}
}
@IBAction func didTapAddClinic(_ sender: UIButton) {
let addVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "NewScreenViewController") as! NewScreenViewController
self.navigationController?.pushViewController(addVC, animated: true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return placeHolderArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LoginTableViewCell", for: indexPath) as! LoginTableViewCell
cell.txtLoginData.tag = indexPath.row
cell.txtLoginData.placeholder = placeHolderArray[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
}
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 | ScottM |
| Solution 2 | |
| Solution 3 |
